import { describe, expect, it } from 'vitest'; import { createGameCreationAppManifest, type RunnableGameVersion, } from '../../../packages/shared/src/contracts/gameCreationApp'; import { projectResourcesFromReadModels } from '../src/view/project-development/resourceProjectionModel'; function runnableVersionFixture( projectId: string, versionId: string, projectRevision: number, overrides: Partial = {}, ): RunnableGameVersion { return { schemaVersion: 'game-creator-runnable-version.v1', versionId, projectId, parentVersionId: null, projectRevision, artifactPath: `.agent/runnable-versions/${versionId}/artifact`, artifactSha256: 'a'.repeat(64), entryPath: 'game/index.html', resourceBindings: [], createdReason: 'initial', validation: { staticSmokePassed: true, previewValidatePassed: true, playtestPassed: true, agentId: 'program-agent', runId: `run-${projectRevision}`, reportPath: `.agent/runtime/browser-validations/program-agent/run-${projectRevision}/1/validation.json`, }, createdAt: projectRevision * 100, ...overrides, }; } 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', '.agent/passes/pass-*/groups/design/director.md', 'memory/draft?.md', 'assets/[ab].png', 'memory/{draft,final}.md', ]; audioTask.status = 'completed'; audioTask.artifacts = ['audio/unregistered-bgm.wav']; manifest.assets = [ { 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.runnableVersions = [ runnableVersionFixture('resource-projection', 'version-1', 7, { resourceBindings: [ { slotId: 'background-music', resourceId: 'registered-bgm' }, ], }), ]; manifest.currentRunnableVersionId = 'version-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: '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: 'version:version-1', category: 'version' }, ]), ); expect( resources.some(({ id }) => [ 'task:design-foundation:build/game.bundle', 'task:audio-asset-plan:audio/unregistered-bgm.wav', 'task:design-foundation:.agent/passes/pass-*/groups/design/director.md', 'task:design-foundation:memory/draft?.md', 'task:design-foundation:assets/[ab].png', 'task:design-foundation:memory/{draft,final}.md', '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.runnableVersions = [ runnableVersionFixture('stable-resource-id', 'stable-version', 1), ]; manifest.currentRunnableVersionId = 'stable-version'; 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.runnableVersions = [ runnableVersionFixture('version-projection', 'version-root', 2), runnableVersionFixture('version-projection', 'version-child', 3, { parentVersionId: 'version-root', createdReason: 'agent-revision', }), ]; manifest.currentRunnableVersionId = 'version-child'; 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'); }); });