162 lines
8.3 KiB
TypeScript
162 lines
8.3 KiB
TypeScript
/** Offline connectivity diagnostic; not in the normal unit suite.
|
|
* Actual React controls + Zustand commands -> stdin bridge -> actual Python
|
|
* Specification kernel / production capability builder / compiler. Upload and
|
|
* transport are simulated. No live gateway, reasoner, paid task or provider.
|
|
*/
|
|
import { spawnSync } from 'node:child_process';
|
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
import { YouthCreationCard } from '@/pages/ImageCanvas/YouthCreationCard';
|
|
import { DesignPlanHistory } from '@/pages/ImageCanvas/DesignPlanHistory';
|
|
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
|
import {
|
|
ImageWorkspaceApiError, submitImageWorkspaceCommand, uploadImageWorkspaceAsset,
|
|
} from '@/lib/image-workspace';
|
|
import {
|
|
designCapabilitiesFixture, designFormFixture, designQuoteFixture, designWorkspaceFixture,
|
|
} from '../../tests/fixtures/design-workspace-v2';
|
|
import type {
|
|
DesignAsset, DesignCompilationIssue, DesignSpecification, DesignUserFieldOperation,
|
|
} from '../../shared/image-workspace';
|
|
|
|
vi.mock('@/lib/image-workspace', async (original) => ({
|
|
...await original<typeof import('@/lib/image-workspace')>(),
|
|
submitImageWorkspaceCommand: vi.fn(),
|
|
uploadImageWorkspaceAsset: vi.fn(),
|
|
resolveImageWorkspaceAssetUrl: vi.fn().mockResolvedValue('blob:offline-preview'),
|
|
}));
|
|
vi.mock('@/lib/host-api', async (original) => ({
|
|
...await original<typeof import('@/lib/host-api')>(), setDesktopBackgroundLease: vi.fn(),
|
|
}));
|
|
vi.mock('@/stores/auth', () => ({
|
|
useAuthStore: { getState: () => ({ isAuthenticated: () => true }) },
|
|
}));
|
|
|
|
function serverStep(
|
|
specification: DesignSpecification | null,
|
|
inputKind: 'direct_edit' | 'prepare_generation',
|
|
operations: DesignUserFieldOperation[] = [],
|
|
) {
|
|
const result = spawnSync(process.env.DESIGN_DIAGNOSTIC_PYTHON!, [
|
|
'-X', 'utf8', '-m', 'scripts.diagnostics.design_media_blockers', '--json-step',
|
|
], {
|
|
cwd: process.env.DESIGN_DIAGNOSTIC_SERVER,
|
|
input: JSON.stringify({ specification, input_kind: inputKind, operations }), encoding: 'utf8',
|
|
});
|
|
if (result.status !== 0) throw new Error(result.stderr || String(result.error));
|
|
return JSON.parse(result.stdout) as {
|
|
specification: DesignSpecification; ready: boolean; blockers: DesignCompilationIssue[];
|
|
};
|
|
}
|
|
|
|
const asset: DesignAsset = {
|
|
assetId: 'image-1', workspaceId: 'workspace-1', role: 'uploaded', mediaType: 'image',
|
|
mimeType: 'image/png', width: 720, height: 1280, durationMilliseconds: null,
|
|
generationTaskId: null, createdAt: '2026-09-16T00:00:00Z',
|
|
contentPath: '/api/design/assets/image-1/content',
|
|
};
|
|
|
|
function LiveCard() {
|
|
const workspace = useImageWorkspaceStore((state) => state.workspace)!;
|
|
const blockers = useImageWorkspaceStore((state) => state.quoteBlockers);
|
|
return <YouthCreationCard
|
|
workspace={workspace}
|
|
quoteBlockers={blockers}
|
|
generationAvailable
|
|
generationOptions={designCapabilitiesFixture.generationOptions}
|
|
/>;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
useImageWorkspaceStore.getState().reset();
|
|
});
|
|
afterEach(cleanup);
|
|
|
|
it('reaches video confirmation through the real prepare-generation bridge', async () => {
|
|
const initial = serverStep(null, 'direct_edit', [
|
|
{ kind: 'set', path: 'intent.media', value: 'image' },
|
|
{ kind: 'set', path: 'content.concept', value: '沙漠里的小动物围圈玩游戏,天上的云在飘。' },
|
|
{ kind: 'set', path: 'output.aspect_ratio', value: '9:16' },
|
|
]);
|
|
let workspace = designWorkspaceFixture({ form: designFormFixture({
|
|
specification: initial.specification, activeQuotes: [],
|
|
}) });
|
|
const commands: string[] = [];
|
|
expect(initial.ready).toBe(true);
|
|
useImageWorkspaceStore.setState({
|
|
workspace, activeWorkspaceId: workspace.workspace.workspaceId, status: 'ready',
|
|
});
|
|
vi.mocked(uploadImageWorkspaceAsset).mockResolvedValue(asset);
|
|
vi.mocked(submitImageWorkspaceCommand).mockImplementation(async (command) => {
|
|
commands.push(command.kind === 'apply_input' ? command.input.kind : command.kind);
|
|
const inputKind = command.kind === 'apply_input' ? command.input.kind : 'direct_edit';
|
|
const operations = command.kind === 'apply_input' && (
|
|
command.input.kind === 'direct_edit' || command.input.kind === 'prepare_generation'
|
|
) ? command.input.operations ?? [] : [];
|
|
const step = serverStep(workspace.form.specification, inputKind, operations);
|
|
workspace = {
|
|
...workspace,
|
|
assets: operations.some((operation) => operation.path === 'references')
|
|
? [asset] : workspace.assets,
|
|
workspace: { ...workspace.workspace, workspaceViewRevision: workspace.workspace.workspaceViewRevision + 1 },
|
|
form: { ...workspace.form, specification: step.specification,
|
|
specificationRevision: workspace.form.specificationRevision + 1,
|
|
directionRevision: workspace.form.directionRevision + 1 },
|
|
};
|
|
useImageWorkspaceStore.setState({ workspace, quoteBlockers: step.blockers });
|
|
if (!step.ready && command.kind === 'request_quote') {
|
|
throw new ImageWorkspaceApiError(409, 'design_quote_blocked', 'offline blocked');
|
|
}
|
|
if (command.kind === 'request_quote') {
|
|
const quote = designQuoteFixture();
|
|
workspace.form.activeQuotes = [{ ...quote, medium: 'video',
|
|
specificationRevision: workspace.form.specificationRevision,
|
|
outputSummary: { ...quote.outputSummary, medium: 'video', aspectRatio: '9:16',
|
|
durationSeconds: 6, deliveryFormat: 'mp4' } }];
|
|
}
|
|
return { workspace, runId: 'offline-run', clientOperationId: command.clientOperationId };
|
|
});
|
|
render(<LiveCard />);
|
|
fireEvent.change(screen.getByRole('combobox', { name: '类型' }), { target: { value: 'video' } });
|
|
await waitFor(() => expect(useImageWorkspaceStore.getState().workspace?.form.specification.values.intent.media).toBe('video'));
|
|
fireEvent.change(screen.getByLabelText('选择参考图片'), {
|
|
target: { files: [new File(['offline-upload'], 'start.png', { type: 'image/png' })] },
|
|
});
|
|
await screen.findByText('已选用');
|
|
expect(useImageWorkspaceStore.getState().workspace?.form.specification.values.video.first_frame_asset_id).toBe('image-1');
|
|
expect(useImageWorkspaceStore.getState().workspace?.form.specification.values.video.shots.length).toBe(1);
|
|
await waitFor(() => expect(screen.getByRole('button', { name: '准备制作方案' })).toBeEnabled());
|
|
fireEvent.click(screen.getByRole('button', { name: '准备制作方案' }));
|
|
await waitFor(() => expect(commands).toContain('request_quote'));
|
|
console.log('FLOW_COMMANDS', commands);
|
|
console.log('FLOW_BLOCKERS', useImageWorkspaceStore.getState().quoteBlockers.map((item) => item.code));
|
|
expect(commands.filter((kind) => kind === 'prepare_generation')).toHaveLength(3);
|
|
expect(useImageWorkspaceStore.getState().quoteBlockers).toEqual([]);
|
|
expect(screen.getByRole('button', { name: '确认并开始制作' })).toBeEnabled();
|
|
fireEvent.click(screen.getByRole('button', { name: '确认并开始制作' }));
|
|
await waitFor(() => expect(commands).toContain('confirm_generation'));
|
|
});
|
|
|
|
it('lets a user play a completed video directly in the active history rail', async () => {
|
|
const video: DesignAsset = { ...asset, assetId: 'video-1', role: 'generated',
|
|
mediaType: 'video', mimeType: 'video/mp4', durationMilliseconds: 6000,
|
|
generationTaskId: 'task-video', contentPath: '/api/design/assets/video-1/content' };
|
|
const workspace = designWorkspaceFixture();
|
|
workspace.assets = [video];
|
|
// Presentation fixture only; no claim that the provider generated these bytes.
|
|
workspace.tasks = [{
|
|
taskId: 'task-video', status: 'succeeded', medium: 'video', createdAt: video.createdAt,
|
|
maximumCustomerChargeAtoms: 500, resultAssetIds: ['video-1'], issues: [],
|
|
outputSummary: { medium: 'video', aspectRatio: '9:16', outputCount: 1, durationSeconds: 6 },
|
|
progress: { stage: 'completed', completedRequiredSteps: 2, totalRequiredSteps: 2 },
|
|
} as typeof workspace.tasks[number]];
|
|
const { container } = render(<DesignPlanHistory workspace={workspace} />);
|
|
await screen.findAllByLabelText('视频作品预览');
|
|
fireEvent.click(screen.getByText('制作记录'));
|
|
const players = [...container.querySelectorAll('video')];
|
|
expect(players.length).toBeGreaterThan(0);
|
|
expect(players.some((player) => player.controls)
|
|
|| screen.queryByRole('button', { name: /播放|查看视频/ }) !== null).toBe(true);
|
|
});
|