diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 0b02cacc2..15976a11c 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -1,7 +1,6 @@ /** @vitest-environment jsdom */ import { vi } from 'vitest'; -import { registerAgentRuntimeCommandTests } from './appSurface/agent-runtime.suite'; import { registerAuthTests } from './appSurface/auth.suite'; import { registerChatComposerControlTests } from './appSurface/chat-composer.suite'; import { registerDesignAgentSurfaceTests } from './appSurface/design-agent.suite'; @@ -12,11 +11,6 @@ import { registerRecentProjectsTests, } from './appSurface/home.suite'; import { registerPlanGddApprovalTests } from './appSurface/plan-gdd.suite'; -import { - registerCanvasAssetTests, - registerProjectAssetTests, -} from './appSurface/project-assets.suite'; -import { registerProjectCommandTests } from './appSurface/project-commands.suite'; import { registerProjectConversationTests } from './appSurface/project-conversation.suite'; import { registerProjectAgentStatusTests, @@ -35,8 +29,8 @@ import { registerToolCallGroupTests } from './appSurface/tool-call-group.suite'; * * 这里只替代"用户选了哪个目标路径"这一件事:`save` 按调用方给的建议文件名返回一个 * 固定的导出路径,让「下载」用例能断言工具条真的走到了 `save_local_project_asset_file`。 - * 声明必须留在入口文件:`project-assets.suite` 等模块比 `project-development.suite` - * 更早 import 工作台视图,放在别处注册会晚于真实模块实例化。 + * 声明必须留在入口文件:`project-conversation.suite` 等模块比 + * `project-development.suite` 更早 import 工作台视图,放在别处注册会晚于真实模块实例化。 */ vi.mock('@tauri-apps/plugin-dialog', () => ({ open: async () => null, @@ -56,10 +50,6 @@ describe('AI 游戏创作 App 界面边界', () => { registerProjectPreviewAndRunHistoryTests(); registerProjectConversationTests(); registerProjectAgentStatusTests(); - registerProjectCommandTests(); - registerProjectAssetTests(); - registerAgentRuntimeCommandTests(); - registerCanvasAssetTests(); registerPlanGddApprovalTests(); registerDesignAgentSurfaceTests(); registerToolCallGroupTests(); diff --git a/apps/ai-game-creator-shell/tests/appSurface/agent-runtime.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/agent-runtime.suite.ts deleted file mode 100644 index 3b628ca11..000000000 --- a/apps/ai-game-creator-shell/tests/appSurface/agent-runtime.suite.ts +++ /dev/null @@ -1,1928 +0,0 @@ -import { - composerText, - composerValue, - createGameCreationAppManifest, - createGameCreationAppSeedTasks, - emptyProjectPolicy, - expect, - fireEvent, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, - it, - renderAppAt, - screen, - setComposerText, - submitChat, - vi, - waitFor, - within, -} from './harness'; - -export function registerAgentRuntimeCommandTests() { - it('reads agent loop trace from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-chat-trace', - commandId: 'game.generate_draft', - status: 'needs-revision', - passes: 2, - maxPasses: 3, - toolCallCount: 18, - maxToolCalls: 128, - stopReason: 'evaluator-feedback', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'Planner -> Orchestrator -> Generator -> Evaluator', - steps: [ - { - pass: 2, - agent: 'Orchestrator', - phase: 'plan', - taskId: 'code-director', - group: 'code', - role: 'Code', - status: 'completed', - inputPaths: ['.agent/findings.md'], - outputPaths: ['.agent/passes/pass-2/task-graph.json'], - summary: '按返工路由重跑程序和预览', - toolCalls: [ - { - toolId: 'agent.task_graph.plan_pass', - status: 'completed', - inputPaths: ['.agent/findings.md'], - outputPaths: ['.agent/passes/pass-2/task-graph.json'], - summary: 'repair pass', - }, - ], - }, - { - pass: 2, - agent: 'Generator', - phase: 'generate', - taskId: 'code-director', - group: 'code', - role: 'Code', - status: 'completed', - inputPaths: ['.agent/spec.md', '.agent/findings.md'], - outputPaths: ['.agent/passes/pass-2/draft.json'], - summary: '生成修复草案', - toolCalls: [], - }, - ], - artifacts: [ - { - path: '.agent/passes/pass-2/task-graph.json', - sizeBytes: 256, - checksum: 'fnv1a64:chat-trace', - }, - ], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: ['code-director'], - activeTaskIds: [ - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - ], - carriedTaskIds: ['design-director'], - repairFocus: ['缺少输入监听'], - repairRoutes: [ - { - issue: '缺少输入监听', - taskIds: [ - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - ], - reason: 'code-repair', - }, - ], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [ - { - pass: 2, - mode: 'repair', - summary: '第 2 轮按 Evaluator 反馈返工', - activeTaskIds: [ - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - ], - carriedTaskIds: ['design-director'], - dependencyWaves: [ - ['code-director'], - ['code-prototype'], - ['quality-review'], - ['preview-readiness'], - ], - repairFocus: ['缺少输入监听'], - repairRoutes: [ - { - issue: '缺少输入监听', - taskIds: [ - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - ], - reason: 'code-repair', - }, - ], - }, - ], - nextStep: 'repair-next-pass', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/trace'); - - expect(await screen.findByText(/Run:run-chat-trace/)).not.toBeNull(); - expect( - screen.getByText(/状态:needs-revision · 2\/3 轮 · evaluator-feedback/), - ).not.toBeNull(); - expect( - screen.getByText( - /active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/, - ), - ).not.toBeNull(); - expect(screen.getByText(/返工路线:code-repair/)).not.toBeNull(); - expect(screen.getByText(/编排轮次:/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - }); - - it('shows an empty agent trace message before the first run', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/trace'); - - expect( - await screen.findByText( - '暂无最近 Agent trace。先生成一次游戏草案后再查看。', - ), - ).not.toBeNull(); - expect(screen.getByLabelText('聊天').textContent).not.toContain( - 'No such file or directory', - ); - }); - - it('requires confirmation for agent trace reads when project policy asks for it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-chat-trace-confirm', - commandId: 'game.generate_draft', - status: 'passed', - passes: 1, - maxPasses: 3, - toolCallCount: 3, - maxToolCalls: 128, - stopReason: 'evaluator-passed', - goal: '做一个厨房弹幕游戏', - coordination: 'Planner -> Generator', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个厨房弹幕游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: [], - }, - passPlans: [], - nextStep: 'preview-playtest', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/trace'); - - expect( - await screen.findByText('准备读取 Agent run trace。'), - ).not.toBeNull(); - expect(screen.getByText('agent.trace_read')).not.toBeNull(); - expect( - screen.getByText('读取 /tmp/authorized-game 的最近 Agent run trace'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ commandId: 'agent.trace_read' }), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText(/Run:run-chat-trace-confirm/), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.pending', - commandId: 'agent.trace_read', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'agent.trace_read', - }); - }); - - it('controls agent run lifecycle from chat through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-control-chat', - commandId: 'game.generate_draft', - status: 'pending', - lifecycleStatus: 'pending', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'retry-requested', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'filesystem', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'rerun-now', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'control_agent_run') { - const action = String(args?.action ?? ''); - const detail = String(args?.detail ?? ''); - const resultByAction = { - status: { - status: 'pending', - lifecycleStatus: 'pending', - nextStep: 'rerun-now', - message: 'run run-control-chat 当前状态:pending / pending', - }, - kill: { - status: 'killed', - lifecycleStatus: 'killed', - nextStep: 'resume-or-retry', - message: 'run run-control-chat 已标记为 killed', - }, - retry: { - status: 'passed', - lifecycleStatus: 'done', - nextStep: 'preview-playtest', - message: - 'run run-control-chat 已重试,已重新运行为 run-control-chat-next:game/index.html', - }, - resume: { - status: 'passed', - lifecycleStatus: 'done', - nextStep: 'preview-playtest', - message: `run run-control-chat 已恢复:${detail},已重新运行为 run-control-chat-next:game/index.html`, - }, - }[action]; - if (!resultByAction) { - throw new Error(`unexpected agent run action ${action}`); - } - return { - runId: 'run-control-chat', - ...resultByAction, - activityPath: '/tmp/authorized-game/.agent/activity.jsonl', - outputPath: '/tmp/authorized-game/.agent/output.jsonl', - contextBundlePath: - '/tmp/authorized-game/.agent/context.bundle.json', - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - fireEvent.click(screen.getByRole('button', { name: '输出' })); - expect(await composerText()).toBe('/read .agent/output.jsonl'); - fireEvent.click(screen.getByRole('button', { name: '活动' })); - expect(await composerText()).toBe('/read .agent/activity.jsonl'); - fireEvent.click(screen.getByRole('button', { name: '上下文包' })); - expect(await composerText()).toBe('/read .agent/context.bundle.json'); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ relativePath: '.agent/context.bundle.json' }), - ); - - await submitChat('/agent-status'); - expect( - await screen.findByText( - /run run-control-chat 当前状态:pending \/ pending/, - ), - ).not.toBeNull(); - expect(screen.getByText(/run:run-control-chat/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '读取 Run 输出' })); - expect(await composerText()).toBe('/read .agent/output.jsonl'); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ relativePath: '.agent/output.jsonl' }), - ); - await setComposerText(screen.getByLabelText('创作想法'), ''); - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'status', - detail: undefined, - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'command.auto', - commandId: 'agent.run_status', - }); - - await submitChat('/agent-kill'); - expect(screen.getByText('agent.kill')).not.toBeNull(); - expect( - screen.getByText( - '标记 /tmp/authorized-game/.agent/run.latest.json 为 killed,并写入 activity/output', - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText(/run run-control-chat 已标记为 killed/), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'kill', - detail: undefined, - }); - - await submitChat('/agent-retry'); - expect(screen.getByText('agent.retry')).not.toBeNull(); - expect( - screen.getByText( - '使用 /tmp/authorized-game/.agent/run.latest.json 的目标重新运行一次', - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText( - /run run-control-chat 已重试,已重新运行为 run-control-chat-next/, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'retry', - detail: undefined, - }); - - await submitChat('/agent-resume 继续修复输入监听'); - expect(screen.getByText('agent.resume')).not.toBeNull(); - expect( - screen.getByText( - '附加说明「继续修复输入监听」,继续运行 /tmp/authorized-game/.agent/run.latest.json 的目标', - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText( - /run run-control-chat 已恢复:继续修复输入监听,已重新运行为 run-control-chat-next/, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'resume', - detail: '继续修复输入监听', - }); - }); - - it('shows an empty agent run status message before the first run', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'control_agent_run') { - throw new Error( - '读取 Agent run trace 失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/agent-status'); - - expect( - await screen.findByText( - '暂无最近 Agent run。先生成一次游戏草案后再查看状态。', - ), - ).not.toBeNull(); - expect(screen.getByLabelText('聊天').textContent).not.toContain( - 'No such file or directory', - ); - }); - - it('shows an empty agent run control message before the first run', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'control_agent_run') { - throw new Error( - '读取 Agent run trace 失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/agent-kill'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - '暂无可控制的 Agent run。先生成一次游戏草案后再操作。', - ), - ).not.toBeNull(); - expect(screen.getByLabelText('聊天').textContent).not.toContain( - 'No such file or directory', - ); - }); - - it('blocks pending agent run control when project policy denies it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: ['agent.kill'], - confirmCommands: [], - }, - }; - } - if (command === 'control_agent_run') { - throw new Error('should not control agent run after deny'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/agent-kill'); - expect(screen.getByText('agent.kill')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('项目权限策略拒绝执行:agent.kill'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'control_agent_run', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_permission_log', - expect.objectContaining({ - event: 'permission.confirm', - commandId: 'agent.kill', - }), - ); - }); - - it('requires project policy confirmation before controlling agent run lifecycle', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-kill-confirm', - commandId: 'game.generate_draft', - status: 'killed', - lifecycleStatus: 'killed', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'killed', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'filesystem', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'rerun-now', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.kill'], - }, - }; - } - if (command === 'control_agent_run') { - return { - runId: 'run-kill-confirm', - status: 'killed', - lifecycleStatus: 'killed', - nextStep: 'rerun-now', - message: 'agent run killed', - activityPath: '/tmp/authorized-game/.agent/activity.jsonl', - outputPath: '/tmp/authorized-game/.agent/output.jsonl', - contextBundlePath: - '/tmp/authorized-game/.agent/context.bundle.json', - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/agent-kill'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('准备执行 Agent run 操作。')).not.toBeNull(); - expect(screen.getByText('agent.kill')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'control_agent_run', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'kill', - detail: undefined, - }); - }); - }); - - it('requires project policy confirmation before reading agent run status from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-status-confirm', - commandId: 'game.generate_draft', - status: 'pending', - lifecycleStatus: 'pending', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'filesystem', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'rerun-now', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.run_status'], - }, - }; - } - if (command === 'control_agent_run') { - return { - runId: 'run-status-confirm', - status: 'pending', - lifecycleStatus: 'pending', - nextStep: 'rerun-now', - message: 'run run-status-confirm 当前状态:pending / pending', - activityPath: '/tmp/authorized-game/.agent/activity.jsonl', - outputPath: '/tmp/authorized-game/.agent/output.jsonl', - contextBundlePath: - '/tmp/authorized-game/.agent/context.bundle.json', - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/agent-status'); - - expect(await screen.findByText('准备查看 Agent run 状态。')).not.toBeNull(); - expect(screen.getByText('agent.run_status')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'control_agent_run', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - /run run-status-confirm 当前状态:pending \/ pending/, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'status', - detail: undefined, - }); - }); - - it('cancels agent run status policy confirmation without reading status', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.run_status'], - }, - }; - } - if (command === 'control_agent_run') { - throw new Error('should wait for agent run status confirmation'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/agent-status'); - - const agentStatusCommand = await screen.findByText('agent.run_status'); - fireEvent.click( - within( - agentStatusCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - expect( - await screen.findByText('run: 已取消读取 Agent run 状态'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'control_agent_run', - expect.anything(), - ); - }); - - it('cancels pending agent run control without invoking native control', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'control_agent_run') { - throw new Error('should not control agent run after cancel'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/agent-kill'); - const agentCommand = screen.getByText('agent.kill'); - fireEvent.click( - within(agentCommand.closest('.pending-command') as HTMLElement).getByRole( - 'button', - { name: '取消' }, - ), - ); - - expect( - await screen.findByText('run: 已取消 Agent run 操作'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'control_agent_run', - expect.anything(), - ); - }); - - it('confirms before reading trace after agent run status from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-status-trace-confirm', - commandId: 'game.generate_draft', - status: 'pending', - lifecycleStatus: 'pending', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'filesystem', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'rerun-now', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'control_agent_run') { - return { - runId: 'run-status-trace-confirm', - status: 'pending', - lifecycleStatus: 'pending', - nextStep: 'rerun-now', - message: 'run run-status-trace-confirm 当前状态:pending / pending', - activityPath: '/tmp/authorized-game/.agent/activity.jsonl', - outputPath: '/tmp/authorized-game/.agent/output.jsonl', - contextBundlePath: - '/tmp/authorized-game/.agent/context.bundle.json', - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/agent-status'); - - expect( - await screen.findByText( - /run run-status-trace-confirm 当前状态:pending \/ pending/, - ), - ).not.toBeNull(); - expect(await screen.findByText('agent.trace_read')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'status', - detail: undefined, - }); - expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - }); - }); - - it('controls agent run lifecycle from the agent status panel', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-panel-control', - commandId: 'game.generate_draft', - status: 'pending', - lifecycleStatus: 'pending', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'retry-requested', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'filesystem', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'rerun-now', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'control_agent_run') { - const action = String(args?.action ?? ''); - return { - runId: 'run-panel-control', - status: action === 'kill' ? 'killed' : 'pending', - lifecycleStatus: action === 'kill' ? 'killed' : 'pending', - nextStep: action === 'retry' ? 'preview-playtest' : 'rerun-now', - message: `panel ${action}`, - activityPath: '/tmp/authorized-game/.agent/activity.jsonl', - outputPath: '/tmp/authorized-game/.agent/output.jsonl', - contextBundlePath: - '/tmp/authorized-game/.agent/context.bundle.json', - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '状态' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'status', - detail: undefined, - }); - }); - - fireEvent.click(screen.getByRole('button', { name: '终止' })); - expect(screen.getByText('agent.kill')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'kill', - detail: undefined, - }); - }); - - fireEvent.click(screen.getByRole('button', { name: '重试' })); - expect(screen.getByText('agent.retry')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'retry', - detail: undefined, - }); - }); - - fireEvent.click(screen.getByRole('button', { name: '继续' })); - expect(screen.getByText('agent.resume')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'resume', - detail: undefined, - }); - }); - const composerInput = screen.getByLabelText('创作想法'); - fireEvent.click(screen.getByRole('button', { name: '继续说明' })); - expect(await composerValue(composerInput)).toBe('/agent-resume '); - }); - - it('requires project policy confirmation before reading agent run status from the panel', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-panel-status-confirm', - commandId: 'game.generate_draft', - status: 'pending', - lifecycleStatus: 'pending', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'filesystem', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'rerun-now', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.run_status'], - }, - }; - } - if (command === 'control_agent_run') { - return { - runId: 'run-panel-status-confirm', - status: 'pending', - lifecycleStatus: 'pending', - nextStep: 'rerun-now', - message: 'panel status', - activityPath: '/tmp/authorized-game/.agent/activity.jsonl', - outputPath: '/tmp/authorized-game/.agent/output.jsonl', - contextBundlePath: - '/tmp/authorized-game/.agent/context.bundle.json', - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '状态' })); - - expect(await screen.findByText('agent.run_status')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'control_agent_run', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'status', - detail: undefined, - }); - }); - }); - - it('confirms before reading trace after agent run status from the panel', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-panel-status-trace-confirm', - commandId: 'game.generate_draft', - status: 'pending', - lifecycleStatus: 'pending', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个反弹弹幕厨房游戏', - coordination: 'filesystem', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个反弹弹幕厨房游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'rerun-now', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'control_agent_run') { - return { - runId: 'run-panel-status-trace-confirm', - status: 'pending', - lifecycleStatus: 'pending', - nextStep: 'rerun-now', - message: 'panel status', - activityPath: '/tmp/authorized-game/.agent/activity.jsonl', - outputPath: '/tmp/authorized-game/.agent/output.jsonl', - contextBundlePath: - '/tmp/authorized-game/.agent/context.bundle.json', - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '状态' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('control_agent_run', { - projectPath: '/tmp/authorized-game', - action: 'status', - detail: undefined, - }); - }); - expect(await screen.findByText('agent.trace_read')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - }); - }); - - it('manages long memory from chat through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let projectMemory = '# 项目长期记忆\n'; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_game_memory') { - return { - scope: 'long', - path: 'memory/project.md', - content: projectMemory, - exists: true, - }; - } - if (command === 'write_local_game_memory') { - projectMemory = String(args?.content ?? ''); - return { - scope: 'long', - path: 'memory/project.md', - content: projectMemory, - exists: true, - }; - } - if (command === 'delete_local_game_memory') { - projectMemory = ''; - return { - scope: 'long', - path: 'memory/project.md', - content: '', - exists: false, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/memory'); - expect(await screen.findByText(/长期记忆:/)).not.toBeNull(); - - await submitChat('/remember long 保留厨房主题'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已追加长期记忆。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'long', - }); - expect(invoke).toHaveBeenCalledWith('write_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'long', - content: '# 项目长期记忆\n- 保留厨房主题\n', - }); - - await submitChat('/memory-set long 覆盖后的长期记忆'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已保存长期记忆。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('write_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'long', - content: '覆盖后的长期记忆', - }); - - await submitChat('/forget-memory'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已删除长期记忆。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('delete_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'long', - }); - }); - - it('requires project policy confirmation before reading memory from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['memory.read'], - }, - }; - } - if (command === 'read_local_game_memory') { - return { - scope: args?.scope, - path: 'memory/project.md', - content: '# 项目长期记忆\n', - exists: true, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/memory'); - - expect(await screen.findByText('准备读取项目记忆。')).not.toBeNull(); - expect(screen.getByText('memory.read')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_game_memory', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/长期记忆:/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'long', - }); - }); - - it('cancels project memory read confirmation from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['memory.read'], - }, - }; - } - if (command === 'read_local_game_memory') { - throw new Error('should wait for confirmation'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/memory'); - - expect(await screen.findByText('准备读取项目记忆。')).not.toBeNull(); - const memoryReadCommand = screen.getByText('memory.read'); - fireEvent.click( - within( - memoryReadCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - await waitFor(() => { - expect(screen.queryByText('memory.read')).toBeNull(); - }); - expect(screen.getByText('已取消读取项目记忆。')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_game_memory', - expect.anything(), - ); - }); - - it('rejects unknown memory scopes before reading or deleting memory', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - - await submitChat('/memory typo'); - expect( - await screen.findByText('格式:/memory [short|long|blackboard]'), - ).not.toBeNull(); - - await submitChat('/forget-memory typo'); - expect( - await screen.findByText('格式:/forget-memory [short|long|blackboard]'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_game_memory', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'delete_local_game_memory', - expect.anything(), - ); - expect(screen.queryByText('memory.delete')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - }); - - it('manages blackboard memory from chat through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let blackboardMemory = '# 项目黑板\n'; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_game_memory') { - return { - scope: args?.scope, - path: 'memory/blackboard.md', - content: blackboardMemory, - exists: true, - }; - } - if (command === 'write_local_game_memory') { - blackboardMemory = String(args?.content ?? ''); - return { - scope: args?.scope, - path: 'memory/blackboard.md', - content: blackboardMemory, - exists: true, - }; - } - if (command === 'delete_local_game_memory') { - blackboardMemory = ''; - return { - scope: args?.scope, - path: 'memory/blackboard.md', - content: '', - exists: false, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/memory blackboard'); - expect(await screen.findByText(/黑板记忆:/)).not.toBeNull(); - - await submitChat('/remember blackboard 共享美术约束'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已追加黑板记忆。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('write_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'blackboard', - content: '# 项目黑板\n- 共享美术约束\n', - }); - - await submitChat('/memory-set 黑板 统一使用俯视角'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已保存黑板记忆。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('write_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'blackboard', - content: '统一使用俯视角', - }); - - await submitChat('/forget-memory blackboard'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已删除黑板记忆。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('delete_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'blackboard', - }); - }); -} diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-assets.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-assets.suite.ts deleted file mode 100644 index 253799014..000000000 --- a/apps/ai-game-creator-shell/tests/appSurface/project-assets.suite.ts +++ /dev/null @@ -1,1737 +0,0 @@ -import { - composerText, - createGameCreationAppManifest, - emptyProjectPolicy, - expect, - fireEvent, - it, - renderAppAt, - screen, - submitChat, - vi, - waitFor, - within, -} from './harness'; - -export function registerProjectAssetTests() { - it('uploads files from chat through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const uploadedManifest = { - ...manifest, - assets: [ - { - id: 'asset-hero', - kind: 'upload', - mediaType: 'image/png', - localPath: 'assets/uploads/hero.png', - source: { kind: 'uploaded' }, - }, - ], - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'upload_local_asset') { - const projectPath = String(args?.projectPath ?? ''); - return { - id: 'asset-hero', - localPath: 'assets/uploads/hero.png', - absolutePath: `${projectPath}/assets/uploads/hero.png`, - manifestPath: `${projectPath}/.agent/manifest.json`, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return uploadedManifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - const file = new File(['hero'], 'hero.png', { type: 'image/png' }); - Object.defineProperty(file, 'arrayBuffer', { - value: async () => Uint8Array.from([104, 101, 114, 111]).buffer, - }); - fireEvent.change(screen.getByLabelText('上传'), { - target: { - files: [file], - }, - }); - expect(screen.getByText(/asset\.upload/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('已保存文件:assets/uploads/hero.png'), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('upload_local_asset', { - projectPath: '/tmp/authorized-game', - fileName: 'hero.png', - mediaType: 'image/png', - bytes: [104, 101, 114, 111], - }); - - await submitChat('/assets'); - expect( - await screen.findByText(/upload · assets\/uploads\/hero\.png · uploaded/), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '读取首个资产' })); - expect(await composerText()).toBe('/read assets/uploads/hero.png'); - expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { - projectPath: '/tmp/authorized-game', - commandId: 'asset.list', - }); - }); - - it('summarizes audio assets from chat without reading project files', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - manifest.assets.push( - { - id: 'audio-uploaded', - kind: 'audio', - mediaType: 'audio/wav', - localPath: 'assets/audio/sfx.wav', - source: { kind: 'uploaded' }, - }, - { - id: 'audio-canvas', - kind: 'canvas', - mediaType: 'audio/mpeg', - localPath: 'assets/canvas-sync/theme.mp3', - source: { kind: 'canvas', canvasProjectId: 'canvas-audio' }, - }, - { - id: 'audio-kind-fallback', - kind: 'sound-effect', - mediaType: 'application/octet-stream', - localPath: 'assets/audio/hit.bin', - source: { kind: 'generated' }, - }, - { - id: 'image-asset', - kind: 'image', - mediaType: 'image/png', - localPath: 'assets/hero.png', - source: { kind: 'generated' }, - }, - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_local_project_file') { - throw new Error('audio summary should not read project files'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/audio'); - - expect(await screen.findByText(/音频素材:3 个/)).not.toBeNull(); - expect(screen.getByText(/来源:上传 1、生成 1、画板 1/)).not.toBeNull(); - expect( - screen.getByText(/assets\/audio\/sfx\.wav · audio\/wav · 上传/), - ).not.toBeNull(); - expect( - screen.getByText( - /assets\/canvas-sync\/theme\.mp3 · audio\/mpeg · 画板 · 画板 canvas-audio/, - ), - ).not.toBeNull(); - expect( - screen.getByText( - /assets\/audio\/hit\.bin · application\/octet-stream · 生成/, - ), - ).not.toBeNull(); - const audioManifestDraftButtons = screen.getAllByRole('button', { - name: '读音频清单', - }); - fireEvent.click( - audioManifestDraftButtons[audioManifestDraftButtons.length - 1], - ); - expect(await composerText()).toBe('/read assets/manifest.audio.json'); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_manifest', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'register_local_asset', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'import_canvas_asset', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'run_limited_local_command', - expect.anything(), - ); - }); - - it('summarizes art assets from chat without reading project files', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - manifest.assets.push( - { - id: 'art-generated', - kind: 'image', - mediaType: 'image/png', - localPath: 'assets/generated/hero.png', - source: { kind: 'generated' }, - }, - { - id: 'art-canvas', - kind: 'character-animation', - mediaType: 'application/vnd.genarrative.image-sequence', - localPath: 'assets/canvas-sync/hero-run', - source: { kind: 'canvas', canvasProjectId: 'canvas-art' }, - }, - { - id: 'audio-asset', - kind: 'audio', - mediaType: 'audio/wav', - localPath: 'assets/audio/sfx.wav', - source: { kind: 'uploaded' }, - }, - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_local_project_file') { - throw new Error('art summary should not read project files'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/art'); - - expect(await screen.findByText(/美术素材:2 个/)).not.toBeNull(); - expect(screen.getByText(/来源:生成 1、画板 1/)).not.toBeNull(); - expect(screen.getByText(/画板来源:已接入/)).not.toBeNull(); - expect( - screen.getByText(/assets\/generated\/hero\.png · image\/png · 生成/), - ).not.toBeNull(); - expect( - screen.getByText( - /assets\/canvas-sync\/hero-run · application\/vnd\.genarrative\.image-sequence · 画板 · 画板 canvas-art/, - ), - ).not.toBeNull(); - const artManifestDraftButtons = screen.getAllByRole('button', { - name: '读美术清单', - }); - fireEvent.click( - artManifestDraftButtons[artManifestDraftButtons.length - 1], - ); - expect(await composerText()).toBe('/read assets/manifest.art.json'); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_manifest', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'generate_platform_art_asset', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'sync_canvas_project_assets', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'import_canvas_asset', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'register_local_asset', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'run_limited_local_command', - expect.anything(), - ); - }); - - it('registers an existing project asset from chat after confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'register_local_asset') { - const projectPath = String(args?.projectPath ?? ''); - return { - id: 'asset-existing', - localPath: String(args?.localPath ?? ''), - absolutePath: `${projectPath}/${String(args?.localPath ?? '')}`, - manifestPath: `${projectPath}/.agent/manifest.json`, - }; - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/asset-register assets/hero.png sprite image/png'); - - expect( - await screen.findByText('asset.register · assets/hero.png'), - ).not.toBeNull(); - expect( - screen.getByText( - '登记 /tmp/authorized-game/assets/hero.png · sprite · image/png', - ), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'register_local_asset', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('已登记资产:assets/hero.png'), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('register_local_asset', { - projectPath: '/tmp/authorized-game', - localPath: 'assets/hero.png', - kind: 'sprite', - mediaType: 'image/png', - sourceKind: 'generated', - canvasProjectId: '', - resourceId: '', - assetObjectId: '', - taskId: '', - prompt: '', - model: '', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'asset.register', - }); - }); - - it('cancels asset registration from chat before invoking Tauri', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'register_local_asset') { - throw new Error('should not register after cancel'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/asset-register assets/hero.png'); - const command = await screen.findByText('asset.register · assets/hero.png'); - fireEvent.click( - within(command.closest('.pending-command') as HTMLElement).getByRole( - 'button', - { name: '取消' }, - ), - ); - - expect(screen.getByText('已取消。')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'register_local_asset', - expect.anything(), - ); - await waitFor(() => - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.cancel', - commandId: 'asset.register', - }), - ); - }); - - it('requires project policy confirmation before registering assets from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['asset.register'], - }, - }; - } - if (command === 'register_local_asset') { - const projectPath = String(args?.projectPath ?? ''); - return { - id: 'asset-existing', - localPath: String(args?.localPath ?? ''), - absolutePath: `${projectPath}/${String(args?.localPath ?? '')}`, - manifestPath: `${projectPath}/.agent/manifest.json`, - }; - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/asset-register assets/hero.png sprite image/png'); - - expect( - await screen.findByText('准备登记项目资产:assets/hero.png'), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('准备登记项目资产。')).not.toBeNull(); - expect( - screen.getByText('登记 /tmp/authorized-game/assets/hero.png'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'register_local_asset', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('已登记资产:assets/hero.png'), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('register_local_asset', { - projectPath: '/tmp/authorized-game', - localPath: 'assets/hero.png', - kind: 'sprite', - mediaType: 'image/png', - sourceKind: 'generated', - canvasProjectId: '', - resourceId: '', - assetObjectId: '', - taskId: '', - prompt: '', - model: '', - }); - }); - - it('rejects unsafe asset registration paths from chat before confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'register_local_asset') { - throw new Error('should not register unsafe path'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/asset-register ../hero.png'); - - expect( - await screen.findByText('资产路径必须是项目内相对路径。'), - ).not.toBeNull(); - expect(screen.queryByText(/asset\.register/)).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'register_local_asset', - expect.anything(), - ); - }); - - it('truncates long asset lists in chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const assetManifest = { - ...manifest, - assets: Array.from({ length: 22 }, (_, index) => ({ - id: `asset-${index + 1}`, - kind: 'generated', - mediaType: 'image/png', - localPath: `assets/generated/asset-${index + 1}.png`, - source: { kind: 'generated' }, - })), - } satisfies typeof manifest; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return assetManifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/assets'); - - expect(await screen.findByText(/本地项目资产:/)).not.toBeNull(); - expect(screen.getByText(/asset-20\.png/)).not.toBeNull(); - expect(screen.queryByText(/asset-21\.png/)).toBeNull(); - expect(screen.getByText(/还有 2 个资产/)).not.toBeNull(); - }); - - it('cancels asset list policy confirmation without reading assets', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['asset.list'], - }, - }; - } - if (command === 'get_local_game_manifest') { - throw new Error('should wait for asset list confirmation'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/assets'); - - const assetCommand = await screen.findByText('asset.list'); - fireEvent.click( - within(assetCommand.closest('.pending-command') as HTMLElement).getByRole( - 'button', - { name: '取消' }, - ), - ); - - expect(await screen.findByText('已取消读取项目资产')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_manifest', - expect.objectContaining({ commandId: 'asset.list' }), - ); - }); - - it('reads local project files from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_project_file') { - return { - path: String(args?.relativePath ?? ''), - absolutePath: `${String(args?.projectPath ?? '')}/${String( - args?.relativePath ?? '', - )}`, - content: '', - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/read game/index.html'); - - expect(await screen.findByText(/文件:game\/index\.html/)).not.toBeNull(); - const fileContent = screen.getByText(''); - expect(fileContent.tagName).toBe('CODE'); - expect(fileContent.closest('pre')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: 'game/index.html', - commandId: 'file.read', - }); - }); - - it('requires confirmation for file reads when project policy asks for it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['file.read'], - }, - }; - } - if (command === 'read_local_project_file') { - return { - path: String(args?.relativePath ?? ''), - absolutePath: `${String(args?.projectPath ?? '')}/${String( - args?.relativePath ?? '', - )}`, - content: '', - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/read game/index.html'); - - expect(await screen.findByText('准备读取项目文件。')).not.toBeNull(); - expect(screen.getByText('file.read')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ commandId: 'file.read' }), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/文件:game\/index\.html/)).not.toBeNull(); - const fileContent = screen.getByText(''); - expect(fileContent.tagName).toBe('CODE'); - expect(fileContent.closest('pre')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: 'game/index.html', - commandId: 'file.read', - }); - }); - - it('rejects unsafe project file reads before calling Tauri', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/read ../secret.txt'); - expect(screen.getByText('文件路径必须是项目内相对路径。')).not.toBeNull(); - - await submitChat('/read /tmp/secret.txt'); - expect(screen.getAllByText('文件路径必须是项目内相对路径。').length).toBe( - 2, - ); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ relativePath: '../secret.txt' }), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ relativePath: '/tmp/secret.txt' }), - ); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.objectContaining({ - message: expect.objectContaining({ - content: '文件路径必须是项目内相对路径。', - }), - }), - ); - }); - }); - - it('shows task decomposition from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/tasks'); - - expect(await screen.findByText(/任务拆分:/)).not.toBeNull(); - expect( - screen.getByText(/设计实现组 \/ Director:拆解创作方向 · 待处理/), - ).not.toBeNull(); - expect(screen.getByText(/下一步:设计实现组 \/ Director/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { - projectPath: '/tmp/authorized-game', - commandId: 'task.list', - }); - }); -} - -export function registerCanvasAssetTests() { - it('imports canvas assets from chat with asset object ids', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - if (command === 'import_canvas_asset') { - return { - id: 'asset-1', - localPath: String(args?.localPath ?? ''), - absolutePath: `${String(args?.projectPath ?? '')}/${String( - args?.localPath ?? '', - )}`, - manifestPath: `${String(args?.projectPath ?? '')}/.agent/manifest.json`, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - - await submitChat( - '/import-canvas-asset assets/hero.png canvas-project-1 object:asset-object-1 character image/png', - ); - expect( - await screen.findByText( - /导入 \/tmp\/authorized-game\/assets\/hero\.png · 画板 canvas-project-1 \/ object:asset-object-1 · character · image\/png/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - '已导入画板资产 canvas-project-1 / object:asset-object-1:assets/hero.png', - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('import_canvas_asset', { - projectPath: '/tmp/authorized-game', - localPath: 'assets/hero.png', - kind: 'character', - mediaType: 'image/png', - canvasProjectId: 'canvas-project-1', - resourceId: '', - assetObjectId: 'asset-object-1', - taskId: '', - prompt: '', - model: '', - }); - }); - - it.skip('fills the canvas export import command from the main quick action file picker', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - if (command === 'pick_local_file') { - return '/tmp/canvas-export.zip'; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - fireEvent.click(screen.getByRole('button', { name: '导入画板包' })); - - expect( - await screen.findByText('已选择画板导出包:/tmp/canvas-export.zip'), - ).not.toBeNull(); - expect(await composerText()).toBe( - '/import-canvas-export /tmp/canvas-export.zip ', - ); - expect(invoke).toHaveBeenCalledWith('pick_local_file'); - expect(invoke).not.toHaveBeenCalledWith( - 'import_canvas_export', - expect.anything(), - ); - }); - - it('rejects unsafe canvas import paths before confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - expect(await screen.findByText(/还没有最近一次 Agent run/)).not.toBeNull(); - - await submitChat( - '/import-canvas-asset /tmp/hero.png canvas-project-1 resource-1', - ); - expect( - screen.getByText('画板资产路径必须是项目内相对路径。'), - ).not.toBeNull(); - - await submitChat( - '/import-canvas-asset ../hero.png canvas-project-1 resource-1', - ); - expect( - screen.getAllByText('画板资产路径必须是项目内相对路径。').length, - ).toBe(2); - - await submitChat('/import-canvas-export relative.zip canvas-project-1'); - expect( - screen.getByText('画板导出 ZIP 路径必须是绝对路径。'), - ).not.toBeNull(); - expect(screen.queryByText('canvas.asset_import')).toBeNull(); - expect(screen.queryByText('canvas.export_import')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'import_canvas_asset', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'import_canvas_export', - expect.anything(), - ); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.objectContaining({ - message: expect.objectContaining({ - content: '画板导出 ZIP 路径必须是绝对路径。', - }), - }), - ); - }); - }); - - it('rejects canvas project ids with control characters before confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - - await submitChat('/canvas bad\u0007canvas'); - await submitChat('/sync-canvas-project bad\u0007canvas'); - await submitChat( - '/import-canvas-asset assets/hero.png bad\u0007canvas resource-1', - ); - await submitChat( - '/import-canvas-export /tmp/canvas-export.zip bad\u0007canvas', - ); - - expect(screen.getAllByText('画板项目 ID 不能包含控制字符。').length).toBe( - 4, - ); - expect(screen.queryByText('canvas.project_open')).toBeNull(); - expect(screen.queryByText('canvas.project_sync')).toBeNull(); - expect(screen.queryByText('canvas.asset_import')).toBeNull(); - expect(screen.queryByText('canvas.export_import')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'open_canvas_project', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'sync_canvas_project_assets', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'import_canvas_asset', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'import_canvas_export', - expect.anything(), - ); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.objectContaining({ - message: expect.objectContaining({ - content: - '/import-canvas-export /tmp/canvas-export.zip bad\u0007canvas', - }), - }), - ); - }); - }); - - it('opens canvas projects from chat after confirmation', async () => { - let resolveOpenCanvasProject: - | ((value: { url: string }) => void) - | undefined; - const invoke = vi.fn( - async (command: string, _args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'open_canvas_project') { - return new Promise<{ url: string }>((resolve) => { - resolveOpenCanvasProject = resolve; - }); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/canvas canvas-project-1'); - - expect( - await screen.findByText('准备打开画板项目:canvas-project-1'), - ).not.toBeNull(); - expect( - screen.getByText('canvas.project_open · canvas-project-1'), - ).not.toBeNull(); - expect( - screen.getByText('打开本机画板项目 canvas-project-1'), - ).not.toBeNull(); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('正在打开画板:canvas-project-1'), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('open_canvas_project', { - canvasProjectId: 'canvas-project-1', - editorBaseUrl: 'http://127.0.0.1:3000', - }); - - resolveOpenCanvasProject?.({ - url: 'http://127.0.0.1:3000/editor/canvas?projectid=canvas-project-1', - }); - - expect( - await screen.findByText( - '已打开画板:http://127.0.0.1:3000/editor/canvas?projectid=canvas-project-1', - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '同步此画板' })); - expect(await composerText()).toBe('/sync-canvas-project canvas-project-1'); - }); - - it('syncs canvas project assets from chat after project confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - if (command === 'sync_canvas_project_assets') { - return { - canvasProjectId: String(args?.canvasProjectId ?? ''), - importRoot: 'assets/canvas-sync/canvas-project-1-1', - importedCount: 1, - assets: [ - { - id: 'canvas-1', - localPath: 'assets/canvas-sync/canvas-project-1-1/res-1.png', - absolutePath: - '/tmp/authorized-game/assets/canvas-sync/canvas-project-1-1/res-1.png', - manifestPath: '/tmp/authorized-game/.agent/manifest.json', - }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - - await submitChat('/sync-canvas-project canvas-project-1'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - '已同步 1 个画板资产自 canvas-project-1:assets/canvas-sync/canvas-project-1-1', - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('sync_canvas_project_assets', { - projectPath: '/tmp/authorized-game', - canvasProjectId: 'canvas-project-1', - }); - }); - - it('generates platform art assets from chat after project confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - if (command === 'generate_platform_art_asset') { - return { - id: 'generated-art-1', - localPath: 'assets/canvas-generated/generated-art-1.png', - absolutePath: - '/tmp/authorized-game/assets/canvas-generated/generated-art-1.png', - manifestPath: '/tmp/authorized-game/.agent/manifest.json', - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - - await submitChat('/generate-art 月光厨房弹幕主角'); - expect(await screen.findByText('准备生成首版美术素材。')).not.toBeNull(); - expect(screen.getByText('canvas.asset_generate')).not.toBeNull(); - expect( - screen.getByText( - /通过平台 External Editor API 生成美术素材并写入 \/tmp\/authorized-game\/assets\/canvas-generated\//, - ), - ).not.toBeNull(); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - '已生成美术素材:assets/canvas-generated/generated-art-1.png', - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('generate_platform_art_asset', { - projectPath: '/tmp/authorized-game', - prompt: '月光厨房弹幕主角', - }); - }); - - it('requires project policy confirmation before generating platform art assets', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['canvas.asset_generate'], - }, - }; - } - if (command === 'generate_platform_art_asset') { - return { - id: 'generated-art-confirm', - localPath: 'assets/canvas-generated/generated-art-confirm.png', - absolutePath: - '/tmp/authorized-game/assets/canvas-generated/generated-art-confirm.png', - manifestPath: '/tmp/authorized-game/.agent/manifest.json', - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/generate-art 月光厨房弹幕主角'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('准备生成首版美术素材。')).not.toBeNull(); - expect(screen.getByText('canvas.asset_generate')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'generate_platform_art_asset', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - '已生成美术素材:assets/canvas-generated/generated-art-confirm.png', - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('generate_platform_art_asset', { - projectPath: '/tmp/authorized-game', - prompt: '月光厨房弹幕主角', - }); - }); - - it('imports canvas export packages from chat after project confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'import_canvas_export') { - return { - canvasProjectId: String(args?.canvasProjectId ?? ''), - importRoot: 'assets/canvas-imports/canvas-project-1-1', - importedCount: 2, - assets: [], - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat( - '/import-canvas-export /tmp/canvas-export.zip canvas-project-1', - ); - expect( - await screen.findByText( - /导入 \/tmp\/canvas-export\.zip 到 \/tmp\/authorized-game\/assets\/canvas-imports\/ · 画板 canvas-project-1/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - '已导入 2 个画板资产自 canvas-project-1:assets/canvas-imports/canvas-project-1-1', - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('import_canvas_export', { - projectPath: '/tmp/authorized-game', - exportPath: '/tmp/canvas-export.zip', - canvasProjectId: 'canvas-project-1', - }); - }); -} diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-commands.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-commands.suite.ts deleted file mode 100644 index 4fac342f8..000000000 --- a/apps/ai-game-creator-shell/tests/appSurface/project-commands.suite.ts +++ /dev/null @@ -1,3766 +0,0 @@ -import { - act, - composerText, - createGameCreationAppManifest, - createGameCreationAppSeedTasks, - emptyProjectPolicy, - expect, - fireEvent, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, - it, - renderAppAt, - screen, - submitChat, - vi, - waitFor, - within, -} from './harness'; - -export function registerProjectCommandTests() { - it('does not mark a failed agent loop trace as passed in chat audit', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const failedTrace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-chat-audit-failed', - commandId: 'game.generate_draft', - status: 'failed', - passes: 3, - maxPasses: 3, - toolCallCount: 21, - maxToolCalls: 128, - stopReason: 'max-passes-exhausted', - goal: '做一个三轮仍失败的厨房弹幕游戏', - coordination: 'filesystem', - steps: [ - { - pass: 1, - agent: 'Planner', - phase: 'planning', - taskId: 'design-director', - group: 'design', - role: 'Director', - status: 'completed', - inputPaths: ['memory/session.md', 'memory/project.md'], - outputPaths: ['.agent/spec.md'], - summary: '拆解创作目标', - toolCalls: [], - }, - { - pass: 1, - agent: 'Orchestrator', - phase: 'plan', - taskId: 'code-director', - group: 'code', - role: 'Director', - status: 'completed', - inputPaths: ['.agent/spec.md'], - outputPaths: ['.agent/passes/pass-1/task-graph.json'], - summary: '生成首轮任务图', - toolCalls: [], - }, - { - pass: 1, - agent: '数值组 / Difficulty', - phase: 'role-brief', - taskId: 'balance-seed', - group: 'balance', - role: 'Difficulty', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-1/groups/balance/difficulty.md'], - summary: '生成数值约束', - toolCalls: [], - }, - { - pass: 1, - agent: '美术组 / Asset', - phase: 'role-brief', - taskId: 'art-asset-plan', - group: 'art', - role: 'Asset', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-1/groups/art/asset.md'], - summary: '规划画板资产', - toolCalls: [], - }, - { - pass: 1, - agent: '音乐组 / SFX', - phase: 'role-brief', - taskId: 'audio-asset-plan', - group: 'audio', - role: 'SFX', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-1/groups/audio/sfx.md'], - summary: '规划音效资产', - toolCalls: [], - }, - { - pass: 3, - agent: 'Generator', - phase: 'generate', - taskId: 'code-prototype', - group: 'code', - role: 'Code', - status: 'completed', - inputPaths: ['.agent/spec.md', '.agent/findings.md'], - outputPaths: ['.agent/passes/pass-3/draft.json'], - summary: '生成仍未通过的原型', - toolCalls: [], - }, - { - pass: 3, - agent: 'Evaluator', - phase: 'evaluate', - taskId: 'quality-review', - group: 'code', - role: 'Review', - status: 'needs-revision', - inputPaths: ['.agent/passes/pass-3/draft.json'], - outputPaths: ['.agent/findings.md'], - summary: '三轮后仍缺少输入监听', - toolCalls: [], - }, - { - pass: 3, - agent: '运营组 / Publish', - phase: 'handoff', - taskId: 'publish-package', - group: 'publishing', - role: 'Publish', - status: 'completed', - inputPaths: ['.agent/passes/pass-3/handoff.md'], - outputPaths: ['exports/README.md'], - summary: '整理失败验收说明', - toolCalls: [], - }, - ], - artifacts: [ - { - path: '.agent/passes/pass-3/game.html', - sizeBytes: 512, - checksum: 'fnv1a64:failed-pass', - }, - ], - taskGraph: { - goal: '做一个三轮仍失败的厨房弹幕游戏', - readyTaskIds: [], - activeTaskIds: [ - 'code-prototype', - 'quality-review', - 'preview-readiness', - ], - carriedTaskIds: ['design-director'], - repairFocus: ['缺少输入监听'], - repairRoutes: [ - { - issue: '缺少输入监听', - taskIds: ['code-prototype', 'quality-review', 'preview-readiness'], - reason: 'code-runtime', - }, - ], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [ - { - pass: 3, - mode: 'repair', - summary: '第 3 轮返工仍未通过', - activeTaskIds: [ - 'code-prototype', - 'quality-review', - 'preview-readiness', - ], - carriedTaskIds: ['design-director'], - dependencyWaves: [ - ['code-prototype'], - ['quality-review'], - ['preview-readiness'], - ], - repairFocus: ['缺少输入监听'], - repairRoutes: [ - { - issue: '缺少输入监听', - taskIds: [ - 'code-prototype', - 'quality-review', - 'preview-readiness', - ], - reason: 'code-runtime', - }, - ], - }, - ], - nextStep: 'inspect-error', - error: 'Agent loop 已重试 3 轮但仍未通过 Evaluator:缺少输入监听', - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(failedTrace), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - - await submitChat('/audit'); - - expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); - expect(screen.getByText(/6 组协作证据:通过/)).not.toBeNull(); - expect( - screen.getByText(/Loop trace:未通过 · run run-chat-audit-failed/), - ).not.toBeNull(); - expect( - screen.queryByText(/Loop trace:通过 · run run-chat-audit-failed/), - ).toBeNull(); - expect(screen.getByText(/返工路由\/Carry-over:通过/)).not.toBeNull(); - }); - - it('does not create a generation command before a local project is initialized', async () => { - renderAppAt('/?dev'); - - await submitChat('做一个反弹弹幕厨房游戏'); - - expect(screen.getByText('请先用 /project 设置本地项目。')).not.toBeNull(); - expect(screen.queryByText('game.generate_draft')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - }); - - it('reports unknown slash commands instead of falling back to generation', async () => { - renderAppAt('/?dev'); - - await submitChat('/unknown-command'); - - expect( - screen.getByText('未知命令:/unknown-command。输入 /help 查看可用命令。'), - ).not.toBeNull(); - expect(screen.queryByText('game.generate_draft')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - }); - - it('reports missing slash command arguments instead of falling back to generation', async () => { - renderAppAt('/'); - - await submitChat('/project'); - expect(screen.getByText('格式:/project /绝对路径')).not.toBeNull(); - - await submitChat('/remember'); - expect(screen.getByText('请提供要追加的记忆内容。')).not.toBeNull(); - - await submitChat('/sync-canvas-project'); - expect(screen.getByText('请提供画板项目 ID。')).not.toBeNull(); - - await submitChat('/import-canvas-asset'); - expect( - screen.getByText( - '格式:/import-canvas-asset assets/hero.png 画板项目ID 资源ID|object:资产对象ID', - ), - ).not.toBeNull(); - expect(screen.queryByText('game.generate_draft')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - }); - - it('rejects relative project paths before confirmation', async () => { - const invoke = vi.fn(); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project relative-game'); - - expect(screen.getByText('请提供本地项目绝对路径。')).not.toBeNull(); - expect(screen.queryByText('project.create')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(invoke).not.toHaveBeenCalled(); - - await submitChat('/project /tmp/bad\u0007path'); - - expect(screen.getByText('本地项目路径不能包含控制字符。')).not.toBeNull(); - expect(screen.queryByText('project.create')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(invoke).not.toHaveBeenCalled(); - }); - - it('warns before project.create initializes a non-empty folder from the main window', async () => { - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'is_local_project_directory_non_empty') { - expect(args).toEqual({ - projectPath: '/tmp/main-non-empty-game', - }); - return true; - } - if (command === 'init_local_game_project') { - throw new Error('should wait for explicit confirmation'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false); - renderAppAt('/'); - - await submitChat('/project /tmp/main-non-empty-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByRole('dialog', { name: '文件夹不是空的' }), - ).not.toBeNull(); - expect(screen.getByText('/tmp/main-non-empty-game')).not.toBeNull(); - expect(confirm).not.toHaveBeenCalled(); - fireEvent.click(screen.getByRole('button', { name: '取消' })); - - expect( - await screen.findByText( - '已取消在非空文件夹中新建项目:/tmp/main-non-empty-game', - ), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'init_local_game_project', - expect.anything(), - ); - }); - - it('creates from the main window after confirming a non-empty folder warning', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - 'main-non-empty-game', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'is_local_project_directory_non_empty') { - return true; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'append_local_permission_log') { - return {}; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true); - renderAppAt('/'); - - await submitChat('/project /tmp/main-non-empty-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByRole('dialog', { name: '文件夹不是空的' }), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'init_local_game_project', - expect.anything(), - ); - fireEvent.click(screen.getByRole('button', { name: '继续新建' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('init_local_game_project', { - projectPath: '/tmp/main-non-empty-game', - projectId: expect.stringMatching(/^local-project-/), - name: '未命名游戏原型', - }); - }); - expect(confirm).not.toHaveBeenCalled(); - }); - - it('restores checkpoints through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'restore_local_project_checkpoint') { - return { - checkpointId: String(args?.checkpointId ?? ''), - restoredCount: 3, - deletedCount: 1, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/restore checkpoint-1'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - /已回滚 3 个文件到 \/tmp\/authorized-game,删除 1 个新增文件:checkpoint-1/, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('restore_local_project_checkpoint', { - projectPath: '/tmp/authorized-game', - checkpointId: 'checkpoint-1', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'project.restore', - }); - }); - - it('lists recent checkpoints from chat through project files', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const manifests = new Map([ - [ - '.agent/checkpoints/checkpoint-new/manifest.json', - JSON.stringify({ - checkpointId: 'checkpoint-new', - createdAt: 1700000002, - files: [ - { path: 'game/index.html', size: 10 }, - { path: 'assets/hero.png', size: 2 }, - ], - }), - ], - [ - '.agent/checkpoints/checkpoint-old/manifest.json', - JSON.stringify({ - checkpointId: 'checkpoint-old', - createdAt: 1700000001, - files: [{ path: 'game/index.html', size: 8 }], - }), - ], - ]); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [ - { - path: '.agent/checkpoints/checkpoint-old/manifest.json', - kind: 'file', - size: 100, - modifiedAt: 100, - }, - { - path: '.agent/checkpoints/checkpoint-new/manifest.json', - kind: 'file', - size: 120, - modifiedAt: 200, - }, - { - path: '.agent/checkpoints/checkpoint-new/files/game/index.html', - kind: 'file', - size: 10, - modifiedAt: 200, - }, - ], - }; - } - if (command === 'read_local_project_file') { - const relativePath = String(args?.relativePath ?? ''); - return { - path: relativePath, - absolutePath: `${String(args?.projectPath ?? '')}/${relativePath}`, - content: manifests.get(relativePath) ?? '{}', - }; - } - if (command === 'diff_local_project_checkpoint') { - return { - checkpointId: String(args?.checkpointId ?? ''), - added: [{ path: 'game/index.html' }], - changed: [], - deleted: [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/checkpoints'); - - expect(await screen.findByText(/最近 checkpoint:/)).not.toBeNull(); - expect( - screen.getByText( - /checkpoint-new · 2 个文件 · 12B · createdAt 1700000002 · \/diff checkpoint-new · \/restore checkpoint-new/, - ), - ).not.toBeNull(); - expect( - screen.getByText( - /checkpoint-old · 1 个文件 · 8B · createdAt 1700000001 · \/diff checkpoint-old · \/restore checkpoint-old/, - ), - ).not.toBeNull(); - fireEvent.click( - within(screen.getByLabelText('最近 checkpoint')).getByRole('button', { - name: '对比 checkpoint-new', - }), - ); - expect( - await screen.findByText(/checkpoint:checkpoint-new/), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('list_local_project_files', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/checkpoints/checkpoint-new/manifest.json', - commandId: 'file.read', - }); - expect(invoke).toHaveBeenCalledWith('diff_local_project_checkpoint', { - projectPath: '/tmp/authorized-game', - checkpointId: 'checkpoint-new', - }); - }); - - it('requires file read confirmation before reading checkpoint manifests', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['file.read'], - }, - }; - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [ - { - path: '.agent/checkpoints/checkpoint-1/manifest.json', - kind: 'file', - size: 100, - modifiedAt: 100, - }, - ], - }; - } - if (command === 'read_local_project_file') { - return { - path: String(args?.relativePath ?? ''), - absolutePath: `${String(args?.projectPath ?? '')}/${String( - args?.relativePath ?? '', - )}`, - content: JSON.stringify({ - checkpointId: 'checkpoint-1', - createdAt: 1700000000, - files: [], - }), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/checkpoints'); - - expect( - await screen.findByText('准备读取 checkpoint manifest。'), - ).not.toBeNull(); - expect(screen.getByText('file.read')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => - expect(screen.getByLabelText('聊天').textContent).toContain( - 'checkpoint-1', - ), - ); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/checkpoints/checkpoint-1/manifest.json', - commandId: 'file.read', - }); - }); - - it('requires project policy confirmation before restoring checkpoints from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['project.restore'], - }, - }; - } - if (command === 'restore_local_project_checkpoint') { - return { - checkpointId: String(args?.checkpointId ?? ''), - restoredCount: 2, - deletedCount: 0, - }; - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/restore checkpoint-1'); - - expect(await screen.findByText(/project\.restore/)).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'restore_local_project_checkpoint', - expect.anything(), - ); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - /已回滚 2 个文件到 \/tmp\/authorized-game,删除 0 个新增文件:checkpoint-1/, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('restore_local_project_checkpoint', { - projectPath: '/tmp/authorized-game', - checkpointId: 'checkpoint-1', - }); - }); - - it('shows truncated checkpoint diff counts in chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const added = Array.from({ length: 22 }, (_, index) => ({ - path: `game/file-${index + 1}.ts`, - })); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'diff_local_project_checkpoint') { - return { - checkpointId: String(args?.checkpointId ?? ''), - added, - changed: [], - deleted: [], - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/diff checkpoint-1'); - - expect(await screen.findByText(/checkpoint:checkpoint-1/)).not.toBeNull(); - expect(screen.getByText(/^game\/file-20\.ts$/)).not.toBeNull(); - expect(screen.queryByText(/^game\/file-21\.ts$/)).toBeNull(); - expect(screen.getByText(/还有 2 项/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('diff_local_project_checkpoint', { - projectPath: '/tmp/authorized-game', - checkpointId: 'checkpoint-1', - }); - }); - - it('requires project policy confirmation before diffing checkpoints from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['project.diff'], - }, - }; - } - if (command === 'diff_local_project_checkpoint') { - return { - checkpointId: String(args?.checkpointId ?? ''), - added: [], - changed: [{ path: 'game/index.html' }], - deleted: [], - }; - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/diff checkpoint-1'); - - expect(await screen.findByText('准备对比项目 checkpoint。')).not.toBeNull(); - expect(screen.getByText('project.diff')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'diff_local_project_checkpoint', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/checkpoint:checkpoint-1/)).not.toBeNull(); - expect(screen.getByText(/^game\/index\.html$/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('diff_local_project_checkpoint', { - projectPath: '/tmp/authorized-game', - checkpointId: 'checkpoint-1', - }); - }); - - it('rejects unsafe checkpoint ids before diff or restore calls', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/diff ../checkpoint-1'); - expect(screen.getByText('checkpoint id 非法。')).not.toBeNull(); - - await submitChat('/restore checkpoint/evil'); - expect(screen.getAllByText('checkpoint id 非法。').length).toBe(2); - expect(screen.queryByText('project.restore')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'diff_local_project_checkpoint', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'restore_local_project_checkpoint', - expect.anything(), - ); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.objectContaining({ - message: expect.objectContaining({ - content: 'checkpoint id 非法。', - }), - }), - ); - }); - }); - - it('reads project policy from chat and truncates long command lists', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const deniedCommands = Array.from( - { length: 14 }, - (_, index) => `file.write.${index + 1}`, - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands, - confirmCommands: ['game.static_smoke'], - agentPolicies: { - 'design-director': { - deniedCommands: ['file.read'], - confirmCommands: ['memory.write'], - }, - }, - }, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/policy'); - - expect( - await screen.findByText(/策略:\.agent\/policy\.json/), - ).not.toBeNull(); - expect(screen.getByText(/file\.write\.12/)).not.toBeNull(); - expect(screen.queryByText(/file\.write\.13/)).toBeNull(); - expect(screen.getByText(/拒绝:.*还有 2 项/)).not.toBeNull(); - expect(screen.getByText(/确认:game\.static_smoke/)).not.toBeNull(); - expect( - screen.getByText( - /Agent design-director:拒绝 file\.read;确认 memory\.write/, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('updates project policy from chat after confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let policy = { - deniedCommands: ['file.delete'], - confirmCommands: ['game.static_smoke'], - agentPolicies: { - 'design-director': { - deniedCommands: ['file.read'], - confirmCommands: ['memory.write'], - }, - }, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy, - }; - } - if (command === 'write_project_permission_policy') { - policy = args?.policy as typeof policy; - return { - path: '.agent/policy.json', - policy, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/policy-deny file.write'); - expect(await screen.findByText('准备拒绝命令:file.write')).not.toBeNull(); - expect(screen.getByText('project.policy_write')).not.toBeNull(); - expect( - screen.getByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.delete、file\.write · 确认:game\.static_smoke/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText(/拒绝:file\.delete、file\.write/), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.delete', 'file.write'], - confirmCommands: ['game.static_smoke'], - agentPolicies: { - 'design-director': { - deniedCommands: ['file.read'], - confirmCommands: ['memory.write'], - }, - }, - }, - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'project.policy_write', - }); - }); - - it('updates per-agent project policy from chat after confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let policy = { - deniedCommands: ['project.index'], - confirmCommands: [], - agentPolicies: { - 'art-director': { - deniedCommands: [], - confirmCommands: ['memory.write'], - }, - }, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy, - }; - } - if (command === 'write_project_permission_policy') { - policy = args?.policy as typeof policy; - return { - path: '.agent/policy.json', - policy, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/agent-policy-deny design-director file.read'); - expect( - await screen.findByText('准备拒绝 Agent design-director 命令:file.read'), - ).not.toBeNull(); - expect( - screen.getByText( - /Agent:art-director 拒绝:无 确认:memory\.write;design-director 拒绝:file\.read 确认:无/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - /Agent design-director:拒绝 file\.read;确认 无/, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['project.index'], - confirmCommands: [], - agentPolicies: { - 'art-director': { - deniedCommands: [], - confirmCommands: ['memory.write'], - }, - 'design-director': { - deniedCommands: ['file.read'], - confirmCommands: [], - }, - }, - }, - }); - }); - - it('updates project policy confirm commands from chat after confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let policy = { - deniedCommands: ['file.delete'], - confirmCommands: [], - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy, - }; - } - if (command === 'write_project_permission_policy') { - policy = args?.policy as typeof policy; - return { - path: '.agent/policy.json', - policy, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/policy-confirm project.index'); - expect( - await screen.findByText('准备确认命令:project.index'), - ).not.toBeNull(); - expect( - screen.getByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.delete · 确认:project\.index/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/确认:project\.index/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.delete'], - confirmCommands: ['project.index'], - }, - }); - - await submitChat('/policy-auto project.index'); - expect( - await screen.findByText('准备自动执行命令:project.index'), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/确认:无/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.delete'], - confirmCommands: [], - }, - }); - }); - - // 连续 12 次聊天提交验证策略累计与互斥,包含 Lexical 提交和确认面板更新。 - it('keeps project policy deny and confirm command lists mutually exclusive', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let policy = { - deniedCommands: ['project.index'], - confirmCommands: ['file.write'], - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy, - }; - } - if (command === 'write_project_permission_policy') { - policy = args?.policy as typeof policy; - return { - path: '.agent/policy.json', - policy, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/policy-deny file.write'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:project\.index、file\.write · 确认:无/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['project.index', 'file.write'], - confirmCommands: [], - }, - }); - }); - - await submitChat('/policy-confirm project.index'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: ['project.index'], - }, - }); - }); - - await submitChat('/policy-confirm project.diff'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: ['project.index', 'project.diff'], - }, - }); - }); - - await submitChat('/policy-confirm preview.stop'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff、preview\.stop/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: ['project.index', 'project.diff', 'preview.stop'], - }, - }); - }); - - await submitChat('/policy-confirm preview.open'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff、preview\.stop、preview\.open/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: [ - 'project.index', - 'project.diff', - 'preview.stop', - 'preview.open', - ], - }, - }); - }); - - await submitChat('/policy-confirm preview.start'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff、preview\.stop、preview\.open、preview\.start/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: [ - 'project.index', - 'project.diff', - 'preview.stop', - 'preview.open', - 'preview.start', - ], - }, - }); - }); - - await submitChat('/policy-confirm agent.run_status'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff、preview\.stop、preview\.open、preview\.start、agent\.run_status/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: [ - 'project.index', - 'project.diff', - 'preview.stop', - 'preview.open', - 'preview.start', - 'agent.run_status', - ], - }, - }); - }); - - await submitChat('/policy-confirm agent.kill'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff、preview\.stop、preview\.open、preview\.start、agent\.run_status、agent\.kill/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: [ - 'project.index', - 'project.diff', - 'preview.stop', - 'preview.open', - 'preview.start', - 'agent.run_status', - 'agent.kill', - ], - }, - }); - }); - - await submitChat('/policy-confirm agent.retry'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff、preview\.stop、preview\.open、preview\.start、agent\.run_status、agent\.kill、agent\.retry/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: [ - 'project.index', - 'project.diff', - 'preview.stop', - 'preview.open', - 'preview.start', - 'agent.run_status', - 'agent.kill', - 'agent.retry', - ], - }, - }); - }); - - await submitChat('/policy-confirm agent.resume'); - expect( - await screen.findByText( - /写入 \/tmp\/authorized-game\/\.agent\/policy\.json · 拒绝:file\.write · 确认:project\.index、project\.diff、preview\.stop、preview\.open、preview\.start、agent\.run_status、agent\.kill、agent\.retry、agent\.resume/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: ['file.write'], - confirmCommands: [ - 'project.index', - 'project.diff', - 'preview.stop', - 'preview.open', - 'preview.start', - 'agent.run_status', - 'agent.kill', - 'agent.retry', - 'agent.resume', - ], - }, - }); - }); - - await submitChat('/policy-confirm asset.register'); - expect(await screen.findByText(/确认:.*asset\.register/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: expect.objectContaining({ - deniedCommands: ['file.write'], - confirmCommands: expect.arrayContaining(['asset.register']), - }), - }); - }); - }, 15_000); - - it('does not write project policy for no-op policy changes', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: ['file.delete'], - confirmCommands: [], - }, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/policy-allow file.write'); - - expect( - await screen.findByText('命令不在拒绝列表中:file.write'), - ).not.toBeNull(); - await submitChat('/policy-auto project.index'); - expect( - await screen.findByText('命令不在确认列表中:project.index'), - ).not.toBeNull(); - expect(screen.queryByText('project.policy_write')).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'write_project_permission_policy', - expect.anything(), - ); - }); - - it('allows checkpoint commands to require project policy confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let policy = { - deniedCommands: [], - confirmCommands: [], - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy, - }; - } - if (command === 'write_project_permission_policy') { - policy = args?.policy as typeof policy; - return { - path: '.agent/policy.json', - policy, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/policy-confirm project.checkpoint'); - - expect(await screen.findByText(/确认:project\.checkpoint/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: [], - confirmCommands: ['project.checkpoint'], - }, - }); - }); - - await submitChat('/policy-confirm project.restore'); - - expect( - await screen.findByText(/确认:project\.checkpoint、project\.restore/), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: [], - confirmCommands: ['project.checkpoint', 'project.restore'], - }, - }); - }); - - await submitChat('/policy-confirm project.export_list'); - - expect( - await screen.findByText( - /确认:project\.checkpoint、project\.restore、project\.export_list/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: [], - confirmCommands: [ - 'project.checkpoint', - 'project.restore', - 'project.export_list', - ], - }, - }); - }); - }); - - it('allows canvas project commands to require project policy confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let policy = { - deniedCommands: [], - confirmCommands: [], - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy, - }; - } - if (command === 'write_project_permission_policy') { - policy = args?.policy as typeof policy; - return { - path: '.agent/policy.json', - policy, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/policy-confirm canvas.project_sync'); - expect( - await screen.findByText(/确认:canvas\.project_sync/), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: [], - confirmCommands: ['canvas.project_sync'], - }, - }); - }); - - await submitChat('/policy-confirm canvas.asset_generate'); - expect( - await screen.findByText( - /确认:canvas\.project_sync、canvas\.asset_generate/, - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('write_project_permission_policy', { - projectPath: '/tmp/authorized-game', - policy: { - deniedCommands: [], - confirmCommands: ['canvas.project_sync', 'canvas.asset_generate'], - }, - }); - }); - }); - - it('rejects unsupported project policy confirm commands before reading policy', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { - projectPath: '/tmp/authorized-game', - }); - }); - invoke.mockClear(); - - await submitChat('/policy-confirm file.write'); - - expect( - await screen.findByText( - '当前仅支持确认 project.index、project.status、project.bootstrap、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。', - ), - ).not.toBeNull(); - expect(screen.queryByText('project.policy_write')).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_project_permission_policy', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'write_project_permission_policy', - expect.anything(), - ); - }); - - it('rejects unknown project policy command ids before reading policy', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: '1', - ...(args?.message as Record), - updatedAt: 1, - }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { - projectPath: '/tmp/authorized-game', - }); - }); - invoke.mockClear(); - - await submitChat('/policy-deny not.a.command'); - - expect( - await screen.findByText('未知内置命令:not.a.command'), - ).not.toBeNull(); - expect(screen.queryByText('project.policy_write')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_project_permission_policy', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'write_project_permission_policy', - expect.anything(), - ); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.objectContaining({ - message: expect.objectContaining({ - content: '未知内置命令:not.a.command', - }), - }), - ); - }); - }); - - it('reports policy write failures when Tauri is unavailable after confirmation', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: [], - }, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/policy-deny file.write'); - expect(await screen.findByText('准备拒绝命令:file.write')).not.toBeNull(); - delete window.__TAURI__; - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('需要在 Tauri App 内运行。')).not.toBeNull(); - }); - - it('does not stop a preview before a local project is initialized', async () => { - const invoke = vi.fn(); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/preview-stop'); - - expect(screen.getByText('请先用 /project 设置本地项目。')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalled(); - }); - - it('stops preview through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'stop_local_game_preview') { - return { - status: 'stopped', - url: null, - port: null, - root: null, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/preview-stop'); - - expect(await screen.findByText('预览已停止。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('stop_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'command.auto', - commandId: 'preview.stop', - }); - }); - - it('requires project policy confirmation before stopping preview from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['preview.stop'], - }, - }; - } - if (command === 'stop_local_game_preview') { - return { - status: 'stopped', - url: null, - port: null, - root: null, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/preview-stop'); - - expect(await screen.findByText('准备停止本地预览。')).not.toBeNull(); - expect(screen.getByText('preview.stop')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'stop_local_game_preview', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('预览已停止。')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('stop_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('does not show preview status before a local project is initialized', async () => { - const invoke = vi.fn(); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/preview-status'); - - expect(screen.getByText('请先用 /project 设置本地项目。')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalled(); - }); - - it('does not open a preview before a local project is initialized', async () => { - const invoke = vi.fn(); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/open-preview'); - - expect(screen.getByText('请先用 /project 设置本地项目。')).not.toBeNull(); - expect(screen.queryByText('preview.open')).toBeNull(); - expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); - expect(invoke).not.toHaveBeenCalled(); - }); - - it('opens preview through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'activate_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/open-preview'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('已切换到客户端运行视图:http://127.0.0.1:3210/'), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('requires project policy confirmation before opening preview from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['preview.open'], - }, - }; - } - if (command === 'activate_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/open-preview'); - expect(await screen.findByText('准备打开当前本地预览。')).not.toBeNull(); - expect(screen.getByText('preview.open')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { - projectPath: '/tmp/authorized-game', - }); - }); - expect(screen.getByText('preview.open')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'activate_local_game_preview', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('已切换到客户端运行视图:http://127.0.0.1:3210/'), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('cancels pending preview open without opening preview', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'activate_local_game_preview') { - throw new Error('should not open preview after cancel'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/open-preview'); - const previewCommand = screen.getByText('preview.open'); - fireEvent.click( - within( - previewCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - expect(await screen.findByText('已取消预览操作')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'activate_local_game_preview', - expect.anything(), - ); - }); - - it('starts preview from chat through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'start_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - if (command === 'activate_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/preview'); - expect(screen.getByText('preview.start')).not.toBeNull(); - expect( - screen.getByText('启动 /tmp/authorized-game/game/ 并载入客户端运行视图'), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - /预览已在客户端运行视图启动:http:\/\/127\.0\.0\.1:3210\//, - ), - ).not.toBeNull(); - expect(screen.queryByTitle('本地游戏预览')).toBeNull(); - expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).not.toHaveBeenCalledWith('activate_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.pending', - commandId: 'preview.start', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'preview.start', - }); - }); - - it('requires project policy confirmation before starting preview from chat', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['preview.start'], - }, - }; - } - if (command === 'start_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - if (command === 'activate_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/preview'); - expect(screen.getByText('preview.start')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { - projectPath: '/tmp/authorized-game', - }); - }); - expect(screen.getByText('preview.start')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'start_local_game_preview', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - /预览已在客户端运行视图启动:http:\/\/127\.0\.0\.1:3210\//, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('blocks preview start before native start when project policy denies it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: ['preview.start'], - confirmCommands: [], - }, - }; - } - if (command === 'start_local_game_preview') { - throw new Error('should not start preview after deny'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/preview'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText('项目权限策略拒绝执行:preview.start'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'start_local_game_preview', - expect.anything(), - ); - }); - - it('cancels preview start policy confirmation without starting preview', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['preview.start'], - }, - }; - } - if (command === 'start_local_game_preview') { - throw new Error('should wait for preview confirmation'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/preview'); - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: '确认' })); - }); - - const previewCommand = await screen.findByText('preview.start'); - fireEvent.click( - within( - previewCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - expect(await screen.findByText('已取消预览操作')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'start_local_game_preview', - expect.anything(), - ); - }); - - it('reads preview status through the authorized local project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: [], - }, - }; - } - if (command === 'get_local_game_preview_status') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?dev'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/preview-status'); - - expect( - await screen.findByText('预览运行中:http://127.0.0.1:3210/'), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '填入打开预览命令' })); - expect(await composerText()).toBe('/open-preview'); - expect(screen.getByText('preview.status')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'command.auto', - commandId: 'preview.status', - }); - }); - - it('requires confirmation for preview status when project policy asks for it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['preview.status'], - }, - }; - } - if (command === 'get_local_game_preview_status') { - return { - status: 'stopped', - url: null, - port: null, - root: null, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?dev'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/preview-status'); - - expect(await screen.findByText('准备查看预览状态。')).not.toBeNull(); - expect(screen.getByText('preview.status')).not.toBeNull(); - expect( - screen.getByText('查看 /tmp/authorized-game 的预览状态'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_preview_status', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('预览未启动。')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '填入启动预览命令' })); - expect(await composerText()).toBe('/preview'); - expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.pending', - commandId: 'preview.status', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'preview.status', - }); - }); - - it('runs static smoke and starts preview from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'run_limited_local_command') { - return { - commandId: 'game.static_smoke', - status: 'completed', - output: 'static smoke passed', - logPath: '.agent/logs/command.log', - }; - } - if (command === 'start_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - if (command === 'activate_local_game_preview') { - return { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - root: String(args?.projectPath ?? ''), - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/run'); - expect(screen.getByText('game.run_local')).not.toBeNull(); - expect( - screen.getByText('运行自检并启动 /tmp/authorized-game/game/'), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect( - await screen.findByText( - /运行通过,已载入客户端运行视图:http:\/\/127\.0\.0\.1:3210\//, - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('run_limited_local_command', { - projectPath: '/tmp/authorized-game', - commandId: 'game.static_smoke', - }); - expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).not.toHaveBeenCalledWith('activate_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('rejects unsafe initialized project paths before local project actions', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, _args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - return { - projectPath: 'relative-game', - manifestPath: 'relative-game/.agent/manifest.json', - manifest, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findAllByText('本地项目路径无效')).toHaveLength(2); - - await submitChat('/run'); - - expect( - await screen.findByText('请先用 /project 设置本地项目。'), - ).not.toBeNull(); - expect(screen.queryByText('game.run_local')).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_conversation', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'run_limited_local_command', - expect.anything(), - ); - expect(invoke).not.toHaveBeenCalledWith( - 'start_local_game_preview', - expect.anything(), - ); - }); - - it('runs static smoke from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'run_limited_local_command') { - return { - commandId: 'game.static_smoke', - status: 'completed', - output: 'static smoke passed', - logPath: '.agent/logs/command.log', - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/smoke'); - expect( - screen.getByText('command.run_limited · 静态入口自检'), - ).not.toBeNull(); - expect( - screen.getByText('运行 game.static_smoke 于 /tmp/authorized-game'), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/static smoke passed/)).not.toBeNull(); - expect( - screen.getByText(/日志:\.agent\/logs\/command\.log/), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('run_limited_local_command', { - projectPath: '/tmp/authorized-game', - commandId: 'game.static_smoke', - }); - }); - - it('lists limited commands from chat without opening developer panels', async () => { - const invoke = vi.fn(async (command: string) => { - if (command === 'get_limited_local_commands') { - return [{ id: 'game.custom_smoke', title: '自定义自检' }]; - } - throw new Error(`unexpected invoke ${command}`); - }); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/commands'); - - expect(await screen.findByText(/可运行受限命令:/)).not.toBeNull(); - expect(screen.getByText(/game\.custom_smoke · 自定义自检/)).not.toBeNull(); - expect(screen.queryByLabelText('开发环境')).toBeNull(); - expect(invoke).toHaveBeenCalledWith('get_limited_local_commands'); - }); - - it('reads project status from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const statusManifest = { - ...manifest, - tasks: manifest.tasks.map((task, index) => ({ - ...task, - status: index === 0 ? 'completed' : task.status, - })), - assets: [ - { - id: 'asset-hero', - kind: 'character', - mediaType: 'image/png', - localPath: 'assets/uploads/hero.png', - source: { kind: 'uploaded' }, - }, - ], - preview: { - status: 'running', - url: 'http://127.0.0.1:3210/', - port: 3210, - }, - commandRuns: [ - { - commandId: 'game.static_smoke', - status: 'completed', - output: 'ok', - logPath: '.agent/logs/command.log', - updatedAt: 1, - }, - ], - } satisfies typeof manifest; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'get_local_game_manifest') { - return statusManifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/status'); - - const statusMessage = await screen.findByText( - (_, element) => - element?.classList.contains('message--assistant') === true && - element.textContent?.includes('项目:未命名游戏原型') === true && - element.textContent.includes('目录:/tmp/authorized-game'), - ); - expect(statusMessage.textContent).toContain('任务:已完成 1,待处理 15'); - expect(statusMessage.textContent).toContain('资产:1 个'); - expect(statusMessage.textContent).toContain( - '预览:运行中 http://127.0.0.1:3210/', - ); - expect(statusMessage.textContent).toContain( - '最近命令:game.static_smoke · 完成', - ); - expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { - projectPath: '/tmp/authorized-game', - commandId: 'project.status', - }); - }); - - it('requires confirmation for project status when project policy asks for it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['project.status'], - }, - }; - } - if (command === 'get_local_game_manifest') { - return manifest; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/status'); - - expect(await screen.findByText('准备读取项目状态。')).not.toBeNull(); - expect(screen.getByText('project.status')).not.toBeNull(); - expect( - screen.getByText('读取 /tmp/authorized-game 的项目状态'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_manifest', - expect.objectContaining({ commandId: 'project.status' }), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/项目:未命名游戏原型/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { - projectPath: '/tmp/authorized-game', - commandId: 'project.status', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.pending', - commandId: 'project.status', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'project.status', - }); - }); - - it('blocks project status before native read when project policy denies it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: ['project.status'], - confirmCommands: [], - }, - }; - } - if (command === 'get_local_game_manifest') { - throw new Error('should not read project status after deny'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/status'); - - expect( - await screen.findByText('项目权限策略拒绝执行:project.status'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_manifest', - expect.objectContaining({ commandId: 'project.status' }), - ); - }); - - it('cancels project status policy confirmation without reading status', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['project.status'], - }, - }; - } - if (command === 'get_local_game_manifest') { - throw new Error('should wait for project status confirmation'); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/status'); - - const statusCommand = await screen.findByText('project.status'); - fireEvent.click( - within( - statusCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - expect(await screen.findByText('已取消读取项目状态')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_manifest', - expect.objectContaining({ commandId: 'project.status' }), - ); - }); - - it('lists local project files from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [ - { path: 'game', kind: 'directory', size: 0 }, - { path: 'game/index.html', kind: 'file', size: 128 }, - { path: 'assets/uploads/hero.png', kind: 'file', size: 4 }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/files'); - - expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); - expect( - screen.getByText( - (_, element) => - element?.tagName === 'LI' && element.textContent?.trim() === 'game/', - ), - ).not.toBeNull(); - // 列表项渲染成 `
  • game/index.html

  • `:去掉手写短横线之后 - // `
  • ` 与内部 `

    ` 的文本完全相同,必须限定在 `LI` 上匹配,否则 getByText 会命中两个元素。 - expect( - screen.getByText( - (_, element) => - element?.tagName === 'LI' && - element.textContent?.trim() === 'game/index.html', - ), - ).not.toBeNull(); - expect( - screen.getAllByText(/^assets\/uploads\/hero\.png$/).length, - ).toBeGreaterThan(0); - expect(invoke).toHaveBeenCalledWith('list_local_project_files', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('requires confirmation for file list when project policy asks for it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['file.list'], - }, - }; - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [{ path: 'game/index.html', kind: 'file', size: 128 }], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/files'); - - expect(await screen.findByText('准备列出项目文件。')).not.toBeNull(); - expect(screen.getByText('file.list')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'list_local_project_files', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); - // 同 `lists local project files` 的原理:`

  • ` 与内部 `

    ` 文本相同,需限定 `LI`。 - expect( - screen.getByText( - (_, element) => - element?.tagName === 'LI' && - element.textContent?.trim() === 'game/index.html', - ), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('list_local_project_files', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('requires confirmation for export list when project policy asks for it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['project.export_list'], - }, - }; - } - if (command === 'list_local_project_export_packages') { - return { - projectPath: String(args?.projectPath ?? ''), - packages: [ - { - packagePath: `${String(args?.projectPath ?? '')}/exports/playtest-package-001.zip`, - packageRelativePath: 'exports/playtest-package-001.zip', - totalBytes: 1024, - modifiedAt: 1700000001000, - }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - invoke.mockClear(); - - await submitChat('/exports'); - - expect(await screen.findByText('准备列出本地试玩包。')).not.toBeNull(); - expect(screen.getByText('project.export_list')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'list_local_project_export_packages', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/本地试玩包:/)).not.toBeNull(); - expect( - screen.getByText(/exports\/playtest-package-001\.zip · 1024B/), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('list_local_project_export_packages', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('builds the local project index from chat through the authorized project path', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const files = Array.from({ length: 14 }, (_, index) => ({ - path: `game/file-${index + 1}.html`, - size: index + 10, - checksum: `fnv1a64:${index + 1}`, - })); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: [], - }, - }; - } - if (command === 'build_local_project_index') { - return { - projectPath: String(args?.projectPath ?? ''), - indexPath: '.agent/project.index.json', - fileCount: files.length, - totalBytes: 231, - files, - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/index'); - - expect(await screen.findByText(/索引:14 个文件,231B/)).not.toBeNull(); - expect( - screen.getByText(/路径:\.agent\/project\.index\.json/), - ).not.toBeNull(); - expect(screen.getByText(/^game\/file-12\.html · 21B$/)).not.toBeNull(); - expect(screen.queryByText(/^game\/file-13\.html$/)).toBeNull(); - expect(screen.getByText(/还有 2 项/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('build_local_project_index', { - projectPath: '/tmp/authorized-game', - }); - }); - - it('requires confirmation for project index when project policy asks for it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['project.index'], - }, - }; - } - if (command === 'build_local_project_index') { - return { - projectPath: String(args?.projectPath ?? ''), - indexPath: '.agent/project.index.json', - fileCount: 1, - totalBytes: 12, - files: [ - { - path: 'game/index.html', - size: 12, - checksum: 'fnv1a64:index', - }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已设置本地项目:/tmp/authorized-game'), - ).not.toBeNull(); - - await submitChat('/index'); - - expect(await screen.findByText('准备刷新本地项目索引。')).not.toBeNull(); - expect(screen.getByText('project.index')).not.toBeNull(); - expect( - screen.getByText('刷新 /tmp/authorized-game/.agent/project.index.json'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'build_local_project_index', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText(/索引:1 个文件,12B/)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('build_local_project_index', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.pending', - commandId: 'project.index', - }); - expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { - projectPath: '/tmp/authorized-game', - event: 'permission.confirm', - commandId: 'project.index', - }); - }); - - it('checks LLM config from chat without leaking the API key value', async () => { - const invoke = vi.fn(async (command: string) => { - if (command === 'check_game_creator_llm_config') { - return { - agentMode: 'codex_app_server', - configured: true, - accountCredentialState: 'available', - officialRouteLocked: true, - baseUrl: 'https://router.genarrative.world/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'high', - stream: false, - webSearchEnabled: false, - error: null, - agents: [ - { - agentId: 'planner', - label: 'Planner', - agentMode: 'codex_app_server', - configured: true, - accountCredentialState: 'available', - officialRouteLocked: true, - baseUrl: 'https://router.genarrative.world/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'medium', - stream: true, - webSearchEnabled: false, - error: null, - }, - { - agentId: 'generator', - label: 'Generator', - agentMode: 'codex_app_server', - configured: true, - accountCredentialState: 'available', - officialRouteLocked: true, - baseUrl: 'https://router.genarrative.world/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'default', - stream: false, - webSearchEnabled: false, - error: null, - }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/llm-status'); - - expect(await screen.findByText(/官方智能服务:已连接/)).not.toBeNull(); - expect(screen.getByLabelText('聊天').textContent).toContain( - '官方智能服务:已连接,流式 关闭,联网检索 关闭,账号状态 已就绪。', - ); - expect(screen.getByLabelText('聊天').textContent).toContain( - 'Planner:已连接,流式 开启,联网检索 关闭,账号状态 已就绪', - ); - expect(screen.getByLabelText('聊天').textContent).toContain( - 'Generator:已连接,流式 关闭,联网检索 关闭,账号状态 已就绪', - ); - expect(screen.queryByText(/sk-test-secret/)).toBeNull(); - expect(screen.queryByText(/planner-secret/)).toBeNull(); - expect(screen.queryByText(/generator-secret/)).toBeNull(); - expect(invoke).toHaveBeenCalledWith('check_game_creator_llm_config'); - }); - - it('summarizes Agent LLM routes from chat without leaking API key values', async () => { - const invoke = vi.fn(async (command: string) => { - if (command === 'check_game_creator_llm_config') { - return { - agentMode: 'codex_app_server', - configured: true, - accountCredentialState: 'available', - officialRouteLocked: true, - baseUrl: 'https://router.genarrative.world/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'high', - stream: false, - webSearchEnabled: false, - error: null, - agents: [ - { - agentId: 'planner', - label: 'Planner', - agentMode: 'codex_app_server', - configured: true, - accountCredentialState: 'available', - officialRouteLocked: true, - baseUrl: 'https://router.genarrative.world/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'high', - stream: false, - webSearchEnabled: false, - error: null, - }, - { - agentId: 'generator', - label: 'Generator', - agentMode: 'codex_app_server', - configured: true, - accountCredentialState: 'available', - officialRouteLocked: true, - baseUrl: 'https://router.genarrative.world/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'high', - stream: false, - webSearchEnabled: false, - error: null, - }, - { - agentId: 'audio-sfx', - label: '音效规划', - agentMode: 'codex_app_server', - configured: true, - accountCredentialState: 'available', - officialRouteLocked: true, - baseUrl: 'https://router.genarrative.world/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'high', - stream: false, - webSearchEnabled: false, - error: null, - }, - ], - }; - } - throw new Error(`unexpected invoke ${command}`); - }); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/llm-routes'); - - expect(await screen.findByText(/Agent 智能服务状态:/)).not.toBeNull(); - const chatText = screen.getByLabelText('聊天').textContent ?? ''; - expect(chatText).toContain('总体:已连接 · 3/3 个 Agent 可用 · 0 个待处理'); - expect(chatText).toContain('账号状态:已就绪'); - expect(chatText).toContain('输出方式:流式关闭 · 联网检索关闭'); - expect(chatText).toContain('所有 Agent 使用统一的官方智能服务'); - expect(screen.getByRole('button', { name: '查看状态' })).not.toBeNull(); - expect(screen.queryByText(/sk-test-secret/)).toBeNull(); - expect(screen.queryByText(/generator-secret/)).toBeNull(); - expect(invoke).toHaveBeenCalledWith('check_game_creator_llm_config'); - expect(invoke).not.toHaveBeenCalledWith( - 'generate_local_game_draft', - expect.anything(), - ); - }); -} diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts index ec2eab47b..6fc36c056 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts @@ -4,20 +4,12 @@ import { isDirectCodexTurnAlreadyRunningError, } from '../../src/view/project-development/chat/conversation/directCodexConversation'; import { - act, - agentRuntimeUserInputRequest, - composerText, createGameCreationAppManifest, - createGameCreationAppSeedTasks, - createProjectChatRuntimeHarness, emptyProjectPolicy, expect, fireEvent, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, it, mockRoleAgentReply, - openMainProject, renderAppAt, roleAgentMockReply, screen, @@ -245,928 +237,6 @@ export function registerProjectConversationTests() { }); }); - it('does not persist the transient project open status while history is loading', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let finishConversationRead: - | ((value: { path: string; agentId: null; messages: [] }) => void) - | null = null; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_conversation') { - return await new Promise((resolve) => { - finishConversationRead = resolve as typeof finishConversationRead; - }); - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { - projectPath: '/tmp/authorized-game', - }); - }); - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.objectContaining({ - message: expect.objectContaining({ - content: '已设置本地项目:/tmp/authorized-game', - }), - }), - ); - - await act(async () => { - finishConversationRead?.({ - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }); - }); - - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.objectContaining({ - message: expect.objectContaining({ - content: '已设置本地项目:/tmp/authorized-game', - }), - }), - ); - }); - - it('retries unsaved slash chat messages after conversation persistence fails', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - let appendAttempts = 0; - let releaseRetryAppend: (() => void) | null = null; - const savedContents: string[] = []; - const makeConversationResult = () => ({ - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - if (command === 'chat_with_game_creator_role_agent') { - return { - replyText: mockRoleAgentReply(), - }; - } - if (command === 'append_local_conversation_message') { - appendAttempts += 1; - const message = args?.message as { content: string }; - if (appendAttempts === 1) { - throw new Error('conversation append failed once'); - } - if (appendAttempts === 2) { - return await new Promise((resolve) => { - releaseRetryAppend = () => { - savedContents.push(message.content); - resolve(makeConversationResult()); - }; - }); - } - savedContents.push(message.content); - return makeConversationResult(); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - await submitChat('/第一条本地命令'); - await waitFor(() => { - expect(appendAttempts).toBe(1); - }); - expect( - await screen.findByText( - '项目对话保存失败:conversation append failed once', - ), - ).not.toBeNull(); - - await submitChat('/第二条本地命令'); - await waitFor(() => { - expect(releaseRetryAppend).not.toBeNull(); - }); - await act(async () => { - releaseRetryAppend?.(); - }); - - await waitFor(() => { - expect(savedContents).toEqual([ - '/第一条本地命令', - '未知命令:/第一条本地命令。输入 /help 查看可用命令。', - '/第二条本地命令', - '未知命令:/第二条本地命令。输入 /help 查看可用命令。', - ]); - }); - expect( - screen.queryByText('项目对话保存失败:conversation append failed once'), - ).toBeNull(); - expect(screen.getByText('已打开:authorized-game')).not.toBeNull(); - }); - - it('confirms before saving slash conversation when policy requires it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const savedContents: string[] = []; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['conversation.write'], - }, - }; - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - if (command === 'append_local_conversation_message') { - savedContents.push( - String((args?.message as { content?: unknown })?.content ?? ''), - ); - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - await submitChat('/需要确认保存'); - - const conversationWriteCommand = - await screen.findByText('conversation.write'); - expect(await screen.findByText('等待确认保存项目对话')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.anything(), - ); - - fireEvent.click( - within( - conversationWriteCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '确认' }), - ); - - await waitFor(() => { - expect(savedContents).toEqual([ - '/需要确认保存', - '未知命令:/需要确认保存。输入 /help 查看可用命令。', - ]); - }); - }); - - it('does not immediately re-prompt after cancelling slash conversation save', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const savedContents: string[] = []; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['conversation.write'], - }, - }; - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - if (command === 'append_local_conversation_message') { - savedContents.push( - String((args?.message as { content?: unknown })?.content ?? ''), - ); - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - await submitChat('/先不保存'); - - const firstPrompt = await screen.findByText('conversation.write'); - fireEvent.click( - within(firstPrompt.closest('.pending-command') as HTMLElement).getByRole( - 'button', - { name: '取消' }, - ), - ); - - await waitFor(() => { - expect(screen.queryByText('conversation.write')).toBeNull(); - }); - expect(screen.getByText('已打开:authorized-game')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.anything(), - ); - - await submitChat('/继续补充'); - - const secondPrompt = await screen.findByText('conversation.write'); - fireEvent.click( - within(secondPrompt.closest('.pending-command') as HTMLElement).getByRole( - 'button', - { name: '确认' }, - ), - ); - - await waitFor(() => { - expect(savedContents).toEqual([ - '/先不保存', - '未知命令:/先不保存。输入 /help 查看可用命令。', - '/继续补充', - '未知命令:/继续补充。输入 /help 查看可用命令。', - ]); - }); - }); - - it('persists slash chat messages submitted while a previous write is still running', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const savedContents: string[] = []; - let releaseFirstAppend: (() => void) | null = null; - const makeConversationResult = () => ({ - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return makeConversationResult(); - } - if (command === 'read_local_project_file') { - throw new Error( - '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', - ); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - if (command === 'append_local_conversation_message') { - const message = args?.message as { content: string }; - if (savedContents.length === 0 && !releaseFirstAppend) { - return await new Promise((resolve) => { - releaseFirstAppend = () => { - savedContents.push(message.content); - resolve(makeConversationResult()); - }; - }); - } - savedContents.push(message.content); - return makeConversationResult(); - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - await submitChat('/第一条并发命令'); - await waitFor(() => { - expect(releaseFirstAppend).not.toBeNull(); - }); - await submitChat('/第二条并发命令'); - - releaseFirstAppend?.(); - - await waitFor(() => { - expect(savedContents).toEqual([ - '/第一条并发命令', - '未知命令:/第一条并发命令。输入 /help 查看可用命令。', - '/第二条并发命令', - '未知命令:/第二条并发命令。输入 /help 查看可用命令。', - ]); - }); - }); - - it('opens a specific agent conversation and persists messages to that agent', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const agentMessages: Array<{ - schemaVersion: string; - role: 'user' | 'assistant'; - content: string; - agentId: string | null; - updatedAt: number; - }> = []; - let agentConversationReadCount = 0; - let agentMemoryContent = '# 策划 Director 私有记忆\n- 保留轻量像素风\n'; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_conversation') { - if (args?.agentId) { - agentConversationReadCount += 1; - } - return { - path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', - agentId: args?.agentId, - messages: [...agentMessages], - }; - } - if (command === 'read_local_agent_memory') { - return { - taskId: args?.taskId, - path: '/tmp/authorized-game/memory/agents/design/director.md', - content: agentMemoryContent, - exists: true, - }; - } - if (command === 'write_local_agent_memory') { - agentMemoryContent = String(args?.content ?? ''); - return { - taskId: args?.taskId, - path: '/tmp/authorized-game/memory/agents/design/director.md', - content: agentMemoryContent, - exists: true, - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: '/tmp/authorized-game/.agent/run.latest.json', - content: JSON.stringify({ - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-agent-dialog-evidence', - commandId: 'game.generate_draft', - status: 'running', - lifecycleStatus: 'pending', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个厨房弹幕游戏', - coordination: 'Planner', - steps: [ - { - pass: 1, - agent: 'Planner', - phase: 'plan', - taskId: 'design-director', - group: 'design', - role: 'Director', - status: 'running', - inputPaths: ['memory/session.md'], - outputPaths: ['.agent/spec.md'], - summary: '正在拆解创作方向', - toolCalls: [ - { - toolId: 'llm.planner', - status: 'ok', - inputPaths: ['memory/session.md'], - outputPaths: ['.agent/spec.md'], - summary: 'Planner 已读取短期记忆', - }, - { - toolId: 'agent.tool.suggest.canvas.project_sync', - status: 'suggested', - inputPaths: ['assets/manifest.art.json'], - outputPaths: [], - summary: '建议用户确认 /sync-canvas-project <画板项目ID>', - }, - ], - }, - ], - artifacts: [], - taskGraph: { - goal: '做一个厨房弹幕游戏', - readyTaskIds: [], - activeTaskIds: ['design-director'], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'continue', - error: null, - updatedAt: 1, - } satisfies GameCreationAgentRunTrace), - }; - } - if (command === 'list_local_project_files') { - return { - projectPath: String(args?.projectPath ?? ''), - files: [ - { - path: '.agent/runs/run-agent-dialog-evidence.json', - kind: 'file', - size: 1, - }, - ], - }; - } - if (command === 'chat_with_game_creator_role_agent') { - return { - replyText: mockRoleAgentReply(), - }; - } - if (command === 'append_local_conversation_message') { - const message = args?.message as { - role: 'user' | 'assistant'; - content: string; - agentId: string | null; - }; - if (args?.agentId) { - agentMessages.push({ - schemaVersion: 'game-creator-conversation.v1', - role: message.role, - content: message.content, - agentId: String(args.agentId), - updatedAt: agentMessages.length + 1, - }); - } - return { - path: args?.agentId - ? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl' - : '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: args?.agentId ?? null, - messages: args?.agentId ? [...agentMessages] : [], - }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); - - expect(await screen.findByText('正在拆解创作方向')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); - const agentDialog = await screen.findByLabelText('Agent 对话'); - expect(agentDialog).not.toBeNull(); - expect(agentDialog.textContent).toContain('正在拆解创作方向'); - expect(agentDialog.textContent).toContain('pass 1 · plan'); - expect(agentDialog.textContent).toContain('run: pending'); - expect(agentDialog.textContent).toContain('编排:本轮 active'); - expect(agentDialog.textContent).toContain( - '已读取 0 条:/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', - ); - - const input = screen.getByLabelText('Agent 对话内容'); - fireEvent.change(input, { target: { value: '优先保留轻量像素风' } }); - fireEvent.submit(input.closest('form') as HTMLFormElement); - - expect( - await screen.findByText( - '已保存 2 条:/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', - ), - ).not.toBeNull(); - expect(screen.getByText(roleAgentMockReply)).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_conversation', { - projectPath: '/tmp/authorized-game', - agentId: 'design-director', - }); - expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain( - '保留轻量像素风', - ); - expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( - 'in: memory/session.md', - ); - expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( - 'out: .agent/spec.md', - ); - expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( - 'tool: llm.planner · ok · Planner 已读取短期记忆', - ); - expect( - screen.getByRole('button', { name: '填入读取 memory/session.md' }), - ).not.toBeNull(); - expect(screen.getByRole('button', { name: '填入同步命令' })).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_agent_memory', { - projectPath: '/tmp/authorized-game', - taskId: 'design-director', - }); - expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', { - projectPath: '/tmp/authorized-game', - agentId: 'design-director', - message: { - role: 'user', - content: '优先保留轻量像素风', - agentId: null, - }, - }); - expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', { - projectPath: '/tmp/authorized-game', - agentId: 'design-director', - message: { - role: 'assistant', - content: roleAgentMockReply, - agentId: null, - }, - }); - expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', { - projectPath: '/tmp/authorized-game', - agentId: 'design-director', - prompt: '优先保留轻量像素风', - }); - fireEvent.change(screen.getByLabelText('Agent 对话内容'), { - target: { value: '稳定结论:锅铲音效要跟随连击节奏' }, - }); - fireEvent.click( - within(agentDialog).getByRole('button', { name: '记入记忆' }), - ); - expect( - await screen.findByText('已写入 拆解创作方向 私有记忆。'), - ).not.toBeNull(); - expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain( - '锅铲音效要跟随连击节奏', - ); - expect(invoke).toHaveBeenCalledWith('write_local_agent_memory', { - projectPath: '/tmp/authorized-game', - taskId: 'design-director', - content: - '# 策划 Director 私有记忆\n- 保留轻量像素风\n- 稳定结论:锅铲音效要跟随连击节奏\n', - }); - agentMessages.push({ - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: '刷新后外部记录', - agentId: 'design-director', - updatedAt: 2, - }); - agentMessages.push({ - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: - '后台任务失败:kind=codex-app-server-context-window-exceeded ' + - `fingerprint=${'c'.repeat(64)} chars=2048`, - agentId: 'design-director', - updatedAt: 3, - }); - fireEvent.click(within(agentDialog).getByRole('button', { name: '刷新' })); - expect(await screen.findByText('刷新后外部记录')).not.toBeNull(); - expect( - await screen.findByText( - '设计实现 Agent 模型上下文已超限,请缩小任务范围后重试', - ), - ).not.toBeNull(); - expect(agentDialog.textContent).not.toContain('fingerprint'); - expect(agentDialog.textContent).not.toContain('chars='); - expect(agentConversationReadCount).toBeGreaterThanOrEqual(2); - fireEvent.click(screen.getByRole('button', { name: '填入同步命令' })); - expect(screen.queryByLabelText('Agent 对话')).toBeNull(); - expect(await composerText()).toBe('/sync-canvas-project '); - fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); - const reopenedAgentDialog = await screen.findByLabelText('Agent 对话'); - fireEvent.click( - within(reopenedAgentDialog).getByRole('button', { - name: '填入读取 .agent/spec.md', - }), - ); - expect(screen.queryByLabelText('Agent 对话')).toBeNull(); - expect(await composerText()).toBe('/read .agent/spec.md'); - expect(invoke).not.toHaveBeenCalledWith( - 'sync_canvas_project_assets', - expect.anything(), - ); - }); - - it('answers Needs input from the selected project Agent dialog', async () => { - const harness = createProjectChatRuntimeHarness(); - const agentId = 'design-director'; - const sessionId = 'selected-agent-session'; - const runId = 'selected-agent-needs-input-run'; - const request = agentRuntimeUserInputRequest({ - agentId, - sessionId, - runId, - requestId: 'selected-agent-request', - actionId: 'selected-agent-action', - }); - let messages: Array> = []; - let currentState: Record = { - schemaVersion: 'game-creator-agent-runtime.v1', - agentId, - taskId: agentId, - sessionId, - runId, - source: 'agent-background-task', - status: 'waiting-for-user-input', - phase: 'waiting-for-user-input', - currentTask: '准备首版角色规范图', - currentGoal: '确定美术方向', - currentAction: '等待用户补充关键信息', - waitingOn: '你的澄清回答', - nextStep: '提交全部回答后继续同一 Run', - plan: [], - observations: [], - allowedTools: ['user.input_request'], - pendingToolAction: null, - lastResponse: null, - error: null, - updatedAt: 7000, - }; - const runtimeResult = () => ({ - state: currentState, - sessionPath: `${harness.projectPath}/.agent/runtime/agents/${agentId}.json`, - eventPath: `${harness.projectPath}/.agent/runtime/events/${agentId}.jsonl`, - taskPath: `${harness.projectPath}/.agent/runtime/tasks/${agentId}.jsonl`, - taskQueue: { - total: 1, - pending: 0, - running: currentState.status === 'running' ? 1 : 0, - waitingForConfirmation: 0, - waitingForUserInput: - currentState.status === 'waiting-for-user-input' ? 1 : 0, - cancelled: 0, - completed: 0, - failed: 0, - latestRunId: runId, - updatedAt: Number(currentState.updatedAt), - }, - recentEvents: [], - recentTasks: [], - userInputRequest: - currentState.status === 'waiting-for-user-input' ? request : null, - }); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'check_game_creator_llm_config') { - return { - configured: true, - apiKeyPresent: true, - baseUrl: 'https://llm.example.test/v1', - model: 'gpt-5.5', - apiKind: 'openai_chat', - stream: true, - webSearchEnabled: false, - error: null, - agents: [], - }; - } - if ( - command === 'read_local_conversation' && - args?.agentId === agentId - ) { - return { - path: `${harness.projectPath}/.agent/conversations/agents/${agentId}/sessions/${sessionId}.jsonl`, - agentId, - sessionId, - messages: [...messages], - }; - } - if ( - command === 'read_game_creator_agent_runtime' && - args?.agentId === agentId - ) { - return runtimeResult(); - } - if (command === 'read_local_agent_memory') { - return { - taskId: agentId, - path: `${harness.projectPath}/memory/agents/design/director.md`, - content: '', - exists: false, - }; - } - if ( - command === 'answer_game_creator_agent_runtime_user_input' && - args?.agentId === agentId - ) { - messages = [ - { - schemaVersion: 'game-creator-conversation.v1', - role: 'user', - content: String( - (args.answers as Record).visual_direction, - ), - agentId, - messageId: 'selected-agent-answer', - updatedAt: 7100, - }, - ]; - currentState = { - ...currentState, - status: 'running', - phase: 'planning', - currentAction: '根据用户回答继续规划', - updatedAt: 7200, - }; - return runtimeResult(); - } - return harness.invoke(command, args); - }, - ); - window.__TAURI__ = { - core: { invoke }, - event: { listen: harness.listen }, - }; - renderAppAt('/'); - await openMainProject(harness.projectPath); - fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); - const dialog = await screen.findByLabelText('Agent 对话'); - const card = await within(dialog).findByLabelText('Needs input'); - - expect( - (within(dialog).getByLabelText('Agent 对话内容') as HTMLInputElement) - .disabled, - ).toBe(true); - expect( - ( - within(dialog).getByRole('button', { - name: '后台运行', - }) as HTMLButtonElement - ).disabled, - ).toBe(true); - fireEvent.click(within(card).getByRole('button', { name: /手绘风/ })); - fireEvent.click(within(card).getByRole('button', { name: '提交回答' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith( - 'answer_game_creator_agent_runtime_user_input', - { - projectPath: harness.projectPath, - agentId, - runId, - actionId: request.actionId, - requestId: request.requestId, - responseId: expect.stringMatching(/^app-user-input-/), - answers: { visual_direction: '手绘风' }, - }, - ); - }); - expect(await within(dialog).findByText('手绘风')).not.toBeNull(); - expect(within(dialog).queryByLabelText('Needs input')).toBeNull(); - }); - it.skip('requires confirmation before reading a specific agent conversation when policy asks for it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 728ceedf7..b6708f836 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -22,7 +22,6 @@ import { cleanup, createGameCreationAppManifest, createGameCreationAppSeedTasks, - emptyProjectPolicy, expect, findResourceSelectButton, fireEvent, @@ -31,7 +30,6 @@ import { getResourceSelectButton, installResizeObserverStub, it, - mockRoleAgentReply, openResourceFilterPanel, ProjectDevelopmentView, queryResourceSelectButton, @@ -41,7 +39,6 @@ import { renderAppAt, resolve, screen, - submitChat, vi, waitFor, within, @@ -6554,108 +6551,7 @@ export function registerProjectWorkbenchFoundationTests() { }); } -export function registerProjectWorkbenchNavigationTests() { - it('keeps project switching inside the single-window client flow', async () => { - const invoke = vi.fn(async () => undefined); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main'); - - fireEvent.click(screen.getByRole('button', { name: '切换项目' })); - - expect(screen.getByText('请回到首页的项目组切换项目。')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'open_game_creator_launcher_window', - ); - - await submitChat('/switch-project'); - - expect( - screen.getAllByText('请回到首页的项目组切换项目。').length, - ).toBeGreaterThanOrEqual(2); - expect(invoke).not.toHaveBeenCalledWith( - 'open_game_creator_launcher_window', - ); - }); - - it('opens the current project directory from the main project window', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'chat_with_game_creator_role_agent') { - return { - replyText: mockRoleAgentReply(), - }; - } - if (command === 'append_local_conversation_message') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'open_local_project_directory') { - return undefined; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - - await screen.findByText('已打开:authorized-game'); - const revealButtons = screen.getAllByRole('button', { name: '显示目录' }); - fireEvent.click(revealButtons[revealButtons.length - 1]); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('open_local_project_directory', { - projectPath: '/tmp/authorized-game', - }); - }); - expect(await screen.findByText('已打开项目目录。')).not.toBeNull(); - - await submitChat('/open-project'); - - await waitFor(() => { - expect( - invoke.mock.calls.filter( - ([command]) => command === 'open_local_project_directory', - ), - ).toHaveLength(2); - }); - expect( - screen.getAllByText('已打开项目目录。').length, - ).toBeGreaterThanOrEqual(2); - }); -} +export function registerProjectWorkbenchNavigationTests() {} export function registerProjectAgentStatusTests() { it('keeps ready task scheduling out of the normal user window', async () => {