289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type {
|
|
DesignAsset,
|
|
DesignCompilationIssue,
|
|
DesignGenerationTask,
|
|
DesignSpecificationValues,
|
|
} from '../../shared/image-workspace';
|
|
import {
|
|
DESIGN_SPECIFICATION_PATHS,
|
|
YOUTH_FIELD_TIERS,
|
|
projectYouthCreationCard,
|
|
} from '@/pages/ImageCanvas/youth-form-projection';
|
|
import {
|
|
designFormFixture,
|
|
designQuoteFixture,
|
|
designValuesFixture,
|
|
designWorkspaceFixture,
|
|
} from '../fixtures/design-workspace-v2';
|
|
|
|
function workspaceWithValues(values: DesignSpecificationValues) {
|
|
return designWorkspaceFixture({
|
|
form: designFormFixture({
|
|
specification: {
|
|
schema_version: 1,
|
|
values,
|
|
field_decisions: {},
|
|
},
|
|
}),
|
|
turns: [],
|
|
});
|
|
}
|
|
|
|
function emptyValues(): DesignSpecificationValues {
|
|
const values = structuredClone(designValuesFixture);
|
|
values.intent.media = null;
|
|
values.intent.creation_intent = null;
|
|
values.intent.purpose = null;
|
|
values.intent.channel = null;
|
|
values.intent.audience = null;
|
|
values.content.concept = null;
|
|
values.output.aspect_ratio = null;
|
|
values.output.orientation = null;
|
|
return values;
|
|
}
|
|
|
|
function taskFixture(
|
|
overrides: Partial<DesignGenerationTask> = {},
|
|
): DesignGenerationTask {
|
|
const quote = designQuoteFixture();
|
|
return {
|
|
taskId: 'task-1',
|
|
quoteId: quote.quoteId,
|
|
status: 'running',
|
|
taskRevision: 1,
|
|
specificationRevision: quote.specificationRevision,
|
|
specificationRevisionId: quote.specificationRevisionId,
|
|
specificationDigest: quote.specificationDigest,
|
|
medium: quote.medium,
|
|
compilerVersion: quote.compilerVersion,
|
|
outputSummary: quote.outputSummary,
|
|
maximumCustomerChargeAtoms: quote.maximumCustomerChargeAtoms,
|
|
customerBilling: {
|
|
quotedAtoms: 1200,
|
|
heldAtoms: 1200,
|
|
chargedAtoms: 0,
|
|
refundedAtoms: 0,
|
|
pendingResolutionAtoms: 0,
|
|
},
|
|
customerHoldExpiresAt: '2030-01-02T03:04:05.000Z',
|
|
resolutionDeadlineAt: null,
|
|
progress: {
|
|
stage: 'generating',
|
|
completedRequiredSteps: 0,
|
|
totalRequiredSteps: 1,
|
|
},
|
|
executionHealth: 'normal',
|
|
cancellation: { state: 'available', outcome: null },
|
|
issues: [],
|
|
resultAssetIds: [],
|
|
nextActions: ['cancel'],
|
|
createdAt: '2026-09-02T09:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('youth Design projection', () => {
|
|
it('classifies every current Specification field into exactly one presentation tier', () => {
|
|
expect(Object.keys(YOUTH_FIELD_TIERS).sort()).toEqual(
|
|
[...DESIGN_SPECIFICATION_PATHS].sort(),
|
|
);
|
|
expect(new Set(Object.values(YOUTH_FIELD_TIERS))).toEqual(new Set(['A', 'B', 'C']));
|
|
});
|
|
|
|
it('keeps a fresh Workspace simple instead of projecting professional defaults', () => {
|
|
const model = projectYouthCreationCard(workspaceWithValues(emptyValues()));
|
|
|
|
expect(model.phase).toBe('empty');
|
|
expect(model.summary).toBeNull();
|
|
expect(model.medium).toBeNull();
|
|
expect(model.destination).toBeNull();
|
|
expect(model.style).toBeNull();
|
|
expect(model.references).toEqual([]);
|
|
expect(model.readinessMessage).toBe('等你的第一个想法');
|
|
});
|
|
|
|
it('projects a concise card from canonical creator meaning', () => {
|
|
const values = structuredClone(designValuesFixture);
|
|
values.content.concept = '一只熊猫宇航员在月球上踢球';
|
|
values.content.emotional_tone = '有趣、梦幻、颜色明亮';
|
|
values.visual.art_direction = '手绘故事书';
|
|
values.constraints.must_include = ['远处的地球'];
|
|
values.constraints.must_avoid = ['真实人物'];
|
|
values.output.aspect_ratio = '9:16';
|
|
values.output.orientation = 'portrait';
|
|
const model = projectYouthCreationCard(workspaceWithValues(values));
|
|
|
|
expect(model.phase).toBe('ready');
|
|
expect(model.summary).toBe('一只熊猫宇航员在月球上踢球');
|
|
expect(model.medium).toBe('image');
|
|
expect(model.destination?.label).toBe('手机竖屏');
|
|
expect(model.style?.label).toBe('手绘故事书');
|
|
expect(model.mustInclude).toEqual(['远处的地球']);
|
|
expect(model.mustAvoid).toEqual(['真实人物']);
|
|
expect(model.readinessMessage).toBe('想法已经清楚,可以检查制作方案');
|
|
expect(model.canRequestQuote).toBe(true);
|
|
});
|
|
|
|
it('invites a server check without claiming an incomplete idea can already be made', () => {
|
|
const values = structuredClone(designValuesFixture);
|
|
values.content.concept = '一只会修理月亮的小狐狸';
|
|
values.output.aspect_ratio = null;
|
|
values.output.orientation = null;
|
|
|
|
const model = projectYouthCreationCard(workspaceWithValues(values));
|
|
|
|
expect(model.phase).toBe('ready');
|
|
expect(model.canRequestQuote).toBe(true);
|
|
expect(model.readinessMessage).toBe('想法已经清楚,可以检查制作方案');
|
|
expect(model.readinessMessage).not.toContain('可以开始制作');
|
|
});
|
|
|
|
it('projects concrete quote blockers without exposing server decision prompts', () => {
|
|
const workspace = designWorkspaceFixture({
|
|
form: designFormFixture({
|
|
decisionPrompts: [{
|
|
id: 'question-1',
|
|
kind: 'question',
|
|
basedOnSpecificationRevisionId: 'specification-revision-3',
|
|
targetPaths: ['content.emotional_tone'],
|
|
title: '你想让画面更像哪一种?',
|
|
body: '选一个最接近的感觉,也可以交给我。',
|
|
options: [
|
|
{ id: 'cartoon', label: '有趣的卡通', description: null },
|
|
{ id: 'cinema', label: '像电影一样', description: null },
|
|
{ id: 'delegate', label: '你帮我选', description: null },
|
|
],
|
|
}, {
|
|
id: 'question-2',
|
|
kind: 'question',
|
|
basedOnSpecificationRevisionId: 'specification-revision-3',
|
|
targetPaths: ['visual.palette'],
|
|
title: '第二个问题',
|
|
body: '现在不该显示',
|
|
options: [],
|
|
}],
|
|
}),
|
|
});
|
|
const blockers: DesignCompilationIssue[] = [{
|
|
code: 'aspect_ratio_required',
|
|
message: 'Choose an aspect ratio supported by the frozen route.',
|
|
severity: 'blocker',
|
|
path: 'output.aspect_ratio',
|
|
}];
|
|
const model = projectYouthCreationCard(workspace, { quoteBlockers: blockers });
|
|
|
|
expect(model.phase).toBe('exploring');
|
|
expect(model.blockers).toEqual([{
|
|
...blockers[0],
|
|
message: '先选择作品形状。',
|
|
}]);
|
|
expect(JSON.stringify(model.blockers)).not.toContain('frozen route');
|
|
expect(model.blockerCount).toBe(1);
|
|
expect(model.readinessMessage).toBe('还差 1 个关键选择');
|
|
expect(model.canRequestQuote).toBe(false);
|
|
});
|
|
|
|
it('uses Quote and Task resource state without overwriting the creative summary', () => {
|
|
const workspace = designWorkspaceFixture({
|
|
form: designFormFixture({ activeQuotes: [designQuoteFixture()] }),
|
|
});
|
|
const confirmingModel = projectYouthCreationCard(workspace);
|
|
expect(confirmingModel.phase).toBe('confirming');
|
|
expect(confirmingModel.canRequestQuote).toBe(false);
|
|
|
|
const generating = designWorkspaceFixture({ tasks: [taskFixture()] });
|
|
const generatingModel = projectYouthCreationCard(generating);
|
|
expect(generatingModel.phase).toBe('generating');
|
|
expect(generatingModel.summary).toBe(designValuesFixture.content.concept);
|
|
expect(generatingModel.readinessMessage).toBe('正在制作,请稍等一下');
|
|
|
|
const resultAsset: DesignAsset = {
|
|
assetId: 'asset-result',
|
|
workspaceId: 'workspace-1',
|
|
role: 'generated',
|
|
mediaType: 'image',
|
|
mimeType: 'image/png',
|
|
width: 1024,
|
|
height: 1365,
|
|
durationMilliseconds: null,
|
|
generationTaskId: 'task-1',
|
|
createdAt: '2026-09-02T09:05:00.000Z',
|
|
contentPath: '/api/design/assets/asset-result/content',
|
|
};
|
|
const completed = designWorkspaceFixture({
|
|
tasks: [taskFixture({
|
|
status: 'succeeded',
|
|
progress: {
|
|
stage: 'completed',
|
|
completedRequiredSteps: 1,
|
|
totalRequiredSteps: 1,
|
|
},
|
|
resultAssetIds: ['asset-result'],
|
|
nextActions: ['view_result', 'revise_design', 'generate_again'],
|
|
})],
|
|
assets: [resultAsset],
|
|
});
|
|
expect(projectYouthCreationCard(completed).phase).toBe('result');
|
|
expect(projectYouthCreationCard(completed).readinessMessage).toBe('完成了,看看你的作品吧');
|
|
});
|
|
|
|
it('summarizes bound references and video choices without exposing technical timing', () => {
|
|
const values = structuredClone(designValuesFixture);
|
|
values.intent.media = 'video';
|
|
values.video.total_duration_seconds = 10;
|
|
values.video.pacing = '很有活力';
|
|
values.video.end_state = '机器人端出一盘星星饼干';
|
|
values.video.shots = [{
|
|
id: 'shot-1',
|
|
start_milliseconds: 0,
|
|
end_milliseconds: 10_000,
|
|
story_beat: '机器人在厨房里做饭',
|
|
framing: 'close-up',
|
|
camera: 'dolly-in',
|
|
subject_motion: '快速搅拌',
|
|
environment_motion: null,
|
|
focus_behavior: null,
|
|
transition: null,
|
|
}];
|
|
values.references = [{
|
|
id: 'reference-1',
|
|
asset_id: 'asset-reference',
|
|
asset_revision: null,
|
|
role: 'subject_identity',
|
|
preserve: ['圆脸', '红围巾'],
|
|
adapt: [],
|
|
do_not_copy: [],
|
|
reviewed_observations: ['白色背景'],
|
|
}];
|
|
const workspace = workspaceWithValues(values);
|
|
workspace.assets = [{
|
|
assetId: 'asset-reference',
|
|
workspaceId: 'workspace-1',
|
|
role: 'uploaded',
|
|
mediaType: 'image',
|
|
mimeType: 'image/png',
|
|
width: 600,
|
|
height: 600,
|
|
durationMilliseconds: null,
|
|
generationTaskId: null,
|
|
createdAt: '2026-09-02T09:00:00.000Z',
|
|
contentPath: '/api/design/assets/asset-reference/content',
|
|
}];
|
|
|
|
const model = projectYouthCreationCard(workspace);
|
|
expect(model.video).toEqual({
|
|
durationSeconds: 10,
|
|
pacing: '很有活力',
|
|
endState: '机器人端出一盘星星饼干',
|
|
storyBeats: ['机器人在厨房里做饭'],
|
|
});
|
|
expect(model.references[0]).toMatchObject({
|
|
assetId: 'asset-reference',
|
|
preserve: ['圆脸', '红围巾'],
|
|
});
|
|
expect(JSON.stringify(model)).not.toContain('milliseconds');
|
|
expect(JSON.stringify(model)).not.toContain('dolly-in');
|
|
});
|
|
});
|