import { describe, expect, it } from 'vitest'; import { createGameCreationAppManifest, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, type GameCreationAgentRunTrace, } from '../../../packages/shared/src/contracts/gameCreationApp'; import { deriveAgentStatusCards, isAbsoluteProjectPath, needsInitializedChatProject, parseRememberInput, pendingCommandDetail, resolveChatProjectPath, resolvePendingCommandProjectPath, } from '../src/App'; describe('AI 游戏创作聊天记忆命令', () => { it('recognizes local project absolute paths across desktop platforms', () => { expect(isAbsoluteProjectPath('/tmp/game')).toBe(true); expect(isAbsoluteProjectPath('C:\\Games\\demo')).toBe(true); expect(isAbsoluteProjectPath('D:/Games/demo')).toBe(true); expect(isAbsoluteProjectPath('relative-game')).toBe(false); }); it('defaults /remember to long memory and supports short and blackboard scopes', () => { expect(parseRememberInput('主角喜欢反弹弹幕')).toEqual({ scope: 'long', content: '主角喜欢反弹弹幕', }); expect(parseRememberInput('short 本轮先修输入手感')).toEqual({ scope: 'short', content: '本轮先修输入手感', }); expect(parseRememberInput('长期 保留厨房主题')).toEqual({ scope: 'long', content: '保留厨房主题', }); expect(parseRememberInput('project 覆盖后的长期设定')).toEqual({ scope: 'long', content: '覆盖后的长期设定', }); expect(parseRememberInput('blackboard 共享美术约束')).toEqual({ scope: 'blackboard', content: '共享美术约束', }); expect(parseRememberInput('黑板 统一使用俯视角')).toEqual({ scope: 'blackboard', content: '统一使用俯视角', }); }); it('describes append and replace memory writes before confirmation', () => { expect( pendingCommandDetail( { id: 'memory.write', scope: 'long', content: '保留厨房主题', mode: 'append', }, '/tmp/game', ), ).toBe('追加到 /tmp/game/memory/project.md'); expect( pendingCommandDetail( { id: 'memory.write', scope: 'short', content: '覆盖本轮上下文', mode: 'replace', }, '/tmp/game', ), ).toBe('覆盖保存到 /tmp/game/memory/session.md'); expect( pendingCommandDetail( { id: 'memory.write', scope: 'blackboard', content: '共享美术约束', mode: 'append', }, '/tmp/game', ), ).toBe('追加到 /tmp/game/memory/blackboard.md'); }); it('requires an initialized local project before chat memory writes', () => { expect(resolveChatProjectPath(null)).toBeNull(); expect(resolveChatProjectPath({ projectPath: '/tmp/game' })).toBe( '/tmp/game', ); expect(resolveChatProjectPath({ projectPath: 'relative-game' })).toBeNull(); expect( resolveChatProjectPath({ projectPath: '/tmp/bad\u0007game' }), ).toBeNull(); expect(parseRememberInput('long')).toEqual({ scope: 'long', content: '', }); expect(parseRememberInput('short 覆盖本轮上下文')).toEqual({ scope: 'short', content: '覆盖本轮上下文', }); expect( pendingCommandDetail( { id: 'memory.delete', scope: 'short', }, '/tmp/game', ), ).toBe('删除 /tmp/game/memory/session.md'); expect( pendingCommandDetail( { id: 'memory.delete', scope: 'blackboard', }, '/tmp/game', ), ).toBe('删除 /tmp/game/memory/blackboard.md'); }); it('describes local generation side effects before confirmation', () => { expect( pendingCommandDetail( { id: 'game.generate_draft', prompt: '做一个厨房弹幕游戏', }, '/tmp/game', ), ).toBe( '调用 LLM Planner / Generator,编排 6 组角色 brief,写入 /tmp/game/game、assets、memory、exports,通过 Evaluator 和自检后启动本地 HTTP 预览并载入客户端运行视图', ); }); it('describes preview side effects before confirmation', () => { expect(pendingCommandDetail({ id: 'preview.start' }, '/tmp/game')).toBe( '启动 /tmp/game/game/ 并载入客户端运行视图', ); expect(pendingCommandDetail({ id: 'preview.open' }, '/tmp/game')).toBe( '打开 /tmp/game 的当前本地预览', ); }); it('requires a project before chat commands write or run local artifacts', () => { expect(needsInitializedChatProject('game.generate_draft')).toBe(true); expect(needsInitializedChatProject('asset.upload')).toBe(true); expect(needsInitializedChatProject('agent.kill')).toBe(true); expect(needsInitializedChatProject('agent.retry')).toBe(true); expect(needsInitializedChatProject('agent.resume')).toBe(true); expect(needsInitializedChatProject('command.run_limited')).toBe(true); expect(needsInitializedChatProject('preview.open')).toBe(true); expect(needsInitializedChatProject('preview.start')).toBe(true); expect(needsInitializedChatProject('canvas.project_sync')).toBe(true); expect(needsInitializedChatProject('canvas.export_import')).toBe(true); expect(needsInitializedChatProject('memory.write')).toBe(true); expect(needsInitializedChatProject('memory.delete')).toBe(true); expect(needsInitializedChatProject('project.checkpoint')).toBe(true); expect(needsInitializedChatProject('project.restore')).toBe(true); expect(needsInitializedChatProject('project.policy_write')).toBe(true); expect(needsInitializedChatProject('project.create')).toBe(false); expect(needsInitializedChatProject('canvas.project_open')).toBe(false); }); it('describes project safety commands before confirmation', () => { expect( pendingCommandDetail({ id: 'project.checkpoint' }, '/tmp/game'), ).toBe('保存 /tmp/game 当前项目快照'); expect( pendingCommandDetail( { id: 'project.restore', checkpointId: 'checkpoint-1' }, '/tmp/game', ), ).toBe('从 checkpoint-1 恢复 /tmp/game 的已跟踪项目文件'); expect( pendingCommandDetail( { id: 'project.policy_write', policy: { deniedCommands: ['file.write'], confirmCommands: [], }, }, '/tmp/game', ), ).toBe( '写入 /tmp/game/.agent/policy.json · 拒绝:file.write · 确认:无 · Agent:无 Agent 独立策略', ); }); it('describes canvas asset imports before confirmation', () => { expect( pendingCommandDetail( { id: 'canvas.asset_import', localPath: 'assets/hero.png', canvasProjectId: 'canvas-project-1', canvasAssetId: '', canvasAssetObjectId: 'asset-object-1', kind: 'character', mediaType: 'image/png', }, '/tmp/game', ), ).toBe( '导入 /tmp/game/assets/hero.png · 画板 canvas-project-1 / object:asset-object-1 · character · image/png', ); expect( pendingCommandDetail( { id: 'canvas.export_import', exportPath: '/tmp/canvas-export.zip', canvasProjectId: 'canvas-project-1', }, '/tmp/game', ), ).toBe( '导入 /tmp/canvas-export.zip 到 /tmp/game/assets/canvas-imports/ · 画板 canvas-project-1', ); }); it('describes agent run lifecycle controls before confirmation', () => { expect(pendingCommandDetail({ id: 'agent.kill' }, '/tmp/game')).toBe( '标记 /tmp/game/.agent/run.latest.json 为 killed,并写入 activity/output', ); expect(pendingCommandDetail({ id: 'agent.retry' }, '/tmp/game')).toBe( '使用 /tmp/game/.agent/run.latest.json 的目标重新运行一次', ); expect( pendingCommandDetail( { id: 'agent.resume', detail: '继续修复输入监听' }, '/tmp/game', ), ).toBe( '附加说明「继续修复输入监听」,继续运行 /tmp/game/.agent/run.latest.json 的目标', ); }); it('shows the authorized project path in pending write command details', () => { expect( resolvePendingCommandProjectPath( { id: 'game.generate_draft', prompt: '做一个厨房弹幕游戏' }, { projectPath: '/authorized/game' }, '/draft/dev-input', ), ).toBe('/authorized/game'); expect( resolvePendingCommandProjectPath( { id: 'project.create', projectPath: '/new/game' }, { projectPath: '/authorized/game' }, '/draft/dev-input', ), ).toBe('/new/game'); }); it('derives per-agent status cards from manifest and latest trace steps', () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-agent-status', commandId: 'game.generate_draft', status: 'passed', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '像素动作', coordination: 'filesystem', steps: [ { pass: 1, agent: 'Planner', phase: 'plan', taskId: 'design-director', group: 'design', role: 'Director', status: 'completed', inputPaths: [], outputPaths: ['.agent/spec.md'], summary: '拆解完成', toolCalls: [], }, ], artifacts: [], taskGraph: { goal: '像素动作', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: manifest.tasks, }, passPlans: [], nextStep: 'manual-playtest', error: null, updatedAt: 1, } satisfies GameCreationAgentRunTrace; const traced = deriveAgentStatusCards(manifest, trace); expect(traced[0]).toMatchObject({ id: 'design-director', title: '拆解创作方向', status: 'completed', summary: '拆解完成', }); const fallback = deriveAgentStatusCards(manifest, null); expect(fallback[0]?.summary).toBe('创作目标、范围和专业组分工明确'); }); });