import { describe, expect, it } from 'vitest'; import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import { projectResourcesFromReadModels, projectResourceTypeLabel, } from '../src/view/project-development/resourceProjectionModel'; describe('项目资源投影', () => { it('只把明确资源投影到固定分类,未知任务产物不会伪装成项目版本', () => { const manifest = createGameCreationAppManifest( 'resource-projection', '资源投影测试', ); const designTask = manifest.tasks.find( (task) => task.id === 'design-foundation', ); const audioTask = manifest.tasks.find( (task) => task.id === 'audio-asset-plan', ); if (!designTask || !audioTask) { throw new Error('缺少资源投影测试任务'); } designTask.status = 'completed'; designTask.artifacts = [ 'memory/game-design.md', 'assets/ui-preview.svg', 'build/game.bundle', ]; audioTask.status = 'completed'; audioTask.artifacts = ['audio/unregistered-bgm.wav']; manifest.assets = [ { id: 'game-entry', kind: 'game-entry', mediaType: 'text/html', localPath: 'game/index.html', source: { kind: 'generated' }, }, { id: 'registered-bgm', kind: 'bgm', mediaType: 'audio/wav', localPath: 'audio/registered-bgm.wav', source: { kind: 'generated' }, }, { id: 'character-animation', kind: 'character-animation', mediaType: 'application/json', localPath: 'assets/character-animation.json', source: { kind: 'generated' }, }, { id: 'unknown-binary', kind: 'binary', mediaType: 'application/octet-stream', localPath: 'build/game.bundle', source: { kind: 'generated' }, }, ]; manifest.versions = [ { versionId: 'version-1', parentVersionId: null, projectRevision: 7, resourceBindings: [ { slotId: 'background-music', resourceId: 'registered-bgm' }, ], createdReason: 'initial', createdAt: 1, }, ]; const resources = projectResourcesFromReadModels( manifest, [ { fileName: 'rules.yaml', mediaType: 'application/yaml', localPath: 'imports/rules.yaml', status: 'imported', }, { fileName: 'voice.ogg', mediaType: 'audio/ogg', localPath: 'imports/voice.ogg', status: 'imported', }, { fileName: 'archive.zip', mediaType: 'application/zip', localPath: 'imports/archive.zip', status: 'imported', }, ], [ { agentId: 'design-foundation', runId: 'final-run', label: '玩法策划 Agent', title: '玩法文档回执', content: '回执正文', updatedAt: 1, }, ], ); expect(resources.map(({ id, category }) => ({ id, category }))).toEqual( expect.arrayContaining([ { id: 'task:design-foundation:memory/game-design.md', category: 'document', }, { id: 'asset:game-entry', category: 'code' }, { id: 'task:design-foundation:assets/ui-preview.svg', category: 'art', }, { id: 'asset:registered-bgm', category: 'audio' }, { id: 'asset:character-animation', category: 'art' }, { id: 'attachment:imports/rules.yaml', category: 'document' }, { id: 'attachment:imports/voice.ogg', category: 'audio' }, { id: 'agent-result:design-foundation:final-run', category: 'document', }, { id: 'task:audio-asset-plan:audio/unregistered-bgm.wav', category: 'audio', }, { id: 'version:version-1', category: 'version' }, ]), ); expect( resources.some(({ id }) => [ 'task:design-foundation:build/game.bundle', 'asset:unknown-binary', 'attachment:imports/archive.zip', ].includes(id), ), ).toBe(false); expect( resources.filter(({ category }) => category === 'version'), ).toHaveLength(1); }); it('显示名称变化不会改变正式资源身份', () => { const manifest = createGameCreationAppManifest( 'stable-resource-id', '稳定资源身份测试', ); manifest.versions = [ { versionId: 'stable-version', parentVersionId: null, projectRevision: 1, resourceBindings: [], createdReason: 'initial', createdAt: 1, }, ]; const first = projectResourcesFromReadModels( manifest, [], [ { agentId: 'design-foundation', runId: 'stable-run', label: '策划 Agent', title: '旧标题', content: '正文', updatedAt: 1, }, ], ); const second = projectResourcesFromReadModels( manifest, [], [ { agentId: 'design-foundation', runId: 'stable-run', label: '策划 Agent', title: '新标题', content: '正文', updatedAt: 2, }, ], ); expect(first.map(({ id }) => id)).toEqual(second.map(({ id }) => id)); }); it('只从 manifest 投影版本并保留直接父子关系', () => { const manifest = createGameCreationAppManifest( 'version-projection', '版本投影测试', ); manifest.versions = [ { versionId: 'version-root', parentVersionId: null, projectRevision: 2, resourceBindings: [], createdReason: 'initial', createdAt: 100, }, { versionId: 'version-child', parentVersionId: 'version-root', projectRevision: 3, resourceBindings: [], createdReason: 'agent-revision', createdAt: 200, }, ]; const versions = projectResourcesFromReadModels(manifest, [], []).filter( (resource) => resource.category === 'version', ); expect(versions.map((version) => version.label)).toEqual([ '版本 1', '版本 2', ]); expect(versions[0]?.version?.childVersionIds).toEqual(['version-child']); expect(versions[1]?.version?.parentVersionId).toBe('version-root'); }); it('按稳定优先级推导资源卡类型标识并为未知类型兜底', () => { expect( projectResourceTypeLabel({ category: 'version', subtype: 'project-version', path: 'versions/v1', mediaType: '正式项目版本', }), ).toBe('项目版本'); expect( projectResourceTypeLabel({ category: 'document', subtype: 'agent-result', path: '专业 Agent 文本回执', mediaType: 'Agent 历史文本回执', }), ).toBe('Agent 回执'); expect( projectResourceTypeLabel({ category: 'document', subtype: 'task-artifact', path: 'memory/plan.md', mediaType: '项目文档', }), ).toBe('文档'); expect( projectResourceTypeLabel({ category: 'audio', subtype: 'task-artifact', path: 'audio/theme.wav', mediaType: '音乐音效产物', }), ).toBe('音频'); expect( projectResourceTypeLabel({ category: 'art', subtype: 'task-artifact', path: 'assets/hero.svg', mediaType: '美术产物', }), ).toBe('SVG'); expect( projectResourceTypeLabel({ category: 'art', subtype: 'task-artifact', path: 'assets/unknown', mediaType: '美术产物', }), ).toBe('图片'); expect( projectResourceTypeLabel({ category: 'art', subtype: 'character', path: 'assets/hero.svg', mediaType: 'image/svg+xml', }), ).toBe('SVG'); expect( projectResourceTypeLabel({ category: 'art', subtype: 'video', path: 'assets/intro.mp4', mediaType: 'video/mp4', }), ).toBe('视频'); expect( projectResourceTypeLabel({ category: 'art', subtype: 'character', path: 'assets/hero.png', mediaType: 'image/png', }), ).toBe('图片'); expect( projectResourceTypeLabel({ category: 'document', subtype: 'attachment', path: 'docs/rules.yaml', mediaType: 'application/yaml', }), ).toBe('文档'); expect( projectResourceTypeLabel({ category: 'document', subtype: 'unknown', path: 'data/blob', mediaType: 'application/octet-stream', }), ).toBe('文档'); expect( projectResourceTypeLabel({ category: 'code', subtype: 'source', path: 'game/main.ts', mediaType: 'text/typescript', }), ).toBe('游戏代码'); expect( projectResourceTypeLabel({ category: 'art', subtype: 'unknown', path: 'assets/blob', mediaType: 'application/octet-stream', }), ).toBe('图片'); expect( projectResourceTypeLabel({ category: 'unsupported' as never, subtype: 'unknown', path: 'data/blob', mediaType: 'application/octet-stream', }), ).toBe('未知'); }); });