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 { 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'); }); });