/** @vitest-environment jsdom */ import { act, cleanup, fireEvent, render, screen, waitFor, within, } from '@testing-library/react'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { createGameCreationAppManifest, createGameCreationAppSeedTasks, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, type GameCreationAgentRunTrace, } from '../../../packages/shared/src/contracts/gameCreationApp'; import type { AuthUser } from '../../../packages/shared/src/contracts/auth'; import { App, AuthenticatedClient, WorkspaceLauncher, deriveAgentStatusCards, } from '../src/App'; const testAuthUser: AuthUser = { id: 'user-test', publicUserCode: 'tn-test', displayName: '测试用户', avatarUrl: null, phoneNumber: null, phoneNumberMasked: '138****0000', loginMethod: 'password', bindingStatus: 'active', wechatBound: false, wechatDisplayName: null, wechatAccount: null, }; function renderAppAt(path: string) { window.history.pushState({}, '', path); render(React.createElement(App)); } function renderLauncherAt(path: string) { window.history.pushState({}, '', path); render( React.createElement(WorkspaceLauncher, { currentUser: testAuthUser, onLogout: vi.fn(), }), ); } function renderLauncherProjectsAt(path: string) { renderLauncherAt(path); fireEvent.click(screen.getByRole('button', { name: '项目组' })); } function submitChat(value: string) { fireEvent.change(screen.getByLabelText('创作想法'), { target: { value }, }); fireEvent.click(screen.getByRole('button', { name: '发送' })); } function emptyProjectPolicy() { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: [], }, }; } afterEach(() => { cleanup(); window.history.pushState({}, '', '/'); window.localStorage.clear(); delete window.__TAURI__; vi.restoreAllMocks(); }); describe('AI 游戏创作 App 界面边界', () => { it('deduplicates startup auth refresh when React StrictMode hydrates twice', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation( async (input: RequestInfo | URL) => { const url = String(input); if (url === '/api/auth/refresh') { return new Response(JSON.stringify({ token: 'fresh-token' }), { status: 200, }); } if (url === '/api/auth/me') { return new Response( JSON.stringify({ user: testAuthUser, availableLoginMethods: ['password'], }), { status: 200 }, ); } throw new Error(`unexpected fetch ${url}`); }, ); render( React.createElement( React.StrictMode, null, React.createElement(AuthenticatedClient, null, ({ user }) => React.createElement('main', { 'aria-label': '已登录' }, user.displayName), ), ), ); expect(await screen.findByLabelText('已登录')).not.toBeNull(); expect( fetchSpy.mock.calls.filter(([input]) => String(input) === '/api/auth/refresh'), ).toHaveLength(1); expect(window.localStorage.getItem('genarrative.auth.access-token.v1')).toBe( 'fresh-token', ); }); it('shows phone code login before entering the workspace', async () => { vi.spyOn(globalThis, 'fetch').mockImplementation( async (input: RequestInfo | URL) => { const url = String(input); if (url === '/api/auth/refresh') { return new Response('', { status: 401 }); } throw new Error(`unexpected fetch ${url}`); }, ); render( React.createElement(AuthenticatedClient, null, () => React.createElement('main', { 'aria-label': '已登录' }, 'ready'), ), ); expect(await screen.findByRole('main', { name: '登录' })).not.toBeNull(); expect( screen.getByRole('button', { name: '验证码登录' }), ).not.toBeNull(); expect(screen.getByRole('button', { name: '密码登录' })).not.toBeNull(); expect(screen.getByLabelText('手机号')).not.toBeNull(); expect(screen.getByLabelText('验证码')).not.toBeNull(); expect(screen.queryByLabelText('已登录')).toBeNull(); }); it('logs in with a phone code and stores the returned token', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation( async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url === '/api/auth/refresh') { return new Response('', { status: 401 }); } if (url === '/api/auth/phone/send-code') { expect(JSON.parse(String(init?.body))).toMatchObject({ phone: '13800000000', scene: 'login', }); return new Response( JSON.stringify({ ok: true, cooldownSeconds: 60, expiresInSeconds: 300, providerRequestId: 'sms-1', }), { status: 200 }, ); } if (url === '/api/auth/phone/login') { expect(JSON.parse(String(init?.body))).toMatchObject({ phone: '13800000000', code: '123456', }); return new Response( JSON.stringify({ token: 'phone-token', user: { ...testAuthUser, loginMethod: 'phone' }, created: false, referral: null, }), { status: 200 }, ); } throw new Error(`unexpected fetch ${url}`); }, ); render( React.createElement(AuthenticatedClient, null, ({ user }) => React.createElement('main', { 'aria-label': '已登录' }, user.loginMethod), ), ); await screen.findByRole('main', { name: '登录' }); fireEvent.change(screen.getByLabelText('手机号'), { target: { value: '138 0000 0000' }, }); fireEvent.click(screen.getByRole('button', { name: '获取验证码' })); expect(await screen.findByText('验证码已发送,300 秒内有效')).not.toBeNull(); fireEvent.change(screen.getByLabelText('验证码'), { target: { value: '123456' }, }); fireEvent.click(screen.getByRole('button', { name: '登录' })); expect(await screen.findByLabelText('已登录')).not.toBeNull(); expect(screen.getByLabelText('已登录').textContent).toBe('phone'); expect(window.localStorage.getItem('genarrative.auth.access-token.v1')).toBe( 'phone-token', ); expect( fetchSpy.mock.calls.filter( ([input]) => String(input) === '/api/auth/phone/send-code', ), ).toHaveLength(1); expect( fetchSpy.mock.calls.filter( ([input]) => String(input) === '/api/auth/phone/login', ), ).toHaveLength(1); }); it('shows a clear login service error instead of raw Load failed', async () => { vi.spyOn(globalThis, 'fetch').mockImplementation( async (input: RequestInfo | URL) => { const url = String(input); if (url === '/api/auth/refresh') { return new Response('', { status: 401 }); } if (url === '/api/auth/phone/login') { throw new TypeError('Load failed'); } throw new Error(`unexpected fetch ${url}`); }, ); render( React.createElement(AuthenticatedClient, null, () => React.createElement('main', { 'aria-label': '已登录' }, 'ready'), ), ); await screen.findByRole('main', { name: '登录' }); fireEvent.change(screen.getByLabelText('手机号'), { target: { value: '13800000000' }, }); fireEvent.change(screen.getByLabelText('验证码'), { target: { value: '123456' }, }); fireEvent.click(screen.getByRole('button', { name: '登录' })); expect( await screen.findByText( '无法连接登录服务,请确认配套后端或 API 代理已启动后重试', ), ).not.toBeNull(); expect(screen.queryByText(/Load failed/u)).toBeNull(); expect(screen.queryByLabelText('已登录')).toBeNull(); }); it('keeps the stored token when startup auth check cannot reach the service', async () => { window.localStorage.setItem( 'genarrative.auth.access-token.v1', 'existing-token', ); const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation( async (input: RequestInfo | URL) => { const url = String(input); if (url === '/api/auth/me') { throw new TypeError('Load failed'); } throw new Error(`unexpected fetch ${url}`); }, ); render( React.createElement(AuthenticatedClient, null, ({ user }) => React.createElement('main', { 'aria-label': '已登录' }, user.displayName), ), ); expect(await screen.findByRole('main', { name: '登录' })).not.toBeNull(); expect( screen.getByText( '无法连接登录服务,请确认配套后端或 API 代理已启动后重试', ), ).not.toBeNull(); expect(window.localStorage.getItem('genarrative.auth.access-token.v1')).toBe( 'existing-token', ); expect(screen.queryByLabelText('已登录')).toBeNull(); expect( fetchSpy.mock.calls.filter(([input]) => String(input) === '/api/auth/me'), ).toHaveLength(1); }); it('still calls logout when token refresh fails during logout retry', async () => { window.localStorage.setItem( 'genarrative.auth.access-token.v1', 'existing-token', ); let logoutCalls = 0; const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation( async (input: RequestInfo | URL) => { const url = String(input); if (url === '/api/auth/me') { return new Response( JSON.stringify({ user: testAuthUser, availableLoginMethods: ['password'], }), { status: 200 }, ); } if (url === '/api/auth/logout') { logoutCalls += 1; return new Response('', { status: logoutCalls === 1 ? 500 : 200 }); } if (url === '/api/auth/refresh') { return new Response('', { status: 401 }); } throw new Error(`unexpected fetch ${url}`); }, ); render( React.createElement(AuthenticatedClient, null, ({ user, logout }) => React.createElement( 'main', { 'aria-label': '已登录' }, React.createElement('span', null, user.displayName), React.createElement('button', { type: 'button', onClick: logout }, '退出'), ), ), ); expect(await screen.findByLabelText('已登录')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '退出' })); expect(await screen.findByRole('main', { name: '登录' })).not.toBeNull(); expect(screen.getByText('已退出登录')).not.toBeNull(); expect(window.localStorage.getItem('genarrative.auth.access-token.v1')).toBe( null, ); expect( fetchSpy.mock.calls.filter( ([input]) => String(input) === '/api/auth/logout', ), ).toHaveLength(2); expect( fetchSpy.mock.calls.filter( ([input]) => String(input) === '/api/auth/refresh', ), ).toHaveLength(1); }); it('derives agent card status from the latest run trace step', () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const taskGraphTasks = createGameCreationAppSeedTasks().map((task) => task.id === 'audio-director' ? { ...task, status: 'waiting-for-confirmation' as const } : task, ); const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-agent-status-cards', commandId: 'game.generate_draft', status: 'running', lifecycleStatus: 'pending', passes: 1, maxPasses: 3, toolCallCount: 3, maxToolCalls: 128, stopReason: 'running', goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Orchestrator', 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 已读取短期记忆', }, ], }, { pass: 1, agent: 'Asset', phase: 'role', taskId: null, group: 'art', role: 'Asset', status: 'failed', inputPaths: [], outputPaths: [], summary: '美术资产生成失败', toolCalls: [], }, { pass: 1, agent: 'Generator', phase: 'write', taskId: 'code-prototype', group: 'code', role: 'Code', status: 'passed', inputPaths: [], outputPaths: [], summary: '代码已通过生成', toolCalls: [], }, { pass: 1, agent: 'Evaluator', phase: 'evaluation', status: 'passed', inputPaths: ['.agent/passes/pass-1/draft.json', '.agent/findings.md'], outputPaths: ['.agent/findings.md'], summary: '质量评审通过', toolCalls: [ { toolId: 'agent.evaluate', status: 'ok', summary: 'Evaluator 质量评审', }, ], }, ], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: ['audio-director'], activeTaskIds: ['design-director'], carriedTaskIds: ['balance-director'], repairFocus: [], repairRoutes: [], tasks: taskGraphTasks, }, passPlans: [], nextStep: 'continue', error: null, updatedAt: 1, }; const cards = deriveAgentStatusCards(manifest, trace); expect(cards.find((card) => card.id === 'design-director')).toMatchObject({ taskId: 'design-director', status: 'running', summary: '正在拆解创作方向', pass: 1, phase: 'plan', lifecycleStatus: 'pending', hasRecentEvidence: true, taskGraphState: 'active', inputPaths: ['memory/session.md'], outputPaths: ['.agent/spec.md'], toolCalls: [ expect.objectContaining({ toolId: 'llm.planner', status: 'ok', summary: 'Planner 已读取短期记忆', }), ], }); expect(cards.find((card) => card.id === 'art-asset')).toMatchObject({ status: 'failed', summary: '美术资产生成失败', hasRecentEvidence: true, }); expect(cards.find((card) => card.id === 'code-code')).toMatchObject({ taskId: 'code-prototype', status: 'completed', summary: '代码已通过生成', }); expect(cards.find((card) => card.id === 'audio-director')).toMatchObject({ taskId: 'audio-director', status: 'waiting-for-confirmation', taskGraphState: 'ready', hasRecentEvidence: false, }); expect(cards.find((card) => card.id === 'balance-director')).toMatchObject({ taskId: 'balance-director', taskGraphState: 'carried', }); }); it('keeps the user surface to chat, upload, config and command confirmation', async () => { renderAppAt('/?main'); expect(screen.getByLabelText('聊天')).not.toBeNull(); expect(screen.getByLabelText('Agent 状态')).not.toBeNull(); const composerInput = screen.getByLabelText('创作想法'); expect(composerInput).not.toBeNull(); expect(screen.getByText('上传')).not.toBeNull(); expect(screen.getByRole('button', { name: '命令' })).not.toBeNull(); expect(screen.getByRole('button', { name: '能力' })).not.toBeNull(); expect(screen.getByRole('button', { name: '配置' })).not.toBeNull(); expect(screen.getByRole('button', { name: 'LLM状态' })).not.toBeNull(); expect(screen.getByRole('button', { name: '显示目录' })).not.toBeNull(); expect(screen.queryByLabelText('项目摘要')).toBeNull(); expect( (screen.getByRole('button', { name: '灵感草稿' }) as HTMLButtonElement) .disabled, ).toBe(false); expect( (screen.getByRole('button', { name: '项目状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '权限' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '审计' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '运行' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '资产' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '打开画板' }) as HTMLButtonElement) .disabled, ).toBe(false); expect( ( screen.getByRole('button', { name: '导入画板资产', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '任务' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '索引确认' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '资产登记确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '记忆写入确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '预览确认' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '打开预览确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '停止预览确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: 'Agent确认' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '读对话确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '存对话确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: 'Trace' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '文件' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '索引' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '记忆' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '短期记忆' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '记到黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '覆盖黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '清空黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '快照' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '快照列表' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '历史' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '白名单' }) as HTMLButtonElement) .disabled, ).toBe(false); expect( (screen.getByRole('button', { name: '静态自检' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '启动预览' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '打开预览' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '预览状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '停止预览' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '刷新状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '刷新 Agent' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '终止' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '重试' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '继续' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '继续说明' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '输出' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '活动' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '上下文包' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: /拆解创作方向/, }) as HTMLButtonElement ).disabled, ).toBe(true); fireEvent.click(screen.getByRole('button', { name: '打开画板' })); expect(composerInput).toHaveProperty('value', '/canvas '); fireEvent.click(screen.getByRole('button', { name: '灵感草稿' })); expect(composerInput).toHaveProperty( 'value', '像素风厨房弹幕小游戏:玩家用方向键躲避飞来的食材,收集调料加分,60 秒内尽量高分,失败后可一键重开。', ); expect(screen.queryByText('game.generate_draft')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '白名单' })); expect(await screen.findByText(/可运行受限命令:/)).not.toBeNull(); expect(screen.getByRole('button', { name: '切换项目' })).not.toBeNull(); expect(screen.getByText('想做什么游戏?')).not.toBeNull(); expect(screen.getAllByText('暂无最近运行证据').length).toBeGreaterThan(0); expect(screen.queryByLabelText('工作区管理')).toBeNull(); expect(screen.queryByLabelText('开发环境')).toBeNull(); expect(screen.queryByLabelText('运行时配置')).toBeNull(); expect(screen.queryByText('Agent 能力')).toBeNull(); expect(screen.queryByText('编排 Trace')).toBeNull(); }); it('starts from the client home and opens a project in the same window', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath: String(args?.projectPath ?? ''), exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'authorized-game', recentRunStatus: null, recentRunStopReason: null, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); expect(screen.getByLabelText('GameAgent 客户端')).not.toBeNull(); expect(screen.queryByLabelText('通知')).toBeNull(); expect(screen.getByRole('button', { name: '我的' })).not.toBeNull(); expect(screen.getByRole('button', { name: '帮助' })).not.toBeNull(); expect( within(screen.getByLabelText('账户资产')).queryByRole('button', { name: '账户', }), ).toBeNull(); expect( within(screen.getByLabelText('账户资产')).getByRole('button', { name: '泥点余额', }), ).not.toBeNull(); expect( within(screen.getByLabelText('账户资产')).getByRole('button', { name: '充值', }), ).not.toBeNull(); expect( screen .getByLabelText('GameAgent 客户端') .querySelector('.launcher-main') ?.className.includes('launcher-main-with-promo'), ).toBe(false); expect(screen.queryByLabelText('聊天')).toBeNull(); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); await waitFor(() => { expect(screen.getByLabelText('项目开发画布')).not.toBeNull(); }); expect(screen.getByText('authorized-game')).not.toBeNull(); expect(screen.getByText('/tmp/authorized-game')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); expect(window.localStorage.length).toBe(1); expect( window.localStorage.getItem(window.localStorage.key(0) ?? ''), ).toContain('/tmp/authorized-game'); }); it('refreshes recent project status before entering the project placeholder', async () => { let inspectCount = 0; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { inspectCount += 1; return { projectPath: String(args?.projectPath ?? ''), exists: true, isDirectory: true, isGameCreatorProject: true, projectName: inspectCount === 1 ? 'authorized-game' : 'authorized-game-fresh', recentRunStatus: inspectCount === 1 ? null : 'passed', recentRunStopReason: inspectCount === 1 ? null : 'evaluator-passed', }; } throw new Error(`unexpected invoke ${command}`); }, ); window.localStorage.setItem( 'genarrative-ai-game-creator.recent-workspaces.v1', JSON.stringify(['/tmp/authorized-game']), ); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); expect(await screen.findByText('authorized-game')).not.toBeNull(); fireEvent.click(screen.getByText('authorized-game')); expect(await screen.findByLabelText('项目开发画布')).not.toBeNull(); expect(screen.getByText('authorized-game-fresh')).not.toBeNull(); expect(screen.getByText('passed')).not.toBeNull(); expect(screen.getByText('evaluator-passed')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); expect(inspectCount).toBeGreaterThanOrEqual(2); }); it('rejects unsafe main-window projectPath query values before Tauri calls', async () => { const invoke = vi.fn(async () => undefined); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=relative-game'); expect(await screen.findByText('请提供工作区绝对路径')).not.toBeNull(); expect(invoke).not.toHaveBeenCalled(); cleanup(); window.history.pushState({}, '', '/'); renderAppAt('/?main&projectPath=%2Ftmp%2Fbad%0Apath'); expect( await screen.findByText('工作区路径不能包含控制字符'), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalled(); }); it('does not open a missing or non-directory path from the open action', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { const projectPath = String(args?.projectPath ?? ''); if (projectPath === '/tmp/broken-status') { throw new Error('status failed'); } return { projectPath, exists: projectPath !== '/tmp/missing-game', isDirectory: projectPath !== '/tmp/not-a-folder', isGameCreatorProject: projectPath === '/tmp/authorized-game', projectName: projectPath === '/tmp/authorized-game' ? 'authorized-game' : null, recentRunStatus: null, recentRunStopReason: null, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/missing-game' }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); expect(await screen.findByText('项目目录不存在')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/not-a-folder' }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); expect(await screen.findByText('项目路径不是文件夹')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/plain-folder' }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); expect( await screen.findByText('这不是已初始化的 AI 游戏项目,请使用新建项目。'), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); }); it('fills the project path from the native directory picker', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'pick_local_project_directory') { return '/tmp/picked-game'; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '选择' })); expect(await screen.findByDisplayValue('/tmp/picked-game')).not.toBeNull(); expect(screen.getByText('已选择项目目录')).not.toBeNull(); }); it('keeps the typed project path when the native directory picker is cancelled', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'pick_local_project_directory') { return null; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/manual-game' }, }); fireEvent.click(screen.getByRole('button', { name: '选择' })); expect(await screen.findByDisplayValue('/tmp/manual-game')).not.toBeNull(); expect(screen.getByText('已取消')).not.toBeNull(); }); it('creates a project from the home agent input without starting generation', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', 'home-created-game', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'pick_local_project_directory') { return '/tmp/home-created-game'; } if (command === 'is_local_project_directory_non_empty') { return false; } if (command === 'init_local_game_project') { return { projectPath: String(args?.projectPath ?? ''), manifestPath: `${String(args?.projectPath ?? '')}/.agent/manifest.json`, manifest, }; } if (command === 'upload_local_asset') { return { id: 'asset-upload-1', localPath: 'assets/uploads/reference.png', absolutePath: '/tmp/home-created-game/assets/uploads/reference.png', manifestPath: '/tmp/home-created-game/.agent/manifest.json', }; } if (command === 'append_local_conversation_message') { return { path: '.agent/conversations/project.jsonl', agentId: null, messages: [], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '做素材' })); fireEvent.change(screen.getByLabelText('创作想法'), { target: { value: '做一张像素风厨师角色图' }, }); const fileInput = document.querySelector( '.launcher-file-input', ) as HTMLInputElement; const file = new File(['pixel-reference'], 'reference.png', { type: 'image/png', }); const fileBytes = Array.from(new TextEncoder().encode('pixel-reference')); Object.defineProperty(file, 'arrayBuffer', { value: async () => new Uint8Array(fileBytes).buffer, }); fireEvent.change(fileInput, { target: { files: [file] }, }); expect(screen.getByLabelText('附件队列')).not.toBeNull(); expect(screen.getByText('reference.png')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '开启创作' })); expect(await screen.findByLabelText('项目开发画布')).not.toBeNull(); expect(screen.getByText('home-created-game')).not.toBeNull(); expect(screen.getByText('/tmp/home-created-game')).not.toBeNull(); expect(screen.getByText('做素材')).not.toBeNull(); expect(screen.getByText('做一张像素风厨师角色图')).not.toBeNull(); expect(screen.getByText('assets/uploads/reference.png')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('init_local_game_project', { projectPath: '/tmp/home-created-game', projectId: 'local-project-draft', name: 'home-created-game', }); expect(invoke).toHaveBeenCalledWith('upload_local_asset', { projectPath: '/tmp/home-created-game', fileName: 'reference.png', mediaType: 'image/png', bytes: fileBytes, }); const conversationCalls = invoke.mock.calls.filter( ([command]) => command === 'append_local_conversation_message', ); expect(conversationCalls).toHaveLength(2); expect(conversationCalls[0]?.[1]).toMatchObject({ projectPath: '/tmp/home-created-game', agentId: null, message: { role: 'user', agentId: null, content: expect.stringContaining('初始意图:art / 做素材'), }, }); expect(conversationCalls[0]?.[1]).toMatchObject({ message: { content: expect.stringContaining('做一张像素风厨师角色图'), }, }); expect(conversationCalls[0]?.[1]).toMatchObject({ message: { content: expect.stringContaining('附件:reference.png'), }, }); expect(conversationCalls[1]?.[1]).toMatchObject({ projectPath: '/tmp/home-created-game', agentId: null, message: { role: 'assistant', agentId: null, content: expect.stringContaining('assets/uploads/reference.png'), }, }); expect(invoke).not.toHaveBeenCalledWith( 'generate_local_game_draft', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'generate_platform_art_asset', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_agent', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); }); it('rejects launcher project paths with control characters before Tauri calls', () => { const invoke = vi.fn(async () => undefined); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/bad\u0007path' }, }); fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]); expect(screen.getByText('项目目录不能包含控制字符')).not.toBeNull(); expect(invoke).not.toHaveBeenCalled(); }); it('edits runtime config directly from the launcher', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_game_creator_app_config') { return { path: '/home/test/AppData/game-creator.config.json', config: { llm: { apiKey: 'launcher-loaded-secret', baseUrl: 'https://llm.example.test/v1', model: 'gpt-launcher', apiKind: 'legacy', stream: false, requestTimeoutMs: 10, maxRetries: -2, retryBackoffMs: 0, }, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: 'editor-loaded-secret', }, }, }; } if (command === 'write_game_creator_app_config') { return { path: '/home/test/AppData/game-creator.config.json', config: args?.config, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '配置' })); const dialog = await screen.findByRole('dialog', { name: '运行时配置' }); expect(await screen.findByDisplayValue('gpt-launcher')).not.toBeNull(); expect(screen.getByLabelText('LLM 超时 ms')).toHaveProperty( 'value', '1000', ); expect(screen.getByLabelText('LLM 重试次数')).toHaveProperty('value', '0'); expect(screen.getByLabelText('LLM 退避 ms')).toHaveProperty('value', '1'); expect(screen.getByLabelText('LLM API 类型')).toHaveProperty( 'value', 'openai_responses', ); fireEvent.change(screen.getByLabelText('LLM 模型'), { target: { value: 'gpt-launcher-updated' }, }); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', { config: expect.objectContaining({ llm: expect.objectContaining({ model: 'gpt-launcher-updated', apiKind: 'openai_responses', requestTimeoutMs: 1000, maxRetries: 0, retryBackoffMs: 1, }), }), }); }); expect( await screen.findByText( '已保存:/home/test/AppData/game-creator.config.json', ), ).not.toBeNull(); fireEvent.mouseDown(dialog.parentElement as HTMLElement); expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull(); }); it('keeps runtime config open when Escape is pressed in an input', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'read_game_creator_app_config') { return { path: '/home/test/AppData/game-creator.config.json', config: { llm: { apiKey: '', baseUrl: 'https://llm.example.test/v1', model: 'gpt-test', apiKind: 'openai_responses', stream: false, requestTimeoutMs: 180000, maxRetries: 0, retryBackoffMs: 500, }, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '', }, }, }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '配置' })); expect( await screen.findByRole('dialog', { name: '运行时配置' }), ).not.toBeNull(); fireEvent.keyDown(screen.getByLabelText('LLM Base URL'), { key: 'Escape', }); expect(screen.getByRole('dialog', { name: '运行时配置' })).not.toBeNull(); fireEvent.keyDown(window, { key: 'Escape' }); expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull(); }); it('disables runtime config actions while reading config', async () => { let resolveRead: | ((value: { path: string; config: { llm: { apiKey: string; baseUrl: string; model: string; apiKind: string; stream: boolean; requestTimeoutMs: number; maxRetries: number; retryBackoffMs: number; }; editorApi: { baseUrl: string; apiKey: string }; }; }) => void) | undefined; let readCount = 0; const invoke = vi.fn((command: string) => { if (command === 'read_game_creator_app_config') { readCount += 1; return new Promise((resolve) => { resolveRead = resolve as typeof resolveRead; }); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '配置' })); expect( await screen.findByRole('dialog', { name: '运行时配置' }), ).not.toBeNull(); expect(await screen.findByText('正在读取')).not.toBeNull(); expect(screen.getByRole('button', { name: '读取' })).toHaveProperty( 'disabled', true, ); expect(screen.getByRole('button', { name: '恢复默认' })).toHaveProperty( 'disabled', true, ); expect(screen.getByRole('button', { name: '保存' })).toHaveProperty( 'disabled', true, ); fireEvent.click(screen.getByRole('button', { name: '读取' })); fireEvent.click(screen.getByRole('button', { name: '保存' })); expect(readCount).toBe(1); await act(async () => { resolveRead?.({ path: '/home/test/AppData/game-creator.config.json', config: { llm: { apiKey: '', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1', apiKind: 'openai_responses', stream: false, requestTimeoutMs: 180000, maxRetries: 0, retryBackoffMs: 500, }, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '', }, }, }); }); expect(await screen.findByText(/已读取:/)).not.toBeNull(); expect(screen.getByRole('button', { name: '读取' })).toHaveProperty( 'disabled', false, ); }); it('removes and clears recent launcher projects without opening them', () => { const invoke = vi.fn(async () => undefined); window.__TAURI__ = { core: { invoke } }; window.localStorage.setItem( 'genarrative-ai-game-creator.recent-workspaces.v1', JSON.stringify([ '/tmp/recent-one', ' /tmp/recent-one ', ' ', 42, 'relative-game', '/tmp/recent-two', ]), ); renderLauncherAt('/?launcher'); expect(screen.getByLabelText('最近项目')).not.toBeNull(); expect(screen.getByText('暂无最近项目')).not.toBeNull(); expect( screen.queryByRole('button', { name: '移除 /tmp/recent-one' }), ).toBeNull(); expect( screen.queryByRole('button', { name: '显示 /tmp/recent-one' }), ).toBeNull(); expect(screen.queryByRole('button', { name: '刷新' })).toBeNull(); expect(screen.queryByRole('button', { name: '清空' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '项目组' })); expect(screen.getAllByText('/tmp/recent-one')).toHaveLength(1); expect(screen.getByText('/tmp/recent-two')).not.toBeNull(); expect(screen.queryByText('relative-game')).toBeNull(); expect(invoke).not.toHaveBeenCalledWith('inspect_local_project_directory', { projectPath: 'relative-game', }); fireEvent.click( screen.getByRole('button', { name: '移除 /tmp/recent-one' }), ); expect(screen.queryByText('/tmp/recent-one')).toBeNull(); expect(screen.getByText('/tmp/recent-two')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '清空' })); expect(screen.getByText('暂无项目')).not.toBeNull(); expect(window.localStorage.length).toBe(0); }); it('opens a recent launcher project directory in the system file manager', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath: String(args?.projectPath ?? ''), exists: true, isDirectory: true, isGameCreatorProject: true, projectName: '最近的项目', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'open_local_project_directory') { return undefined; } if (command === 'open_game_creator_workspace_window') { return undefined; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; window.localStorage.setItem( 'genarrative-ai-game-creator.recent-workspaces.v1', JSON.stringify(['/tmp/recent-one']), ); renderLauncherAt('/?launcher'); expect(await screen.findByText('最近的项目')).not.toBeNull(); expect( screen.queryByRole('button', { name: '显示 /tmp/recent-one' }), ).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '项目组' })); fireEvent.click( screen.getByRole('button', { name: '显示 /tmp/recent-one' }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('open_local_project_directory', { projectPath: '/tmp/recent-one', }); }); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); expect(screen.getByText('已打开项目目录')).not.toBeNull(); expect(window.localStorage.length).toBe(1); }); it('opens the typed launcher project directory in the system file manager', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'open_local_project_directory') { return undefined; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/typed-game' }, }); const revealButtons = screen.getAllByRole('button', { name: '显示目录' }); fireEvent.click(revealButtons[revealButtons.length - 1]); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('open_local_project_directory', { projectPath: '/tmp/typed-game', }); }); expect(screen.getByText('已打开项目目录')).not.toBeNull(); }); it('rejects invalid typed launcher project directories before opening them', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: 'relative-game' }, }); const revealButtons = screen.getAllByRole('button', { name: '显示目录' }); fireEvent.click(revealButtons[revealButtons.length - 1]); expect(screen.getByText('请提供项目绝对路径')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_local_project_directory', expect.anything(), ); }); it('does not remember a launcher project when project inspection fails', async () => { const invoke = vi.fn( async (command: string) => { if (command === 'inspect_local_project_directory') { throw new Error('inspect failed'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/open-failed-game' }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); expect(await screen.findByText('inspect failed')).not.toBeNull(); expect(window.localStorage.length).toBe(0); }); it('marks missing recent launcher projects and does not reopen them', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { const projectPath = String(args?.projectPath ?? ''); if (projectPath === '/tmp/broken-status') { throw new Error('status failed'); } return { projectPath, exists: projectPath !== '/tmp/missing-game', isDirectory: projectPath !== '/tmp/not-a-folder', isGameCreatorProject: projectPath === '/tmp/ok-game', projectName: projectPath === '/tmp/ok-game' ? '厨房突围' : null, manifestError: projectPath === '/tmp/broken-manifest' ? '解析 manifest 失败' : null, recentRunStatus: projectPath === '/tmp/ok-game' ? 'done' : null, recentRunStopReason: projectPath === '/tmp/ok-game' ? 'preview-running' : null, }; } if (command === 'open_game_creator_workspace_window') { return undefined; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; window.localStorage.setItem( 'genarrative-ai-game-creator.recent-workspaces.v1', JSON.stringify([ '/tmp/missing-game', '/tmp/not-a-folder', '/tmp/plain-folder', '/tmp/broken-manifest', '/tmp/broken-status', '/tmp/ok-game', ]), ); renderLauncherAt('/?launcher'); expect(await screen.findByText('厨房突围')).not.toBeNull(); expect( within(screen.getByLabelText('最近项目')).getByText('厨房突围'), ).not.toBeNull(); expect( within(screen.getByLabelText('最近项目')).queryByText('missing-game'), ).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '项目组' })); expect(screen.getByText('未找到')).not.toBeNull(); expect(screen.getByText('不是文件夹')).not.toBeNull(); expect(screen.getAllByText('未初始化').length).toBeGreaterThan(0); expect(screen.getByText('无法读取')).not.toBeNull(); expect(screen.getByText('检查失败')).not.toBeNull(); expect(screen.getByText('厨房突围')).not.toBeNull(); expect(screen.getByText('run: done · preview-running')).not.toBeNull(); expect( (screen.getByText('missing-game').closest('button') as HTMLButtonElement) .disabled, ).toBe(true); const missingOpenButton = screen .getByText('missing-game') .closest('button') as HTMLButtonElement; const plainFolderOpenButton = screen .getByText('plain-folder') .closest('button') as HTMLButtonElement; const brokenStatusOpenButton = screen .getByText('broken-status') .closest('button') as HTMLButtonElement; const brokenManifestOpenButton = screen .getByText('broken-manifest') .closest('button') as HTMLButtonElement; const missingRevealButton = screen.getByRole('button', { name: '显示 /tmp/missing-game', }) as HTMLButtonElement; const notAFolderRevealButton = screen.getByRole('button', { name: '显示 /tmp/not-a-folder', }) as HTMLButtonElement; const plainFolderRevealButton = screen.getByRole('button', { name: '显示 /tmp/plain-folder', }) as HTMLButtonElement; const brokenManifestRevealButton = screen.getByRole('button', { name: '显示 /tmp/broken-manifest', }) as HTMLButtonElement; const brokenStatusRevealButton = screen.getByRole('button', { name: '显示 /tmp/broken-status', }) as HTMLButtonElement; expect(missingOpenButton.disabled).toBe(true); expect(plainFolderOpenButton.disabled).toBe(true); expect(brokenManifestOpenButton.disabled).toBe(true); expect(brokenStatusOpenButton.disabled).toBe(true); expect(missingRevealButton.disabled).toBe(true); expect(notAFolderRevealButton.disabled).toBe(true); expect(plainFolderRevealButton.disabled).toBe(false); expect(brokenManifestRevealButton.disabled).toBe(false); expect(brokenStatusRevealButton.disabled).toBe(true); fireEvent.click(missingOpenButton); fireEvent.click(plainFolderOpenButton); fireEvent.click(brokenManifestOpenButton); fireEvent.click(brokenStatusOpenButton); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', { projectPath: '/tmp/missing-game' }, ); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', { projectPath: '/tmp/plain-folder' }, ); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', { projectPath: '/tmp/broken-manifest' }, ); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', { projectPath: '/tmp/broken-status' }, ); fireEvent.click(screen.getByText('厨房突围').closest('button')!); await waitFor(() => { expect(screen.getByLabelText('项目开发画布')).not.toBeNull(); }); expect(screen.getByText('done')).not.toBeNull(); expect(screen.getByText('preview-running')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); }); it('refreshes recent launcher project status without changing the list', async () => { let inspectionCount = 0; let finishRefresh: | ((status: { projectPath: string; exists: boolean; isDirectory: boolean; isGameCreatorProject: boolean; projectName: string; recentRunStatus: string; recentRunStopReason: string; }) => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { inspectionCount += 1; const projectPath = String(args?.projectPath ?? ''); if (inspectionCount > 1) { return await new Promise((resolve) => { finishRefresh = resolve; }); } return { projectPath, exists: false, isDirectory: false, isGameCreatorProject: false, projectName: null, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'open_game_creator_workspace_window') { return undefined; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; window.localStorage.setItem( 'genarrative-ai-game-creator.recent-workspaces.v1', JSON.stringify(['/tmp/refreshable-game']), ); renderLauncherAt('/?launcher'); expect(screen.getByLabelText('最近项目')).not.toBeNull(); expect(screen.getByText('暂无最近项目')).not.toBeNull(); expect(screen.queryByRole('button', { name: '刷新' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '项目组' })); expect(await screen.findByText('未找到')).not.toBeNull(); expect( ( screen .getByText('refreshable-game') .closest('button') as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen .getByText('refreshable-game') .closest('button') as HTMLButtonElement ).disabled, ).toBe(true); fireEvent.click(screen.getByRole('button', { name: '刷新' })); expect( (await screen.findByRole('button', { name: '刷新中', })) as HTMLButtonElement, ).toHaveProperty('disabled', true); expect(screen.getByText('检查中')).not.toBeNull(); expect( ( screen .getByText('refreshable-game') .closest('button') as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '显示 /tmp/refreshable-game', }) as HTMLButtonElement ).disabled, ).toBe(true); await act(async () => { finishRefresh?.({ projectPath: '/tmp/refreshable-game', exists: true, isDirectory: true, isGameCreatorProject: true, projectName: '刷新后的项目', recentRunStatus: 'done', recentRunStopReason: 'preview-running', }); }); expect(await screen.findByText('刷新后的项目')).not.toBeNull(); expect(screen.getByText('run: done · preview-running')).not.toBeNull(); expect( (screen.getByText('刷新后的项目').closest('button') as HTMLButtonElement) .disabled, ).toBe(false); expect(window.localStorage.length).toBe(1); }); it('does not restore a removed recent launcher project after a slow refresh', async () => { let finishRefresh: | ((status: { projectPath: string; exists: boolean; isDirectory: boolean; isGameCreatorProject: boolean; projectName: string; recentRunStatus: string; recentRunStopReason: string; }) => void) | null = null; const invoke = vi.fn(async (command: string) => { if (command === 'inspect_local_project_directory') { return await new Promise((resolve) => { finishRefresh = resolve; }); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; window.localStorage.setItem( 'genarrative-ai-game-creator.recent-workspaces.v1', JSON.stringify(['/tmp/slow-refresh-game']), ); renderLauncherAt('/?launcher'); expect(screen.getByLabelText('最近项目')).not.toBeNull(); expect(screen.getByText('暂无最近项目')).not.toBeNull(); expect( screen.queryByRole('button', { name: '移除 /tmp/slow-refresh-game' }), ).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '项目组' })); expect(await screen.findByText('检查中')).not.toBeNull(); fireEvent.click( screen.getByRole('button', { name: '移除 /tmp/slow-refresh-game' }), ); expect(screen.queryByText('/tmp/slow-refresh-game')).toBeNull(); await act(async () => { finishRefresh?.({ projectPath: '/tmp/slow-refresh-game', exists: true, isDirectory: true, isGameCreatorProject: true, projectName: '慢速刷新旧项目', recentRunStatus: 'done', recentRunStopReason: 'late', }); }); expect(screen.queryByText('/tmp/slow-refresh-game')).toBeNull(); expect(screen.queryByText('慢速刷新旧项目')).toBeNull(); expect(screen.queryByText('run: done · late')).toBeNull(); expect(screen.getByText('暂无项目')).not.toBeNull(); }); it('keeps the typed project path when native directory picking is cancelled', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'pick_local_project_directory') { return null; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/typed-game' }, }); fireEvent.click(screen.getByRole('button', { name: '选择' })); expect(await screen.findByText('已取消')).not.toBeNull(); expect(screen.getByDisplayValue('/tmp/typed-game')).not.toBeNull(); }); it('warns when creating in a picked non-empty folder', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'pick_local_project_directory') { return '/tmp/picked-non-empty-game'; } if (command === 'is_local_project_directory_non_empty') { expect(args).toEqual({ projectPath: '/tmp/picked-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 } }; renderLauncherProjectsAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '选择' })); expect(await screen.findByText('已选择项目目录')).not.toBeNull(); expect( screen.getByDisplayValue('/tmp/picked-non-empty-game'), ).not.toBeNull(); fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]); expect( await screen.findByRole('dialog', { name: '文件夹不是空的' }), ).not.toBeNull(); expect(screen.getByText('/tmp/picked-non-empty-game')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'init_local_game_project', expect.anything(), ); }); it('warns before creating a project in a non-empty folder', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'is_local_project_directory_non_empty') { expect(args).toEqual({ projectPath: '/tmp/non-empty-game', }); return true; } if (command === 'init_local_game_project') { return { projectPath: String(args?.projectPath ?? ''), manifestPath: `${String(args?.projectPath ?? '')}/.agent/manifest.json`, manifest: createGameCreationAppManifest( 'local-project-draft', 'non-empty-game', ), }; } throw new Error(`unexpected invoke ${command}`); }, ); const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/non-empty-game' }, }); fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]); expect( await screen.findByRole('dialog', { name: '文件夹不是空的' }), ).not.toBeNull(); expect(screen.getByText('/tmp/non-empty-game')).not.toBeNull(); fireEvent.keyDown(window, { key: 'Escape' }); expect(await screen.findByText('已取消')).not.toBeNull(); expect(confirm).not.toHaveBeenCalled(); expect(screen.queryByRole('dialog', { name: '文件夹不是空的' })).toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); expect(window.localStorage.length).toBe(0); }); it('creates in a non-empty folder after the user confirms the warning', async () => { 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') { return { projectPath: String(args?.projectPath ?? ''), manifestPath: `${String(args?.projectPath ?? '')}/.agent/manifest.json`, manifest: createGameCreationAppManifest( 'local-project-draft', 'non-empty-game', ), }; } throw new Error(`unexpected invoke ${command}`); }, ); const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/non-empty-game' }, }); fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]); expect( await screen.findByRole('dialog', { name: '文件夹不是空的' }), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '继续新建' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('init_local_game_project', { projectPath: '/tmp/non-empty-game', projectId: 'local-project-draft', name: 'non-empty-game', }); expect(screen.getByLabelText('项目开发画布')).not.toBeNull(); }); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); expect(confirm).not.toHaveBeenCalled(); expect(window.localStorage.length).toBe(1); }); it('uses the selected folder name as the default project name', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'is_local_project_directory_non_empty') { return false; } if (command === 'init_local_game_project') { return { projectPath: String(args?.projectPath ?? ''), manifestPath: `${String(args?.projectPath ?? '')}/.agent/manifest.json`, manifest: createGameCreationAppManifest( 'local-project-draft', String(args?.name ?? ''), ), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/folder-named-game/' }, }); fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('init_local_game_project', { projectPath: '/tmp/folder-named-game/', projectId: 'local-project-draft', name: 'folder-named-game', }); }); }); it('keeps the launcher open when project creation fails', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'is_local_project_directory_non_empty') { return false; } if (command === 'init_local_game_project') { throw new Error('初始化失败'); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/new-game' }, }); fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]); expect(await screen.findByText('初始化失败')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_workspace_window', expect.anything(), ); expect(window.localStorage.length).toBe(0); }); 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', ); 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 === '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('已打开:/tmp/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(); submitChat('/open-project'); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_project_directory', ), ).toHaveLength(2); }); expect( screen.getAllByText('已打开项目目录。').length, ).toBeGreaterThanOrEqual(2); }); it('fills an asset registration draft from recent project files without registering immediately', 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 === 'read_local_project_file') { throw new Error('missing trace'); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: 'assets/uploads/hero.png', kind: 'file', size: 4, modifiedAt: 1700000001, }, ], }; } if (command === 'register_local_asset') { throw new Error('should only fill the chat draft'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; await act(async () => { renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); }); await screen.findByText('已打开:/tmp/authorized-game'); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: '文件' })); expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); fireEvent.click( within(screen.getByLabelText('最近项目文件')).getByRole('button', { name: '登记资产 assets/uploads/hero.png', }), ); await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('创作想法')), ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/asset-register assets/uploads/hero.png image image/png', ); expect( screen.queryByText('asset.register · assets/uploads/hero.png'), ).toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'register_local_asset', expect.anything(), ); }); it('runs preview shortcuts from the main project window', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); manifest.goal = '做一个厨房弹幕游戏'; manifest.assets.push({ id: 'asset-hero', kind: 'uploaded', mediaType: 'text/plain', localPath: 'assets/uploads/hero.txt', source: { kind: 'uploaded' }, }); manifest.assets.push({ id: 'asset-canvas', kind: 'canvas', mediaType: 'image/png', localPath: 'assets/canvas-sync/tiles.png', source: { kind: 'canvas', canvasProjectId: 'canvas-1' }, }); manifest.commandRuns = [ { commandId: 'game.static_smoke', status: 'completed', output: 'static smoke passed', logPath: '.agent/logs/command.log', updatedAt: 1700000003, }, ]; const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-main-shortcut-trace', commandId: 'game.generate_draft', status: 'passed', lifecycleStatus: 'done', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [ { pass: 1, agent: 'Generator', phase: 'generate', status: 'completed', inputPaths: ['.agent/spec.md'], outputPaths: ['.agent/passes/pass-1/draft.json'], summary: '生成可运行草案', toolCalls: [ { toolId: 'llm.chat.generator', status: 'ok', summary: 'Generator 生成草案', }, ], }, { pass: 1, agent: 'Evaluator', phase: 'evaluation', status: 'passed', inputPaths: ['.agent/passes/pass-1/draft.json', '.agent/findings.md'], outputPaths: ['.agent/findings.md'], summary: '质量评审通过', toolCalls: [ { toolId: 'agent.evaluate', status: 'ok', summary: 'Evaluator 质量评审', }, ], }, ], artifacts: [ { path: 'exports/README.md', sizeBytes: 128, checksum: 'fnv1a64:exports', }, { path: '.agent/passes/pass-1/agenda.md', sizeBytes: 96, checksum: 'fnv1a64:agenda', }, { path: '.agent/passes/pass-1/groups/art/asset.md', sizeBytes: 192, checksum: 'fnv1a64:art-asset', }, ], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: ['art-asset-plan'], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', error: null, updatedAt: 1, }; 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 === '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 === 'open_local_game_preview') { return { status: 'running', url: 'http://127.0.0.1:3210/', port: 3210, root: String(args?.projectPath ?? ''), }; } if (command === 'get_local_game_preview_status') { return { status: 'running', url: 'http://127.0.0.1:3210/', port: 3210, root: String(args?.projectPath ?? ''), }; } if (command === 'stop_local_game_preview') { return { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'get_game_creation_agent_capabilities') { return [ { id: 'native-only-capability', area: 'agent-runtime', title: 'Native Runtime 能力', }, ]; } if (command === 'get_limited_local_commands') { return [{ id: 'game.static_smoke', title: '静态入口自检' }]; } if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-main', apiKind: 'openai_responses', stream: false, error: null, agents: [ { agentId: 'planner', label: 'Planner', configured: true, apiKeyPresent: true, baseUrl: 'https://planner.example.test/v1', model: 'planner-model', apiKind: 'anthropic', stream: true, error: null, }, { agentId: 'art-asset-plan', label: '美术组 / Asset', configured: true, apiKeyPresent: true, baseUrl: 'https://art.example.test/v1', model: 'art-model', apiKind: 'openai_chat', stream: true, error: null, }, { agentId: 'audio-asset-plan', label: '音乐组 / SFX', configured: false, apiKeyPresent: false, baseUrl: 'https://audio.example.test/v1', model: 'audio-model', apiKind: 'openai_chat', stream: false, error: 'LLM 未配置:请在 agentLlm.audio-asset-plan.apiKey 中设置 API Key', }, ], }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: 'game/index.html', kind: 'file', size: 256, modifiedAt: 1700000001, }, { path: 'assets', kind: 'directory', size: 0, modifiedAt: 1700000000, }, { path: '.agent/checkpoints/checkpoint-main/manifest.json', kind: 'file', size: 96, modifiedAt: 1700000002, }, ], }; } if (command === 'build_local_project_index') { return { projectPath: String(args?.projectPath ?? ''), indexPath: '.agent/project.index.json', fileCount: 2, totalBytes: 384, files: [ { path: 'game/index.html', size: 256, checksum: 'fnv1a64:game' }, { path: 'assets/uploads/hero.png', size: 128, checksum: 'fnv1a64:hero', }, ], }; } if (command === 'read_local_game_memory') { const scope = String(args?.scope ?? 'long'); if (scope === 'short') { return { scope, path: 'memory/session.md', content: '# 短期记忆\n- 本轮偏动作反馈\n', exists: true, }; } if (scope === 'blackboard') { return { scope, path: 'memory/blackboard.md', content: '# 项目黑板\n- 跨 agent 共享约束\n', exists: true, }; } return { scope, path: 'memory/project.md', content: '# 项目长期记忆\n- 保留厨房主题\n', exists: true, }; } if (command === 'read_local_agent_memory') { return { taskId: String(args?.taskId ?? ''), path: 'memory/agents/art/asset.md', content: '# 美术私有记忆\n- 使用厨房像素素材\n', exists: true, }; } if (command === 'read_local_project_file') { if (args?.relativePath === 'game/index.html') { return { path: 'game/index.html', absolutePath: `${String(args?.projectPath ?? '')}/game/index.html`, content: '', }; } if (args?.relativePath === 'assets/uploads/hero.txt') { return { path: 'assets/uploads/hero.txt', absolutePath: `${String(args?.projectPath ?? '')}/assets/uploads/hero.txt`, content: 'hero asset', }; } if ( args?.relativePath === '.agent/checkpoints/checkpoint-main/manifest.json' ) { return { path: '.agent/checkpoints/checkpoint-main/manifest.json', absolutePath: `${String(args?.projectPath ?? '')}/.agent/checkpoints/checkpoint-main/manifest.json`, content: JSON.stringify({ checkpointId: 'checkpoint-main', fileCount: 3, totalBytes: 256, createdAt: 1700000002, }), }; } return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String(args?.relativePath ?? '')}`, content: JSON.stringify(trace), }; } if (command === 'create_local_project_checkpoint') { return { checkpointId: 'checkpoint-main', checkpointPath: '.agent/checkpoints/checkpoint-main', fileCount: 3, totalBytes: 256, }; } if (command === 'export_local_project_package') { return { packagePath: `${String(args?.projectPath ?? '')}/exports/playtest-package-unit.zip`, packageRelativePath: 'exports/playtest-package-unit.zip', fileCount: 4, totalBytes: 512, }; } if (command === 'list_local_project_export_packages') { return { projectPath: String(args?.projectPath ?? ''), packages: [ { packagePath: `${String(args?.projectPath ?? '')}/exports/playtest-package-002.zip`, packageRelativePath: 'exports/playtest-package-002.zip', totalBytes: 2048, modifiedAt: 1700000002000, }, { 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('已打开:/tmp/authorized-game'); expect(screen.getByText('未命名游戏原型')).not.toBeNull(); expect(screen.getByText('/tmp/authorized-game')).not.toBeNull(); expect(screen.getByText('preview: 未启动')).not.toBeNull(); const projectSummary = screen.getByLabelText('项目摘要'); expect(projectSummary.textContent).toContain('任务:已完成 0/'); expect(projectSummary.textContent).toContain('ready'); expect(projectSummary.textContent).toContain( '资产:2 个 · 上传 1 / 画板 1', ); expect(projectSummary.textContent).toContain( '最近命令:game.static_smoke 完成', ); expect(invoke).not.toHaveBeenCalledWith( 'get_local_game_manifest', expect.anything(), ); expect(screen.queryByLabelText('开发环境')).toBeNull(); expect(screen.queryByText('编排 Trace')).toBeNull(); expect(screen.queryByText(/LLM:已配置 · art-model/)).toBeNull(); fireEvent.click(screen.getByRole('button', { name: 'LLM状态' })); expect(await screen.findByText(/LLM 已配置:gpt-main/)).not.toBeNull(); const agentStatusPane = screen.getByLabelText('Agent 状态'); expect( within(agentStatusPane).getByText( 'LLM:已配置 · art-model · openai_chat · 流式开 · Key已读', ), ).not.toBeNull(); expect( within(agentStatusPane).getByText( 'LLM:未就绪 · audio-model · openai_chat · 流式关 · Key未读', ), ).not.toBeNull(); expect(screen.queryByText(/planner-secret/)).toBeNull(); expect(screen.queryByText(/art-secret/)).toBeNull(); submitChat('/agents'); expect( await screen.findByText( /Agent 状态:[\s\S]*美术组 \/ Asset · 规划美术资产 · 待处理[\s\S]*LLM:已配置 · art-model/, ), ).not.toBeNull(); submitChat('/agent-conversations'); expect( await screen.findByText( /Agent 对话读取命令:[\s\S]*美术组 \/ Asset · 规划美术资产:\/read \.agent\/conversations\/agents\/art-asset\.jsonl/, ), ).not.toBeNull(); fireEvent.click( screen.getByRole('button', { name: '读取拆解创作方向对话' }), ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/conversations/agents/design-director.jsonl', ); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.objectContaining({ relativePath: '.agent/conversations/agents/design-director.jsonl', }), ); submitChat('/agent-memories'); expect( await screen.findByText( /Agent 私有记忆读取命令:[\s\S]*美术组 \/ Asset · 规划美术资产:\/read memory\/agents\/art\/asset\.md/, ), ).not.toBeNull(); fireEvent.click( screen.getByRole('button', { name: '读取拆解创作方向记忆' }), ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read memory/agents/design/director.md', ); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.objectContaining({ relativePath: 'memory/agents/design/director.md', }), ); fireEvent.click( within(agentStatusPane).getByRole('button', { name: /规划美术资产/, }), ); const agentDialog = await screen.findByRole('dialog', { name: 'Agent 对话', }); expect( within(agentDialog).getByText( 'LLM:已配置,art-model @ https://art.example.test/v1,openai_chat,流式 开启,API Key 已读取', ), ).not.toBeNull(); fireEvent.click(within(agentDialog).getByRole('button', { name: '关闭' })); fireEvent.click(screen.getByRole('button', { name: '能力' })); expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '项目状态' })); expect(await screen.findByText(/项目:未命名游戏原型/)).not.toBeNull(); expect(screen.getByText(/目录:\/tmp\/authorized-game/)).not.toBeNull(); const manifestReadCountBeforeBrief = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; submitChat('/brief'); expect(await screen.findByText(/项目简报:/)).not.toBeNull(); expect(screen.getByText(/任务:完成 0\/16 · ready 1/)).not.toBeNull(); expect( screen.getByText(/最近 Run:run-main-shortcut-trace/), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '查看下一步' })); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/next'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeBrief); const manifestReadCountBeforeGoal = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const fileReadCountBeforeGoal = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const runLocalCountBeforeGoal = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; submitChat('/goal'); expect(await screen.findByText(/创作目标:/)).not.toBeNull(); const goalMessages = screen.getAllByText(/创作目标:/); const goalMessage = goalMessages[goalMessages.length - 1]; expect(goalMessage.textContent).toContain('Manifest:做一个厨房弹幕游戏'); expect(goalMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(goalMessage.textContent).toContain('Run 目标:做一个厨房弹幕游戏'); expect(goalMessage.textContent).toContain('任务图目标:做一个厨房弹幕游戏'); expect(goalMessage.textContent).toContain('上下文:/context'); expect(goalMessage.textContent).toContain('建议:/agent-resume 细化目标:'); fireEvent.click(screen.getByRole('button', { name: '补充目标' })); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 细化目标:', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeGoal); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeGoal); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeGoal); const commandCountsBeforeSpec = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/spec'); const specMessages = await screen.findAllByText(/创作规格包:/); const specMessage = specMessages[specMessages.length - 1]; expect(specMessage.textContent).toContain('项目:未命名游戏原型'); expect(specMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(specMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(specMessage.textContent).toContain( 'Planner 规格:.agent/spec.md · 已出现在最近 run', ); expect(specMessage.textContent).toContain( '玩法设计:game/game_design.md · 任务声明', ); expect(specMessage.textContent).toContain( '发布说明:exports/README.md · 已出现在最近 run', ); expect(specMessage.textContent).toContain( '边界:只整理规格产物状态;不读取规格文件;不启动预览;不写项目', ); expect(specMessage.textContent).toContain('建议:/read .agent/spec.md'); const specMessageList = document.querySelector('.message-list'); expect(specMessageList).not.toBeNull(); const specDraftButtons = within( specMessageList as HTMLElement, ).getAllByRole('button', { name: '读取规格' }); fireEvent.click(specDraftButtons[specDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/spec.md', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeSpec.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeSpec.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeSpec.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeSpec.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeSpec.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeSpec.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeSpec.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeSpec.exportList); const commandCountsBeforeMvp = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/mvp'); expect(await screen.findByText(/MVP 范围:/)).not.toBeNull(); const mvpMessages = screen.getAllByText(/MVP 范围:/); const mvpMessage = mvpMessages[mvpMessages.length - 1]; expect(mvpMessage.textContent).toContain('项目:未命名游戏原型'); expect(mvpMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(mvpMessage.textContent).toContain( 'MVP 内:可运行 Web 原型;基础输入 / 胜负 / 重开;本地预览;本地试玩包', ); expect(mvpMessage.textContent).toContain( '当前状态:最近 run run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(mvpMessage.textContent).toContain( '任务:完成 0/16 · ready 1 · 失败 0', ); expect(mvpMessage.textContent).toContain('预览:未启动'); expect(mvpMessage.textContent).toContain('资产:2 个'); expect(mvpMessage.textContent).toContain('试玩包:待导出'); expect(mvpMessage.textContent).toContain( '先不做:云同步;Unity/Godot;插件市场;任意 shell;深度资产精修', ); expect(mvpMessage.textContent).toContain('建议:/run'); const mvpMessageList = document.querySelector('.message-list'); expect(mvpMessageList).not.toBeNull(); const mvpDraftButtons = within(mvpMessageList as HTMLElement).getAllByRole( 'button', { name: '启动预览' }, ); fireEvent.click(mvpDraftButtons[mvpDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeMvp.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeMvp.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeMvp.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeMvp.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeMvp.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeMvp.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeMvp.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeMvp.exportList); const commandCountsBeforePitch = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/pitch'); const pitchMessages = await screen.findAllByText(/试玩定位:/); const pitchMessage = pitchMessages[pitchMessages.length - 1]; expect(pitchMessage.textContent).toContain('项目:未命名游戏原型'); expect(pitchMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); expect(pitchMessage.textContent).toContain( '核心乐趣:快速验证目标、操作反馈、胜负结果和重开节奏', ); expect(pitchMessage.textContent).toContain( '当前可演示:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(pitchMessage.textContent).toContain( '讲给测试者:先说明目标,再说明操作,然后看 30 秒内是否能理解胜负和重开', ); expect(pitchMessage.textContent).toContain( '不承诺:云发布;深度美术精修;账号体系;排行榜;长期运营包装', ); expect(pitchMessage.textContent).toContain( '参考:/mvp;/rules;/playtest;/listing', ); expect(pitchMessage.textContent).toContain('建议:/run'); const pitchMessageList = document.querySelector('.message-list'); expect(pitchMessageList).not.toBeNull(); const pitchDraftButtons = within( pitchMessageList as HTMLElement, ).getAllByRole('button', { name: '启动预览' }); fireEvent.click(pitchDraftButtons[pitchDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforePitch.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforePitch.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforePitch.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforePitch.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforePitch.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforePitch.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforePitch.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforePitch.exportList); const commandCountsBeforeDemo = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/demo'); const demoMessages = await screen.findAllByText(/试玩讲解稿:/); const demoMessage = demoMessages[demoMessages.length - 1]; expect(demoMessage.textContent).toContain('项目:未命名游戏原型'); expect(demoMessage.textContent).toContain( '30 秒开场:这是《未命名游戏原型》,目标是做一个厨房弹幕游戏', ); expect(demoMessage.textContent).toContain( '讲解顺序:目标 -> 操作 -> 反馈 -> 胜负 -> 重开', ); expect(demoMessage.textContent).toContain( '口播稿:先看目标提示,尝试移动/点击完成核心动作;看到得分、受击或状态反馈后,继续到胜利或失败;结束后确认能否一键重开', ); expect(demoMessage.textContent).toContain( '当前演示状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(demoMessage.textContent).toContain('最近试玩证据:暂无'); expect(demoMessage.textContent).toContain( '收反馈:操作是否明白;节奏是否太快;胜负是否清楚;视觉 / 音效是否帮助理解', ); expect(demoMessage.textContent).toContain( '边界:只准备试玩讲解;不启动预览;不导出试玩包;不发布作品', ); expect(demoMessage.textContent).toContain( '参考:/rules;/test-plan;/feedback;/share', ); expect(demoMessage.textContent).toContain('建议:/run'); const demoMessageList = document.querySelector('.message-list'); expect(demoMessageList).not.toBeNull(); const demoDraftButtons = within( demoMessageList as HTMLElement, ).getAllByRole('button', { name: '启动预览' }); fireEvent.click(demoDraftButtons[demoDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeDemo.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeDemo.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeDemo.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeDemo.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeDemo.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeDemo.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeDemo.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeDemo.exportList); const commandCountsBeforeControls = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, }; submitChat('/rules'); expect(await screen.findByText(/玩法操作:/)).not.toBeNull(); const controlMessages = screen.getAllByText(/玩法操作:/); const controlMessage = controlMessages[controlMessages.length - 1]; expect(controlMessage.textContent).toContain('项目:未命名游戏原型'); expect(controlMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(controlMessage.textContent).toContain( '核心口径:目标;操作;胜负;重开;本地预览', ); expect(controlMessage.textContent).toContain( '规则来源:game/game_design.md · 未见 trace 产物', ); expect(controlMessage.textContent).toContain( '原型入口:game/index.html · 未见 trace 产物', ); expect(controlMessage.textContent).toContain( 'design-foundation:策划组 / Gameplay 确定玩法规格 · 待处理', ); expect(controlMessage.textContent).toContain( 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', ); expect(controlMessage.textContent).toContain( 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(controlMessage.textContent).toContain( '最近程序/试玩步骤:Generator #1 · completed · 生成可运行草案', ); expect(controlMessage.textContent).toContain( '相关命令:/mvp;/playtest;/feedback', ); expect(controlMessage.textContent).toContain( '建议:/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式', ); const controlMessageList = document.querySelector('.message-list'); expect(controlMessageList).not.toBeNull(); const controlDraftButtons = within( controlMessageList as HTMLElement, ).getAllByRole('button', { name: '补充操作说明' }); fireEvent.click(controlDraftButtons[controlDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeControls.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeControls.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeControls.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeControls.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeControls.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeControls.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeControls.exportPackage); const commandCountsBeforeTutorial = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/tutorial'); const tutorialMessages = await screen.findAllByText(/新手引导:/); const tutorialMessage = tutorialMessages[tutorialMessages.length - 1]; expect(tutorialMessage.textContent).toContain('项目:未命名游戏原型'); expect(tutorialMessage.textContent).toContain( '首屏目标:做一个厨房弹幕游戏', ); expect(tutorialMessage.textContent).toContain( '首局 30 秒:看到目标;尝试操作;收到反馈;理解失败/胜利;能重开', ); expect(tutorialMessage.textContent).toContain( '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(tutorialMessage.textContent).toContain( '试玩任务:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(tutorialMessage.textContent).toContain( '最近引导证据:Generator #1 · completed · 生成可运行草案', ); expect(tutorialMessage.textContent).toContain( '需要补齐:首屏目标提示;操作提示;碰撞/得分反馈;失败或胜利提示;重开按钮', ); expect(tutorialMessage.textContent).toContain( '参考:/rules;/playtest;/feedback;/pitch', ); expect(tutorialMessage.textContent).toContain( '建议:/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示', ); const tutorialMessageList = document.querySelector('.message-list'); expect(tutorialMessageList).not.toBeNull(); const tutorialDraftButtons = within( tutorialMessageList as HTMLElement, ).getAllByRole('button', { name: '补充新手引导' }); fireEvent.click(tutorialDraftButtons[tutorialDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeTutorial.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeTutorial.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeTutorial.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeTutorial.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeTutorial.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeTutorial.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeTutorial.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeTutorial.exportList); const commandCountsBeforeMobile = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/mobile'); const mobileMessages = await screen.findAllByText(/移动试玩:/); const mobileMessage = mobileMessages[mobileMessages.length - 1]; expect(mobileMessage.textContent).toContain('项目:未命名游戏原型'); expect(mobileMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(mobileMessage.textContent).toContain( '输入方式:键盘 / 触屏都应能完成核心循环', ); expect(mobileMessage.textContent).toContain( '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(mobileMessage.textContent).toContain( '移动检查:触屏操作;响应式画布;横竖屏提示;按钮尺寸;失败/胜利重开', ); expect(mobileMessage.textContent).toContain( 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', ); expect(mobileMessage.textContent).toContain( 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(mobileMessage.textContent).toContain( '最近移动相关步骤:Generator #1 · completed · 生成可运行草案', ); expect(mobileMessage.textContent).toContain( '参考:/rules;/tutorial;/playtest;/feedback', ); expect(mobileMessage.textContent).toContain( '建议:/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮', ); const mobileMessageList = document.querySelector('.message-list'); expect(mobileMessageList).not.toBeNull(); const mobileDraftButtons = within( mobileMessageList as HTMLElement, ).getAllByRole('button', { name: '补充移动试玩' }); fireEvent.click(mobileDraftButtons[mobileDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeMobile.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeMobile.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeMobile.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeMobile.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeMobile.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeMobile.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeMobile.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeMobile.exportList); const commandCountsBeforeCompatibility = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/compatibility'); const compatibilityMessages = await screen.findAllByText(/兼容性说明:/); const compatibilityMessage = compatibilityMessages[compatibilityMessages.length - 1]; expect(compatibilityMessage.textContent).toContain('项目:未命名游戏原型'); expect(compatibilityMessage.textContent).toContain( '目标:做一个厨房弹幕游戏', ); expect(compatibilityMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(compatibilityMessage.textContent).toContain( '自检:game.static_smoke 已通过', ); expect(compatibilityMessage.textContent).toContain( '输入兼容:键盘优先;触屏按 /mobile 复查', ); expect(compatibilityMessage.textContent).toContain( '推荐环境:桌面 Chrome / Edge 最新版;本机 127.0.0.1 预览;移动浏览器只做早期体验', ); expect(compatibilityMessage.textContent).toContain( '不承诺:旧浏览器、低端设备、离线模式、云存档、账号同步、手柄或多端数据一致', ); expect(compatibilityMessage.textContent).toContain( '反馈口径:设备 / 浏览器 / 输入方式 / 截图或录屏;问题记录走 /bug-report', ); expect(compatibilityMessage.textContent).toContain( '参考:/mobile;/accessibility;/performance;/known-issues', ); expect(compatibilityMessage.textContent).toContain( '边界:只准备兼容性说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', ); expect(compatibilityMessage.textContent).toContain('建议:/run'); const compatibilityMessageList = document.querySelector('.message-list'); expect(compatibilityMessageList).not.toBeNull(); const compatibilityDraftButtons = within( compatibilityMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click( compatibilityDraftButtons[compatibilityDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeCompatibility.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeCompatibility.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeCompatibility.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeCompatibility.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeCompatibility.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeCompatibility.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeCompatibility.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeCompatibility.exportList); expect( invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ), ).toHaveLength(commandCountsBeforeCompatibility.controlAgentRun); const commandCountsBeforeAccessibility = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/accessibility'); const accessibilityMessages = await screen.findAllByText(/可读性与无障碍:/); const accessibilityMessage = accessibilityMessages[accessibilityMessages.length - 1]; expect(accessibilityMessage.textContent).toContain('项目:未命名游戏原型'); expect(accessibilityMessage.textContent).toContain( '目标:做一个厨房弹幕游戏', ); expect(accessibilityMessage.textContent).toContain( '检查范围:文字可读;颜色对比;按钮/状态命名;键盘等价操作;可见焦点;非颜色唯一反馈;静音可玩', ); expect(accessibilityMessage.textContent).toContain( '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(accessibilityMessage.textContent).toContain( '补齐项:文字对比;清晰按钮标签;键盘等价操作;非颜色唯一反馈;静音可玩', ); expect(accessibilityMessage.textContent).toContain( 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', ); expect(accessibilityMessage.textContent).toContain( 'quality-review:程序组 / Review 执行质量评审 · 待处理', ); expect(accessibilityMessage.textContent).toContain( 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', ); expect(accessibilityMessage.textContent).toContain( 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(accessibilityMessage.textContent).toContain( '最近无障碍相关步骤:Evaluator #1 · passed · 质量评审通过', ); expect(accessibilityMessage.textContent).toContain( '参考:/rules;/mobile;/tutorial;/qa;/playtest', ); expect(accessibilityMessage.textContent).toContain( '建议:/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩', ); const accessibilityMessageList = document.querySelector('.message-list'); expect(accessibilityMessageList).not.toBeNull(); const accessibilityDraftButtons = within( accessibilityMessageList as HTMLElement, ).getAllByRole('button', { name: '补充无障碍' }); fireEvent.click( accessibilityDraftButtons[accessibilityDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeAccessibility.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeAccessibility.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeAccessibility.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeAccessibility.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeAccessibility.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeAccessibility.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeAccessibility.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeAccessibility.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeAccessibility.controlAgentRun); const commandCountsBeforeLocalization = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/localization'); const localizationMessages = await screen.findAllByText(/本地化与文案:/); const localizationMessage = localizationMessages[localizationMessages.length - 1]; expect(localizationMessage.textContent).toContain('项目:未命名游戏原型'); expect(localizationMessage.textContent).toContain( '目标:做一个厨房弹幕游戏', ); expect(localizationMessage.textContent).toContain( '默认语言:简体中文;首版不承诺多语言', ); expect(localizationMessage.textContent).toContain( '文案范围:标题;目标提示;操作按钮;状态反馈;失败/胜利;重开;发布简介', ); expect(localizationMessage.textContent).toContain( '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;发布说明 已生成', ); expect(localizationMessage.textContent).toContain( 'design-foundation:策划组 / Gameplay 确定玩法规格 · 待处理', ); expect(localizationMessage.textContent).toContain( 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', ); expect(localizationMessage.textContent).toContain( 'quality-review:程序组 / Review 执行质量评审 · 待处理', ); expect(localizationMessage.textContent).toContain( 'publish-package:运营组 / Publish 整理发布包装 · 待处理', ); expect(localizationMessage.textContent).toContain( '检查口径:短句优先;动词一致;玩家术语统一;错误提示可复现;UI 文案避免开发解释', ); expect(localizationMessage.textContent).toContain( '暂不做:英日韩等多语言包;自动翻译;地区化素材;语音本地化;商店长文案 A/B', ); expect(localizationMessage.textContent).toContain( '最近文案相关步骤:Evaluator #1 · passed · 质量评审通过', ); expect(localizationMessage.textContent).toContain( '参考:/rules;/tutorial;/listing;/faq;/known-issues', ); expect(localizationMessage.textContent).toContain( '边界:只整理本地化与文案检查;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', ); expect(localizationMessage.textContent).toContain( '建议:/read exports/README.md', ); const localizationMessageList = document.querySelector('.message-list'); expect(localizationMessageList).not.toBeNull(); const localizationDraftButtons = within( localizationMessageList as HTMLElement, ).getAllByRole('button', { name: '读发布说明' }); fireEvent.click( localizationDraftButtons[localizationDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read exports/README.md', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeLocalization.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeLocalization.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeLocalization.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeLocalization.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeLocalization.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeLocalization.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeLocalization.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeLocalization.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeLocalization.controlAgentRun); const commandCountsBeforePerformance = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/performance'); const performanceMessages = await screen.findAllByText(/性能与加载:/); const performanceMessage = performanceMessages[performanceMessages.length - 1]; expect(performanceMessage.textContent).toContain('项目:未命名游戏原型'); expect(performanceMessage.textContent).toContain( '目标:做一个厨房弹幕游戏', ); expect(performanceMessage.textContent).toContain( '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;入口 未见 trace 产物;产物 3 个 / 416B;资产 2 个', ); expect(performanceMessage.textContent).toContain( '检查范围:入口 HTML 自包含;首屏不空白;素材体积;主循环稳定;无远程依赖;预览启动', ); expect(performanceMessage.textContent).toContain( 'exports/README.md · 128B · fnv1a64:exports', ); expect(performanceMessage.textContent).toContain( '.agent/passes/pass-1/agenda.md · 96B · fnv1a64:agenda', ); expect(performanceMessage.textContent).toContain( 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', ); expect(performanceMessage.textContent).toContain( 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', ); expect(performanceMessage.textContent).toContain( 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(performanceMessage.textContent).toContain( '最近性能相关步骤:Generator #1 · completed · 生成可运行草案', ); expect(performanceMessage.textContent).toContain( '参考:/run-artifacts;/playtest;/qa;/export', ); expect(performanceMessage.textContent).toContain('建议:/run-artifacts'); const performanceMessageList = document.querySelector('.message-list'); expect(performanceMessageList).not.toBeNull(); const performanceDraftButtons = within( performanceMessageList as HTMLElement, ).getAllByRole('button', { name: '列出 Run 产物' }); fireEvent.click( performanceDraftButtons[performanceDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/run-artifacts', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforePerformance.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforePerformance.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforePerformance.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforePerformance.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforePerformance.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforePerformance.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforePerformance.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforePerformance.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforePerformance.controlAgentRun); const commandCountsBeforePolish = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/polish'); const polishMessages = await screen.findAllByText(/试玩前打磨:/); const polishMessage = polishMessages[polishMessages.length - 1]; expect(polishMessage.textContent).toContain('项目:未命名游戏原型'); expect(polishMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(polishMessage.textContent).toContain( '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;自检 已通过;资产 2 个(美术 1 / 音频 0)', ); expect(polishMessage.textContent).toContain( '打磨范围:新手引导;移动试玩;可读性与无障碍;性能与加载;美术 / 音频素材;试玩反馈', ); expect(polishMessage.textContent).toContain( '推荐顺序:/tutorial -> /mobile -> /accessibility -> /performance -> /credits -> /feedback', ); expect(polishMessage.textContent).toContain( 'art-polish:美术组 / Polish 检查美术可用性 · 待处理', ); expect(polishMessage.textContent).toContain( 'quality-review:程序组 / Review 执行质量评审 · 待处理', ); expect(polishMessage.textContent).toContain( 'publish-package:运营组 / Publish 整理发布包装 · 待处理', ); expect(polishMessage.textContent).toContain( '最近打磨相关步骤:Evaluator #1 · passed · 质量评审通过', ); expect(polishMessage.textContent).toContain( '边界:只整理试玩前打磨清单;不读取文件;不启动预览;不导出试玩包;不写项目', ); expect(polishMessage.textContent).toContain( '建议:/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈', ); const polishMessageList = document.querySelector('.message-list'); expect(polishMessageList).not.toBeNull(); const polishDraftButtons = within( polishMessageList as HTMLElement, ).getAllByRole('button', { name: '补充打磨' }); fireEvent.click(polishDraftButtons[polishDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforePolish.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforePolish.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforePolish.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforePolish.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforePolish.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforePolish.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforePolish.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforePolish.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforePolish.controlAgentRun); const commandCountsBeforeCredits = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/credits'); const creditMessages = await screen.findAllByText(/素材署名:/); const creditMessage = creditMessages[creditMessages.length - 1]; expect(creditMessage.textContent).toContain('当前资产:2 个'); expect(creditMessage.textContent).toContain('来源分布:上传 1 / 画板 1'); expect(creditMessage.textContent).toContain( 'assets/uploads/hero.txt · text/plain · 上传 · 用户上传', ); expect(creditMessage.textContent).toContain( 'assets/canvas-sync/tiles.png · image/png · 画板 · 画板 canvas-1', ); expect(creditMessage.textContent).toContain( '需要确认:上传素材授权;生成素材模型;画板资源来源;本地试玩包保留来源口径', ); expect(creditMessage.textContent).toContain( '参考:/assets;/art;/audio;/listing', ); expect(creditMessage.textContent).toContain('建议:/assets'); const creditMessageList = document.querySelector('.message-list'); expect(creditMessageList).not.toBeNull(); const creditDraftButtons = within( creditMessageList as HTMLElement, ).getAllByRole('button', { name: '查看资产' }); fireEvent.click(creditDraftButtons[creditDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/assets', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeCredits.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeCredits.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeCredits.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeCredits.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeCredits.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeCredits.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeCredits.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeCredits.exportList); const manifestReadCountBeforeRisks = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const fileReadCountBeforeRisks = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const runLocalCountBeforeRisks = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; submitChat('/risks'); expect(await screen.findByText(/项目风险:/)).not.toBeNull(); expect( screen.getByText(/最近 run 已通过,但当前本地预览未运行。 建议:\/run/), ).not.toBeNull(); expect( screen.getByText(/还有 1 个 ready 任务等待处理。 建议:\/tasks/), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '处理首个风险' })); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeRisks); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeRisks); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeRisks); const commandCountsBeforeBlockers = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/blockers'); const blockerMessages = await screen.findAllByText(/当前阻塞项:/); const blockerMessage = blockerMessages[blockerMessages.length - 1]; expect(blockerMessage.textContent).toContain('项目:未命名游戏原型'); expect(blockerMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(blockerMessage.textContent).toContain( '原型已通过,但本地预览未运行。 建议:/run', ); expect(blockerMessage.textContent).toContain( '原型已通过,但本地试玩包尚未导出。 建议:/export', ); expect(blockerMessage.textContent).toContain( 'ready 任务 1 个:美术组 / Asset 规划美术资产(art-asset-plan)。 建议:/todo', ); expect(blockerMessage.textContent).toContain( '边界:只整理阻塞项;不读取文件;不启动预览;不导出试玩包;不写项目', ); expect(blockerMessage.textContent).toContain('建议:/run'); const blockerMessageList = document.querySelector('.message-list'); expect(blockerMessageList).not.toBeNull(); const blockerDraftButtons = within( blockerMessageList as HTMLElement, ).getAllByRole('button', { name: '处理首个阻塞' }); fireEvent.click(blockerDraftButtons[blockerDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeBlockers.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeBlockers.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeBlockers.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeBlockers.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeBlockers.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeBlockers.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeBlockers.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeBlockers.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeBlockers.controlAgentRun); const commandCountsBeforeReady = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/ready'); const readyMessages = await screen.findAllByText(/试玩就绪度:/); const readyMessage = readyMessages[readyMessages.length - 1]; expect(readyMessage.textContent).toContain('项目:未命名游戏原型'); expect(readyMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(readyMessage.textContent).toContain('原型:最近 run 已通过'); expect(readyMessage.textContent).toContain('预览:未启动'); expect(readyMessage.textContent).toContain('自检:已通过'); expect(readyMessage.textContent).toContain('试玩包:未导出'); expect(readyMessage.textContent).toContain('任务:失败 0 / ready 1'); expect(readyMessage.textContent).toContain( '素材:美术 已有 / 音频 可后补', ); expect(readyMessage.textContent).toContain( '结论:接近可测,先补齐预览 / 自检 / 试玩包', ); expect(readyMessage.textContent).toContain( '边界:只判断就绪度;不读取文件;不启动预览;不导出试玩包;不写项目', ); expect(readyMessage.textContent).toContain('建议:/run'); const readyMessageList = document.querySelector('.message-list'); expect(readyMessageList).not.toBeNull(); const readyDraftButtons = within( readyMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click(readyDraftButtons[readyDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeReady.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeReady.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeReady.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeReady.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeReady.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeReady.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeReady.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeReady.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeReady.controlAgentRun); const commandCountsBeforeEvidence = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/evidence'); const evidenceMessages = await screen.findAllByText(/验证证据台账:/); const evidenceMessage = evidenceMessages[evidenceMessages.length - 1]; expect(evidenceMessage.textContent).toContain('项目:未命名游戏原型'); expect(evidenceMessage.textContent).toContain( 'Run trace:已有 run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(evidenceMessage.textContent).toContain('Evaluator:通过'); expect(evidenceMessage.textContent).toContain('静态自检:已有通过证据'); expect(evidenceMessage.textContent).toContain('预览:未启动'); expect(evidenceMessage.textContent).toContain('试玩包:缺失'); expect(evidenceMessage.textContent).toContain( '入口产物:缺失 trace 产物证据', ); expect(evidenceMessage.textContent).toContain( '资产:2 个 · 美术 有 · 音频 可后补', ); expect(evidenceMessage.textContent).toContain('最近失败命令:暂无'); expect(evidenceMessage.textContent).toContain('缺口:'); expect(evidenceMessage.textContent).toContain( '本地预览未运行 · 建议 /run', ); expect(evidenceMessage.textContent).toContain( '缺少本地试玩包导出记录 · 建议 /export', ); expect(evidenceMessage.textContent).toContain( '边界:只整理当前已加载证据;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(evidenceMessage.textContent).toContain('建议:/run'); const evidenceMessageList = document.querySelector('.message-list'); expect(evidenceMessageList).not.toBeNull(); const evidenceDraftButtons = within( evidenceMessageList as HTMLElement, ).getAllByRole('button', { name: '补齐首个证据缺口' }); fireEvent.click(evidenceDraftButtons[evidenceDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeEvidence.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeEvidence.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeEvidence.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeEvidence.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeEvidence.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeEvidence.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeEvidence.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeEvidence.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeEvidence.controlAgentRun); const commandCountsBeforeDeps = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/deps'); const dependencyMessages = await screen.findAllByText(/任务依赖链:/); const dependencyMessage = dependencyMessages[dependencyMessages.length - 1]; expect(dependencyMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(dependencyMessage.textContent).toContain( '状态:active 0 / carry 0 / ready 1 / 等待依赖 15', ); expect(dependencyMessage.textContent).toContain( '美术组 / Asset 规划美术资产(art-asset-plan) · 依赖:美术组 / Director 确定视觉方向(art-director)', ); expect(dependencyMessage.textContent).toContain( '策划组 / Gameplay 确定玩法规格(design-foundation) · 等待:策划组 / Director 拆解创作方向(design-director)', ); expect(dependencyMessage.textContent).toContain( '边界:只整理任务依赖;不读取任务文件;不启动 run;不修改项目', ); expect(dependencyMessage.textContent).toContain('建议:/criteria'); const dependencyMessageList = document.querySelector('.message-list'); expect(dependencyMessageList).not.toBeNull(); const dependencyDraftButtons = within( dependencyMessageList as HTMLElement, ).getAllByRole('button', { name: '查看验收标准' }); fireEvent.click( dependencyDraftButtons[dependencyDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/criteria', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeDeps.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeDeps.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeDeps.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeDeps.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeDeps.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeDeps.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeDeps.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeDeps.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeDeps.controlAgentRun); const commandCountsBeforeRevise = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/revise'); const revisionMessages = await screen.findAllByText(/改版草稿:/); const revisionMessage = revisionMessages[revisionMessages.length - 1]; expect(revisionMessage.textContent).toContain('项目:未命名游戏原型'); expect(revisionMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(revisionMessage.textContent).toContain( '最近评审:Evaluator #1 · passed · 质量评审通过', ); expect(revisionMessage.textContent).toContain('最近试玩:暂无'); expect(revisionMessage.textContent).toContain( '推进 ready 任务:美术组 / Asset 规划美术资产(art-asset-plan)', ); expect(revisionMessage.textContent).toContain( '补齐本地试玩:启动预览并验证首屏', ); expect(revisionMessage.textContent).toContain('交付:导出本地试玩包'); expect(revisionMessage.textContent).toContain( '保留项:保留当前已通过的核心玩法和可运行入口', ); expect(revisionMessage.textContent).toContain( '调整项:推进 ready 任务:美术组 / Asset 规划美术资产(art-asset-plan);补齐本地试玩:启动预览并验证首屏', ); expect(revisionMessage.textContent).toContain( '新增项:补一次本地预览验证;补导出本地试玩包', ); expect(revisionMessage.textContent).toContain( '验收口径:通过 /ready、/qa 和 /changes 复查', ); expect(revisionMessage.textContent).toContain( '参考命令:/ready;/deps;/qa;/changes', ); expect(revisionMessage.textContent).toContain( '草稿:/agent-resume 改版说明:', ); expect(revisionMessage.textContent).toContain( '边界:只准备改版说明;不继续 run;不读取文件;不启动预览;不导出试玩包;不写项目', ); expect(revisionMessage.textContent).toContain( '建议:/agent-resume 改版说明:', ); const revisionMessageList = document.querySelector('.message-list'); expect(revisionMessageList).not.toBeNull(); const revisionDraftButtons = within( revisionMessageList as HTMLElement, ).getAllByRole('button', { name: '填入改版说明' }); fireEvent.click(revisionDraftButtons[revisionDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', expect.stringContaining('/agent-resume 改版说明:'), ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeRevise.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeRevise.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeRevise.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeRevise.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeRevise.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeRevise.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeRevise.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeRevise.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeRevise.controlAgentRun); const commandCountsBeforePrivacy = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/privacy'); const privacyMessages = await screen.findAllByText(/隐私与导出边界:/); const privacyMessage = privacyMessages[privacyMessages.length - 1]; expect(privacyMessage.textContent).toContain('项目:未命名游戏原型'); expect(privacyMessage.textContent).toContain( '本地目录:/tmp/authorized-game', ); expect(privacyMessage.textContent).toContain( 'API Key:只应保存在 App 运行时配置;不进入 manifest、trace、聊天、导出包或项目文件', ); expect(privacyMessage.textContent).toContain( '本地预览:未启动;仅限 127.0.0.1 本机访问', ); expect(privacyMessage.textContent).toContain( '试玩包:尚未导出;只应包含 game/**、assets/** 和 exports/README.md', ); expect(privacyMessage.textContent).toContain( '内部文件:.agent/**、memory/**、日志、trace、配置和密钥不得进入试玩包', ); expect(privacyMessage.textContent).toContain( '素材来源:2 个;上传 1 / 画板 1', ); expect(privacyMessage.textContent).toContain( 'Trace:run-main-shortcut-trace · 3 个内部产物记录;只通过 /trace 或 /internals 查看,不作为交付内容', ); expect(privacyMessage.textContent).toContain( '交付前建议:/credits;/ready;/export;/exports', ); expect(privacyMessage.textContent).toContain( '边界:只整理隐私与交付口径;不读取文件;不导出;不启动预览;不写项目', ); expect(privacyMessage.textContent).toContain('建议:/credits'); const privacyMessageList = document.querySelector('.message-list'); expect(privacyMessageList).not.toBeNull(); const privacyDraftButtons = within( privacyMessageList as HTMLElement, ).getAllByRole('button', { name: '查看素材来源' }); fireEvent.click(privacyDraftButtons[privacyDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/credits', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforePrivacy.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforePrivacy.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforePrivacy.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforePrivacy.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforePrivacy.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforePrivacy.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforePrivacy.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforePrivacy.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforePrivacy.controlAgentRun); const fileReadCountBeforeCriteria = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforeCriteria = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const runLocalCountBeforeCriteria = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; submitChat('/criteria'); expect(await screen.findByText(/当前验收标准:/)).not.toBeNull(); const criteriaMessages = screen.getAllByText(/当前验收标准:/); const criteriaMessage = criteriaMessages[criteriaMessages.length - 1]; expect(criteriaMessage.textContent).toContain( 'ready:美术组 / Asset 规划美术资产(art-asset-plan)', ); expect(criteriaMessage.textContent).toContain( '验收:角色、场景、UI 和动画需求已映射到画板或本地资产', ); expect(criteriaMessage.textContent).toContain( '产物:assets/manifest.art.json', ); const criteriaDraftButtons = screen.getAllByRole('button', { name: '查看任务', }); fireEvent.click(criteriaDraftButtons[criteriaDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/tasks'); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeCriteria); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeCriteria); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeCriteria); const fileReadCountBeforeGroups = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforeGroups = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const runLocalCountBeforeGroups = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; submitChat('/groups'); expect(await screen.findByText(/专业组进度:/)).not.toBeNull(); const groupMessages = screen.getAllByText(/专业组进度:/); const groupMessage = groupMessages[groupMessages.length - 1]; expect(groupMessage.textContent).toContain( '美术组:完成 0/3 · active 0 · carry 0 · ready 1', ); expect(groupMessage.textContent).toContain('下一步 Asset 规划美术资产'); expect(groupMessage.textContent).toContain('程序组:完成 0/5'); const groupDraftButtons = screen.getAllByRole('button', { name: '查看任务', }); fireEvent.click(groupDraftButtons[groupDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/tasks'); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeGroups); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeGroups); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeGroups); const commandCountsBeforeBalance = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, }; submitChat('/balance'); expect(await screen.findByText(/数值状态:/)).not.toBeNull(); const balanceMessages = screen.getAllByText(/数值状态:/); const balanceMessage = balanceMessages[balanceMessages.length - 1]; expect(balanceMessage.textContent).toContain('项目:未命名游戏原型'); expect(balanceMessage.textContent).toContain( 'balance-director:Director 确定数值口径 · 待处理', ); expect(balanceMessage.textContent).toContain( 'balance-seed:Difficulty 生成初版数值 · 待处理', ); expect(balanceMessage.textContent).toContain( '难度、节奏和得分口径可指导数值表', ); expect(balanceMessage.textContent).toContain( '速度、生命、得分和难度参数可被程序组读取', ); expect(balanceMessage.textContent).toContain( '数值表:game/balance.json · 待生成', ); expect(balanceMessage.textContent).toContain( '试玩关联:/playtest;/feedback', ); expect(balanceMessage.textContent).toContain( '建议:/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快', ); const balanceMessageList = document.querySelector('.message-list'); expect(balanceMessageList).not.toBeNull(); const balanceDraftButtons = within( balanceMessageList as HTMLElement, ).getAllByRole('button', { name: '填写数值反馈' }); fireEvent.click(balanceDraftButtons[balanceDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeBalance.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeBalance.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeBalance.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeBalance.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeBalance.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeBalance.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeBalance.exportPackage); const fileReadCountBeforeBudget = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforeBudget = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const runLocalCountBeforeBudget = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; submitChat('/budget'); expect(await screen.findByText(/运行预算:/)).not.toBeNull(); const budgetMessages = screen.getAllByText(/运行预算:/); const budgetMessage = budgetMessages[budgetMessages.length - 1]; expect(budgetMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(budgetMessage.textContent).toContain('轮次:已用 1/3 · 剩余 2'); expect(budgetMessage.textContent).toContain( '工具调用:已用 1/128 · 剩余 127', ); expect(budgetMessage.textContent).toContain('下一步:preview'); expect(budgetMessage.textContent).toContain('建议:/publish'); const budgetDraftButtons = screen.getAllByRole('button', { name: '查看发布准备', }); fireEvent.click(budgetDraftButtons[budgetDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/publish', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeBudget); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeBudget); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeBudget); const fileReadCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const runLocalCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; const startPreviewCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length; const openPreviewCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length; submitChat('/qa'); expect(await screen.findByText(/质量检查:/)).not.toBeNull(); const qaMessages = screen.getAllByText(/质量检查:/); const qaMessage = qaMessages[qaMessages.length - 1]; expect(qaMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(qaMessage.textContent).toContain('Evaluator:通过 · 质量评审通过'); expect(qaMessage.textContent).toContain( '任务:完成 0/16 · ready 1 · 失败 0', ); expect(qaMessage.textContent).toContain('静态自检:通过'); expect(qaMessage.textContent).toContain('试玩:待启动预览'); expect(qaMessage.textContent).toContain('产物:3 个'); expect(qaMessage.textContent).toContain('建议:/playtest'); const qaDraftButtons = screen.getAllByRole('button', { name: '查看试玩状态', }); fireEvent.click(qaDraftButtons[qaDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/playtest', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeQa); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeQa); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeQa); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(startPreviewCountBeforeQa); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(openPreviewCountBeforeQa); const fileReadCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const fileListCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length; const runLocalCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; const startPreviewCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length; const openPreviewCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length; submitChat('/changes'); expect(await screen.findByText(/最近变更:/)).not.toBeNull(); const changeMessages = screen.getAllByText(/最近变更:/); const changeMessage = changeMessages[changeMessages.length - 1]; expect(changeMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(changeMessage.textContent).toContain('可验产物:3 个'); expect(changeMessage.textContent).toContain('exports/README.md · 128B'); expect(changeMessage.textContent).toContain( '.agent/passes/pass-1/agenda.md · 96B', ); expect(changeMessage.textContent).toContain( 'Generator #1 · completed · .agent/passes/pass-1/draft.json', ); expect(changeMessage.textContent).toContain( '最近命令:game.static_smoke · 完成', ); expect(changeMessage.textContent).toContain( '真实差异:/checkpoints 后 /diff checkpoint-id', ); expect(changeMessage.textContent).toContain( '建议:/read exports/README.md', ); const changeDraftButtons = screen.getAllByRole('button', { name: '读取首个产物', }); fireEvent.click(changeDraftButtons[changeDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read exports/README.md', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeChanges); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeChanges); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(fileListCountBeforeChanges); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeChanges); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(startPreviewCountBeforeChanges); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(openPreviewCountBeforeChanges); const fileReadCountBeforeReview = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforeReview = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const runLocalCountBeforeReview = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; const startPreviewCountBeforeReview = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length; const openPreviewCountBeforeReview = invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length; submitChat('/review'); expect(await screen.findByText(/评审状态:/)).not.toBeNull(); const reviewMessages = screen.getAllByText(/评审状态:/); const reviewMessage = reviewMessages[reviewMessages.length - 1]; expect(reviewMessage.textContent).toContain('Evaluator:通过'); expect(reviewMessage.textContent).toContain('返工焦点:暂无'); expect(reviewMessage.textContent).toContain( '评审记录:/read .agent/findings.md', ); expect(reviewMessage.textContent).toContain( 'Evaluator #1 · passed · 质量评审通过', ); fireEvent.click(screen.getByRole('button', { name: '读取评审记录' })); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/findings.md', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeReview); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeReview); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeReview); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(startPreviewCountBeforeReview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(openPreviewCountBeforeReview); const fileReadCountBeforeContext = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; submitChat('/context'); expect(await screen.findByText(/上下文来源:/)).not.toBeNull(); expect(screen.getByText(/项目对话:\/history/)).not.toBeNull(); expect(screen.getByText(/项目黑板:\/memory blackboard/)).not.toBeNull(); expect(screen.getByText(/Agent 私有记忆:16 个/)).not.toBeNull(); expect( screen.getByText(/最近 Run:run-main-shortcut-trace · \/trace/), ).not.toBeNull(); expect( screen.getByText(/\.agent\/spec\.md:\/read \.agent\/spec\.md/), ).not.toBeNull(); const contextDraftButtons = screen.getAllByRole('button', { name: '读取首个上下文', }); fireEvent.click(contextDraftButtons[contextDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/spec.md', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeContext); const fileReadCountBeforeTimeline = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforeTimeline = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const runLocalCountBeforeTimeline = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; const startPreviewCountBeforeTimeline = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length; const openPreviewCountBeforeTimeline = invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length; submitChat('/timeline'); expect(await screen.findByText(/项目时间线:/)).not.toBeNull(); const timelineMessages = screen.getAllByText(/项目时间线:/); const timelineMessage = timelineMessages[timelineMessages.length - 1]; expect(timelineMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done', ); expect(timelineMessage.textContent).toContain( '命令 game.static_smoke · 完成 · 日志 /read .agent/logs/command.log', ); expect(timelineMessage.textContent).toContain( 'Generator #1 / generate · completed · 生成可运行草案 · 输出 .agent/passes/pass-1/draft.json', ); expect(timelineMessage.textContent).toContain( 'Evaluator #1 / evaluation · passed · 质量评审通过', ); const timelineDraftButtons = screen.getAllByRole('button', { name: '读取最近日志', }); fireEvent.click(timelineDraftButtons[timelineDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/logs/command.log', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeTimeline); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeTimeline); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeTimeline); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(startPreviewCountBeforeTimeline); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(openPreviewCountBeforeTimeline); const manifestReadCountBeforeHandoff = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const fileReadCountBeforeHandoff = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const runLocalCountBeforeHandoff = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; submitChat('/handoff'); expect(await screen.findByText(/项目交接:/)).not.toBeNull(); const handoffMessages = screen.getAllByText(/项目交接:/); const handoffMessage = handoffMessages[handoffMessages.length - 1]; expect(handoffMessage.textContent).toContain( '- Run:run-main-shortcut-trace', ); expect(handoffMessage.textContent).toContain( 'Ready:art/Asset 规划美术资产', ); expect(handoffMessage.textContent).toContain('历史:已加载 0 个 run'); const nextActionButtons = screen.getAllByRole('button', { name: '查看下一步', }); fireEvent.click(nextActionButtons[nextActionButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/next'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforeHandoff); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeHandoff); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeHandoff); const fileReadCountBeforeRuns = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; submitChat('/runs'); expect(await screen.findByText(/Run 历史读取命令:/)).not.toBeNull(); expect( screen.getByText(/当前指针 run-main-shortcut-trace .*:\/trace/), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '查看最近 Run' })); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/trace'); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeRuns); const fileReadCountBeforeRunFiles = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; submitChat('/run-files'); expect( await screen.findByText(/Agent 运行辅助文件读取命令:/), ).not.toBeNull(); expect( screen.getByText(/读输出流 · \.agent\/output\.jsonl/), ).not.toBeNull(); expect( screen.getByText(/读上下文包 · \.agent\/context\.bundle\.json/), ).not.toBeNull(); const runFilesMessageList = document.querySelector('.message-list'); expect(runFilesMessageList).not.toBeNull(); fireEvent.click( within(runFilesMessageList as HTMLElement).getByRole('button', { name: '读输出流', }), ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/output.jsonl', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeRunFiles); const fileReadCountBeforeInternals = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; submitChat('/internals'); expect(await screen.findByText(/项目内部真相源读取命令:/)).not.toBeNull(); expect( screen.getByText(/读 manifest · \.agent\/manifest\.json/), ).not.toBeNull(); expect( screen.getByText(/读项目对话 · \.agent\/conversations\/project\.jsonl/), ).not.toBeNull(); const internalDraftButtons = screen.getAllByRole('button', { name: '读 manifest', }); fireEvent.click(internalDraftButtons[internalDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/manifest.json', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforeInternals); const commandCountsBeforeTodo = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/todo'); const todoMessages = await screen.findAllByText(/下一轮小步:/); const todoMessage = todoMessages[todoMessages.length - 1]; expect(todoMessage.textContent).toContain('项目:未命名游戏原型'); expect(todoMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(todoMessage.textContent).toContain('编排下一步:preview'); expect(todoMessage.textContent).toContain( '1. ready:美术组 / Asset 规划美术资产(art-asset-plan) · 待处理 · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产 · 产物:assets/manifest.art.json', ); expect(todoMessage.textContent).toContain( '边界:只整理下一步;不读取任务文件;不启动 run;不修改项目', ); expect(todoMessage.textContent).toContain('建议:/tasks'); const todoMessageList = document.querySelector('.message-list'); expect(todoMessageList).not.toBeNull(); const todoDraftButtons = within( todoMessageList as HTMLElement, ).getAllByRole('button', { name: '查看任务' }); fireEvent.click(todoDraftButtons[todoDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/tasks'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeTodo.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeTodo.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeTodo.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeTodo.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeTodo.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeTodo.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeTodo.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeTodo.exportList); const commandCountsBeforePlan = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/plan'); const planMessages = await screen.findAllByText(/下一轮分工计划:/); const planMessage = planMessages[planMessages.length - 1]; expect(planMessage.textContent).toContain('项目:未命名游戏原型'); expect(planMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(planMessage.textContent).toContain('编排焦点:preview'); expect(planMessage.textContent).toContain('协作顺序:美术组'); expect(planMessage.textContent).toContain( '美术组:ready · Asset 规划美术资产(art-asset-plan) · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产', ); expect(planMessage.textContent).toContain( '空档组:策划组、程序组、数值组、音乐组、运营组', ); expect(planMessage.textContent).toContain( '边界:只整理下一轮分工;不读取任务文件;不启动 run;不修改项目', ); expect(planMessage.textContent).toContain( '建议:/agent-resume 下一轮计划:美术组 / Asset 规划美术资产', ); const planMessageList = document.querySelector('.message-list'); expect(planMessageList).not.toBeNull(); const planDraftButtons = within( planMessageList as HTMLElement, ).getAllByRole('button', { name: '继续执行计划' }); fireEvent.click(planDraftButtons[planDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/agent-resume 下一轮计划:美术组 / Asset 规划美术资产', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforePlan.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforePlan.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforePlan.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforePlan.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforePlan.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforePlan.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforePlan.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforePlan.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforePlan.controlAgentRun); const commandCountsBeforeGuide = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/guide'); const guideMessages = await screen.findAllByText(/使用导引:/); const guideMessage = guideMessages[guideMessages.length - 1]; expect(guideMessage.textContent).toContain('项目:未命名游戏原型'); expect(guideMessage.textContent).toContain('当前阶段:可导出'); expect(guideMessage.textContent).toContain( '现在先做:先运行自检并启动本地预览,通过后再导出试玩包。', ); expect(guideMessage.textContent).toContain( '推荐命令:/run / /test-plan / /share', ); expect(guideMessage.textContent).toContain( '边界:只给操作导引;不读取文件;不启动 run;不启动预览;不写项目', ); const guideMessageList = document.querySelector('.message-list'); expect(guideMessageList).not.toBeNull(); const guideDraftButtons = within( guideMessageList as HTMLElement, ).getAllByRole('button', { name: '执行导引建议' }); fireEvent.click(guideDraftButtons[guideDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeGuide.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeGuide.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeGuide.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeGuide.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeGuide.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeGuide.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeGuide.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeGuide.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeGuide.controlAgentRun); const commandCountsBeforeProgress = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/progress'); const progressMessages = await screen.findAllByText(/项目进度:/); const progressMessage = progressMessages[progressMessages.length - 1]; expect(progressMessage.textContent).toContain('项目:未命名游戏原型'); expect(progressMessage.textContent).toContain('当前阶段:已生成'); expect(progressMessage.textContent).toMatch( /任务完成度:0\/\d+ · 0% · ready 1 · 失败 0/, ); expect(progressMessage.textContent).toContain( '最近 Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(progressMessage.textContent).toContain('预览:未启动'); expect(progressMessage.textContent).toContain('素材:共 2 个 · 美术 1 · 音频 0'); expect(progressMessage.textContent).toContain( '交付:自检 已通过 · 试玩包 未导出', ); expect(progressMessage.textContent).toContain( '边界:只整理项目进度;不读取文件;不启动 run;不启动预览;不导出试玩包;不写项目', ); expect(progressMessage.textContent).toContain('建议:/run'); const progressMessageList = document.querySelector('.message-list'); expect(progressMessageList).not.toBeNull(); const progressDraftButtons = within( progressMessageList as HTMLElement, ).getAllByRole('button', { name: '启动预览' }); fireEvent.click(progressDraftButtons[progressDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeProgress.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeProgress.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeProgress.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeProgress.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeProgress.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeProgress.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeProgress.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeProgress.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeProgress.controlAgentRun); const runLocalCountBeforeNext = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; submitChat('/next'); expect(await screen.findByText(/下一步建议:/)).not.toBeNull(); expect(screen.getByText(/运行自检并启动本地预览:\/run/)).not.toBeNull(); expect(screen.getByText(/查看创作目标:\/goal/)).not.toBeNull(); expect(screen.getByText(/查看普通用户操作导引:\/guide/)).not.toBeNull(); expect(screen.getByText(/查看项目进度:\/progress/)).not.toBeNull(); expect(screen.getByText(/查看创作规格包:\/spec/)).not.toBeNull(); expect(screen.getByText(/查看本轮 MVP 范围:\/mvp/)).not.toBeNull(); expect(screen.getByText(/查看试玩定位与卖点:\/pitch/)).not.toBeNull(); expect(screen.getByText(/准备 30 秒试玩讲解稿:\/demo/)).not.toBeNull(); expect(screen.getByText(/查看玩法操作与规则:\/rules/)).not.toBeNull(); expect(screen.getByText(/查看新手引导检查:\/tutorial/)).not.toBeNull(); expect(screen.getByText(/查看移动试玩检查:\/mobile/)).not.toBeNull(); expect(screen.getByText(/准备兼容性说明:\/compatibility/)).not.toBeNull(); expect( screen.getByText(/查看可读性与无障碍检查:\/accessibility/), ).not.toBeNull(); expect( screen.getByText(/查看本地化与文案检查:\/localization/), ).not.toBeNull(); expect( screen.getByText(/查看性能与加载检查:\/performance/), ).not.toBeNull(); expect(screen.getByText(/查看试玩前打磨清单:\/polish/)).not.toBeNull(); expect(screen.getByText(/查看数值与难度口径:\/balance/)).not.toBeNull(); expect(screen.getByText(/查看 .* 个 ready 任务:\/tasks/)).not.toBeNull(); expect(screen.getByText(/查看当前任务验收标准:\/criteria/)).not.toBeNull(); expect(screen.getByText(/查看专业组进度:\/groups/)).not.toBeNull(); expect(screen.getByText(/查看质量检查清单:\/qa/)).not.toBeNull(); expect(screen.getByText(/查看最近生成变更:\/changes/)).not.toBeNull(); expect(screen.getByText(/查看下一轮分工计划:\/plan/)).not.toBeNull(); expect(screen.getByText(/查看下一轮小步清单:\/todo/)).not.toBeNull(); expect(screen.getByText(/查看当前阻塞项:\/blockers/)).not.toBeNull(); expect(screen.getByText(/查看试玩就绪度:\/ready/)).not.toBeNull(); expect(screen.getByText(/查看验证证据台账:\/evidence/)).not.toBeNull(); expect(screen.getByText(/查看 Agent LLM 路由:\/llm-routes/)).not.toBeNull(); expect(screen.getByText(/查看任务依赖链:\/deps/)).not.toBeNull(); expect(screen.getByText(/准备下一轮改版说明:\/revise/)).not.toBeNull(); expect(screen.getByText(/查看隐私与导出边界:\/privacy/)).not.toBeNull(); expect(screen.getByText(/查看首批试玩对象:\/audience/)).not.toBeNull(); expect(screen.getByText(/准备试玩邀请文案:\/invite/)).not.toBeNull(); expect(screen.getByText(/准备缺陷复现记录:\/bug-report/)).not.toBeNull(); expect(screen.getByText(/准备试玩问卷问题:\/survey/)).not.toBeNull(); expect(screen.getByText(/准备封面与缩略图检查:\/cover/)).not.toBeNull(); expect(screen.getByText(/准备宣传截图清单:\/screenshots/)).not.toBeNull(); expect(screen.getByText(/准备试玩短视频脚本:\/trailer/)).not.toBeNull(); expect(screen.getByText(/准备试玩常见问答:\/faq/)).not.toBeNull(); expect(screen.getByText(/准备社区发布文案:\/post/)).not.toBeNull(); expect(screen.getByText(/准备上架资料清单:\/store/)).not.toBeNull(); expect(screen.getByText(/准备媒体资料包清单:\/media-kit/)).not.toBeNull(); expect(screen.getByText(/准备试玩更新说明:\/release-notes/)).not.toBeNull(); expect(screen.getByText(/准备已知问题清单:\/known-issues/)).not.toBeNull(); expect(screen.getByText(/查看素材署名与来源:\/credits/)).not.toBeNull(); expect(screen.getByText(/查看美术素材:\/art/)).not.toBeNull(); expect(screen.getByText(/查看发布准备清单:\/publish/)).not.toBeNull(); expect(screen.getByText(/准备作品页文案清单:\/listing/)).not.toBeNull(); expect(screen.getByText(/查看试玩状态:\/playtest/)).not.toBeNull(); expect(screen.getByText(/准备手动测试计划:\/test-plan/)).not.toBeNull(); expect(screen.getByText(/准备试玩反馈:\/feedback/)).not.toBeNull(); expect(screen.getByText(/准备复玩观察清单:\/retention/)).not.toBeNull(); expect(screen.getByText(/准备试玩交付清单:\/share/)).not.toBeNull(); expect(screen.getByText(/查看最近 run 预算:\/budget/)).not.toBeNull(); expect(screen.getByText(/查看评审和返工焦点:\/review/)).not.toBeNull(); expect(screen.getByText(/查看生成上下文来源:\/context/)).not.toBeNull(); expect(screen.getByText(/查看项目活动时间线:\/timeline/)).not.toBeNull(); expect( screen.getByText(/列出内部真相源读取命令:\/internals/), ).not.toBeNull(); expect(screen.getByText(/列出 Agent 轮次产物:\/passes/)).not.toBeNull(); expect( screen.getByText(/列出 Agent 运行辅助文件:\/run-files/), ).not.toBeNull(); const nextMessageList = document.querySelector('.message-list'); expect(nextMessageList).not.toBeNull(); fireEvent.click( within(nextMessageList as HTMLElement).getByRole('button', { name: '运行自检并启动本地预览', }), ); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforeNext); const manifestReadCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const fileReadCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const runLocalCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; const startPreviewCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length; submitChat('/publish'); expect(await screen.findByText(/发布准备:/)).not.toBeNull(); expect( screen.getByText(/原型:最近 run 已通过 run-main-shortcut-trace/), ).not.toBeNull(); expect(screen.getByText(/预览:未启动 · 建议 \/run/)).not.toBeNull(); expect(screen.getByText(/音频:暂无 · 建议 \/audio/)).not.toBeNull(); expect( screen.getByText( /包装:最近 Run 包含 exports\/README\.md · \/read exports\/README\.md/, ), ).not.toBeNull(); const publishDraftButtons = screen.getAllByRole('button', { name: '启动预览', }); fireEvent.click(publishDraftButtons[publishDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforePublish); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforePublish); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforePublish); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(startPreviewCountBeforePublish); const commandCountsBeforeListing = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/listing'); const listingMessages = await screen.findAllByText(/作品页草稿:/); const listingMessage = listingMessages[listingMessages.length - 1]; expect(listingMessage.textContent).toContain('标题:未命名游戏原型'); expect(listingMessage.textContent).toContain( '一句话卖点:做一个厨房弹幕游戏', ); expect(listingMessage.textContent).toContain( 'publish-strategy:Director 整理运营定位 · 待处理', ); expect(listingMessage.textContent).toContain( 'publish-package:Publish 整理发布包装 · 待处理', ); expect(listingMessage.textContent).toContain( '封面素材:1 个视觉素材 · 可用 1 个画板 / 生成来源', ); expect(listingMessage.textContent).toContain( '说明文案:exports/README.md · 已生成', ); expect(listingMessage.textContent).toContain( '标签口径:玩法类型;视觉风格;难度 / 节奏;本地可玩', ); expect(listingMessage.textContent).toContain( '边界:只整理作品页文案和封面需求;不上传云端;不发布作品', ); expect(listingMessage.textContent).toContain( '建议:/read exports/README.md', ); const listingMessageList = document.querySelector('.message-list'); expect(listingMessageList).not.toBeNull(); const listingDraftButtons = within( listingMessageList as HTMLElement, ).getAllByRole('button', { name: '读发布说明' }); fireEvent.click(listingDraftButtons[listingDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read exports/README.md', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeListing.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeListing.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeListing.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeListing.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeListing.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeListing.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeListing.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeListing.exportList); const fileReadCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; const manifestReadCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length; const runLocalCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length; const startPreviewCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length; const openPreviewCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length; submitChat('/playtest'); const playtestMessages = await screen.findAllByText(/试玩状态:/); const playtestMessage = playtestMessages[playtestMessages.length - 1]; expect(playtestMessage.textContent).toContain( '原型:最近 run 已通过 run-main-shortcut-trace', ); expect(playtestMessage.textContent).toContain('预览:未启动 · 建议 /run'); expect(playtestMessage.textContent).toContain( 'Playtest 任务:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(playtestMessage.textContent).toContain( '试玩日志:/read .agent/logs/preview.log', ); const playtestDraftButtons = screen.getAllByRole('button', { name: '启动试玩', }); fireEvent.click(playtestDraftButtons[playtestDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforePlaytest); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(manifestReadCountBeforePlaytest); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(runLocalCountBeforePlaytest); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(startPreviewCountBeforePlaytest); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(openPreviewCountBeforePlaytest); const commandCountsBeforeTestPlan = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, }; submitChat('/test-plan'); const testPlanMessages = await screen.findAllByText(/手动测试计划:/); const testPlanMessage = testPlanMessages[testPlanMessages.length - 1]; expect(testPlanMessage.textContent).toContain('项目:未命名游戏原型'); expect(testPlanMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(testPlanMessage.textContent).toContain( '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;入口 未见 trace 产物', ); expect(testPlanMessage.textContent).toContain( '1. 启动预览:/run 后确认首屏不空白', ); expect(testPlanMessage.textContent).toContain( '2. 30 秒理解:目标、操作、得分/失败和重开可见', ); expect(testPlanMessage.textContent).toContain( '3. 输入验证:键盘/点击/触屏至少一种可完成核心动作', ); expect(testPlanMessage.textContent).toContain( '4. 结局验证:胜利或失败后可重开', ); expect(testPlanMessage.textContent).toContain( '5. 回归检查:/mobile;/accessibility;/performance;/audio', ); expect(testPlanMessage.textContent).toContain( 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', ); expect(testPlanMessage.textContent).toContain( 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(testPlanMessage.textContent).toContain('记录反馈:/feedback'); expect(testPlanMessage.textContent).toContain('建议:/run'); const testPlanMessageList = document.querySelector('.message-list'); expect(testPlanMessageList).not.toBeNull(); const testPlanDraftButtons = within( testPlanMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click(testPlanDraftButtons[testPlanDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeTestPlan.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeTestPlan.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeTestPlan.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeTestPlan.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeTestPlan.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeTestPlan.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeTestPlan.exportPackage); const commandCountsBeforeAudience = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/audience'); const audienceMessages = await screen.findAllByText(/首批试玩对象:/); const audienceMessage = audienceMessages[audienceMessages.length - 1]; expect(audienceMessage.textContent).toContain('项目:未命名游戏原型'); expect(audienceMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(audienceMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(audienceMessage.textContent).toContain( '先测人群:创作者自测 1 轮;熟悉目标的同事 1-2 人;完全没看过项目的人 3-5 人;至少 1 位移动/触屏用户', ); expect(audienceMessage.textContent).toContain( '第一批测试者:3-5 人;每人 5-10 分钟;先看能否独立理解', ); expect(audienceMessage.textContent).toContain( '观察重点:30 秒能否理解目标;输入是否顺;胜负/重开是否明确;难度是否过早劝退;视觉/音效是否干扰', ); expect(audienceMessage.textContent).toContain( 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', ); expect(audienceMessage.textContent).toContain( 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', ); expect(audienceMessage.textContent).toContain( '暂不面向:公开发布、付费用户、大规模投放、儿童/无障碍等强承诺场景', ); expect(audienceMessage.textContent).toContain( '参考:/playtest;/test-plan;/feedback;/share', ); expect(audienceMessage.textContent).toContain( '边界:只整理首批试玩对象;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(audienceMessage.textContent).toContain('建议:/run'); const audienceMessageList = document.querySelector('.message-list'); expect(audienceMessageList).not.toBeNull(); const audienceDraftButtons = within( audienceMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click(audienceDraftButtons[audienceDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeAudience.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeAudience.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeAudience.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeAudience.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeAudience.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeAudience.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeAudience.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeAudience.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeAudience.controlAgentRun); const commandCountsBeforeInvite = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/invite'); const inviteMessages = await screen.findAllByText(/试玩邀请:/); const inviteMessage = inviteMessages[inviteMessages.length - 1]; expect(inviteMessage.textContent).toContain('项目:未命名游戏原型'); expect(inviteMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(inviteMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(inviteMessage.textContent).toContain( '邀请对象:先发 3-5 人;优先熟人/同事/没看过项目的人;暂不公开发布或大规模投放', ); expect(inviteMessage.textContent).toContain('5-10 分钟试玩'); expect(inviteMessage.textContent).toContain('30 秒内能否理解目标'); expect(inviteMessage.textContent).toContain('操作是否顺'); expect(inviteMessage.textContent).toContain('胜负和重开是否清楚'); expect(inviteMessage.textContent).toContain( '发送前:先 /run 启动本地预览,再把本地试玩方式发给测试者', ); expect(inviteMessage.textContent).toContain( '参考:/audience;/test-plan;/feedback;/share', ); expect(inviteMessage.textContent).toContain( '边界:只准备邀请文案;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(inviteMessage.textContent).toContain('建议:/run'); const inviteMessageList = document.querySelector('.message-list'); expect(inviteMessageList).not.toBeNull(); const inviteDraftButtons = within( inviteMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click(inviteDraftButtons[inviteDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeInvite.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeInvite.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeInvite.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeInvite.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeInvite.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeInvite.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeInvite.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeInvite.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeInvite.controlAgentRun); const commandCountsBeforeBugReport = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/bug-report'); const bugReportMessages = await screen.findAllByText(/缺陷记录:/); const bugReportMessage = bugReportMessages[bugReportMessages.length - 1]; expect(bugReportMessage.textContent).toContain('项目:未命名游戏原型'); expect(bugReportMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(bugReportMessage.textContent).toContain( '复现入口:预览 未启动 · 建议 /run', ); expect(bugReportMessage.textContent).toContain('最近试玩证据:暂无'); expect(bugReportMessage.textContent).toContain( '记录模板:问题一句话;复现步骤 1/2/3;期望结果;实际结果;设备/输入方式;严重度 阻断/高/中/低;附件 截图/录屏/日志时间点', ); expect(bugReportMessage.textContent).toContain( '优先级口径:阻断无法进入首局;高影响胜负或重开;中影响理解或手感;低为包装和文字问题', ); expect(bugReportMessage.textContent).toContain( '转修复草稿:/agent-resume 缺陷修复:现象…;复现…;期望…;实际…', ); expect(bugReportMessage.textContent).toContain( '参考:/test-plan;/feedback;/review;/logs', ); expect(bugReportMessage.textContent).toContain( '边界:只准备缺陷记录模板;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(bugReportMessage.textContent).toContain('建议:/run'); const bugReportMessageList = document.querySelector('.message-list'); expect(bugReportMessageList).not.toBeNull(); const bugReportDraftButtons = within( bugReportMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click(bugReportDraftButtons[bugReportDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeBugReport.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeBugReport.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeBugReport.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeBugReport.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeBugReport.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeBugReport.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeBugReport.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeBugReport.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeBugReport.controlAgentRun); const commandCountsBeforeSurvey = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/survey'); const surveyMessages = await screen.findAllByText(/试玩问卷:/); const surveyMessage = surveyMessages[surveyMessages.length - 1]; expect(surveyMessage.textContent).toContain('项目:未命名游戏原型'); expect(surveyMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(surveyMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(surveyMessage.textContent).toContain( '使用场景:发给首批 3-5 位测试者;每人 5-10 分钟;先自由玩一局再回答', ); expect(surveyMessage.textContent).toContain( '问题清单:1. 30 秒内你觉得目标是什么?2. 第一次操作哪里最卡?3. 胜负/重开是否清楚?4. 难度/节奏感觉如何?5. 最想保留和最想改的各一项?', ); expect(surveyMessage.textContent).toContain( '记录格式:每题 1-5 分 + 一句话;补充设备、输入方式、是否愿意再玩一局', ); expect(surveyMessage.textContent).toContain( '追踪方式:单个问题走 /bug-report;整体反馈走 /feedback;下一轮改动走 /revise', ); expect(surveyMessage.textContent).toContain( '参考:/invite;/audience;/feedback;/bug-report', ); expect(surveyMessage.textContent).toContain( '边界:只准备试玩问卷;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(surveyMessage.textContent).toContain('建议:/run'); const surveyMessageList = document.querySelector('.message-list'); expect(surveyMessageList).not.toBeNull(); const surveyDraftButtons = within( surveyMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click(surveyDraftButtons[surveyDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeSurvey.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeSurvey.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeSurvey.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeSurvey.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeSurvey.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeSurvey.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeSurvey.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeSurvey.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeSurvey.controlAgentRun); const commandCountsBeforeCover = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/cover'); const coverMessages = await screen.findAllByText(/封面与缩略图:/); const coverMessage = coverMessages[coverMessages.length - 1]; expect(coverMessage.textContent).toContain('项目:未命名游戏原型'); expect(coverMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(coverMessage.textContent).toContain( '可用素材:视觉素材 1 个;画板/生成候选 1 个;总资产 2 个', ); expect(coverMessage.textContent).toContain( '封面候选:assets/canvas-sync/tiles.png · image/png · 画板', ); expect(coverMessage.textContent).toContain( '用途尺寸:作品页封面 16:9;社区缩略图 1:1;移动首屏 9:16', ); expect(coverMessage.textContent).toContain( '选择口径:优先展示核心玩法状态;避免内部路径、调试面板、密钥配置或纯空场景', ); expect(coverMessage.textContent).toContain( '补齐路径:有可试玩时先 /screenshots;缺美术时 /art;作品页文案走 /listing', ); expect(coverMessage.textContent).toContain( '参考:/screenshots;/listing;/media-kit;/credits', ); expect(coverMessage.textContent).toContain( '边界:只准备封面与缩略图检查;不截屏;不裁剪;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', ); expect(coverMessage.textContent).toContain('建议:/run'); const coverMessageList = document.querySelector('.message-list'); expect(coverMessageList).not.toBeNull(); const coverDraftButtons = within(coverMessageList as HTMLElement) .getAllByRole('button', { name: '启动试玩' }); fireEvent.click(coverDraftButtons[coverDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeCover.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeCover.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeCover.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeCover.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeCover.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeCover.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeCover.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeCover.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeCover.controlAgentRun); const commandCountsBeforeScreenshots = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/screenshots'); const screenshotMessages = await screen.findAllByText(/宣传截图:/); const screenshotMessage = screenshotMessages[screenshotMessages.length - 1]; expect(screenshotMessage.textContent).toContain('项目:未命名游戏原型'); expect(screenshotMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(screenshotMessage.textContent).toContain( '可用素材:视觉素材 1 个;总资产 2 个', ); expect(screenshotMessage.textContent).toContain( '截图目标:封面一张;核心操作一张;胜负/重开一张;移动或窄屏一张;异常/空状态不作为首批宣传图', ); expect(screenshotMessage.textContent).toContain( '拍摄顺序:先确认 /run 可试玩;进入第一局 10-30 秒;截核心交互;再截结算或失败反馈', ); expect(screenshotMessage.textContent).toContain( '命名建议:exports/screenshots/cover.png;gameplay.png;result.png;mobile.png', ); expect(screenshotMessage.textContent).toContain( '文案搭配:每张图只配一句卖点;作品页标题和标签继续走 /listing', ); expect(screenshotMessage.textContent).toContain( '参考:/listing;/publish;/share;/credits', ); expect(screenshotMessage.textContent).toContain( '边界:只准备截图清单;不截屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(screenshotMessage.textContent).toContain('建议:/run'); const screenshotMessageList = document.querySelector('.message-list'); expect(screenshotMessageList).not.toBeNull(); const screenshotDraftButtons = within( screenshotMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click( screenshotDraftButtons[screenshotDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeScreenshots.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeScreenshots.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeScreenshots.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeScreenshots.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeScreenshots.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeScreenshots.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeScreenshots.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeScreenshots.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeScreenshots.controlAgentRun); const commandCountsBeforeTrailer = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/trailer'); const trailerMessages = await screen.findAllByText(/试玩短视频:/); const trailerMessage = trailerMessages[trailerMessages.length - 1]; expect(trailerMessage.textContent).toContain('项目:未命名游戏原型'); expect(trailerMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(trailerMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(trailerMessage.textContent).toContain( '可用素材:视觉素材 1 个;音频素材 0 个;总资产 2 个', ); expect(trailerMessage.textContent).toContain( '15 秒结构:0-3 秒首屏目标;3-8 秒核心操作;8-12 秒胜负 / 重开;12-15 秒结尾 CTA', ); expect(trailerMessage.textContent).toContain( '镜头清单:标题 / 目标提示;玩家第一次操作;得分或失败反馈;重开按钮;结尾试玩邀请', ); expect(trailerMessage.textContent).toContain( '口播节奏:一句玩法目标;一句操作说明;一句邀请试玩和反馈', ); expect(trailerMessage.textContent).toContain( '录制提示:先确认 /run 可试玩;横屏或竖屏只选一种;不露内部路径、调试面板或密钥配置', ); expect(trailerMessage.textContent).toContain( '参考:/screenshots;/listing;/share;/publish', ); expect(trailerMessage.textContent).toContain( '边界:只准备试玩短视频脚本;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(trailerMessage.textContent).toContain('建议:/run'); const trailerMessageList = document.querySelector('.message-list'); expect(trailerMessageList).not.toBeNull(); const trailerDraftButtons = within(trailerMessageList as HTMLElement) .getAllByRole('button', { name: '启动试玩' }); fireEvent.click(trailerDraftButtons[trailerDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeTrailer.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeTrailer.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeTrailer.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeTrailer.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeTrailer.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeTrailer.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeTrailer.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeTrailer.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeTrailer.controlAgentRun); const commandCountsBeforeFaq = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/faq'); const faqMessages = await screen.findAllByText(/试玩 FAQ:/); const faqMessage = faqMessages[faqMessages.length - 1]; expect(faqMessage.textContent).toContain('项目:未命名游戏原型'); expect(faqMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); expect(faqMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(faqMessage.textContent).toContain( '问答清单:1. 这是什么?2. 怎么开始和重开?3. 需要反馈什么?4. 打不开或卡住怎么办?5. 能不能转发或公开?', ); expect(faqMessage.textContent).toContain( '回答口径:早期本地 Web 原型;5-10 分钟试玩;重点反馈目标理解、操作手感、难度、bug 和还想不想再玩', ); expect(faqMessage.textContent).toContain( '测试者提醒:先自由玩一局;不要评价完成度;问卷走 /survey;单个问题走 /bug-report', ); expect(faqMessage.textContent).toContain( '交付搭配:/invite;/share;/screenshots;/trailer', ); expect(faqMessage.textContent).toContain( '边界:只准备试玩常见问答;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(faqMessage.textContent).toContain('建议:/run'); const faqMessageList = document.querySelector('.message-list'); expect(faqMessageList).not.toBeNull(); const faqDraftButtons = within(faqMessageList as HTMLElement) .getAllByRole('button', { name: '启动试玩' }); fireEvent.click(faqDraftButtons[faqDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeFaq.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeFaq.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeFaq.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeFaq.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeFaq.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeFaq.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeFaq.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeFaq.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeFaq.controlAgentRun); const commandCountsBeforePost = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/post'); const postMessages = await screen.findAllByText(/社区发布文案:/); const postMessage = postMessages[postMessages.length - 1]; expect(postMessage.textContent).toContain('项目:未命名游戏原型'); expect(postMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); expect(postMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(postMessage.textContent).toContain( '素材准备:视觉素材 1 个;配图走 /screenshots;短视频走 /trailer', ); expect(postMessage.textContent).toContain( '短文案:我做了一个早期 Web 小游戏原型《未命名游戏原型》,核心目标是做一个厨房弹幕游戏。想找 3-5 位朋友试玩 5 分钟,重点看能不能理解目标、操作顺不顺、还想不想再来一局。', ); expect(postMessage.textContent).toContain( '长文案结构:一句玩法目标;一张截图或短视频;试玩方式;希望收到的三类反馈;已知限制', ); expect(postMessage.textContent).toContain( '标签建议:#Web小游戏 #原型试玩 #AI游戏创作 #本地试玩', ); expect(postMessage.textContent).toContain( 'CTA:愿意试玩请回复;遇到问题按 /faq 或 /bug-report 的口径反馈', ); expect(postMessage.textContent).toContain( '参考:/faq;/screenshots;/trailer;/store;/share', ); expect(postMessage.textContent).toContain( '边界:只准备社区发布文案;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(postMessage.textContent).toContain('建议:/run'); const postMessageList = document.querySelector('.message-list'); expect(postMessageList).not.toBeNull(); const postDraftButtons = within(postMessageList as HTMLElement) .getAllByRole('button', { name: '启动试玩' }); fireEvent.click(postDraftButtons[postDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforePost.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforePost.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforePost.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforePost.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforePost.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforePost.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforePost.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforePost.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforePost.controlAgentRun); const commandCountsBeforeStore = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/store'); const storeMessages = await screen.findAllByText(/上架资料:/); const storeMessage = storeMessages[storeMessages.length - 1]; expect(storeMessage.textContent).toContain('项目:未命名游戏原型'); expect(storeMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); expect(storeMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(storeMessage.textContent).toContain( '资产概况:视觉 1 个;音频 0 个;总资产 2 个', ); expect(storeMessage.textContent).toContain( '必备资料:作品页文案 /listing;宣传截图 /screenshots;素材署名 /credits;隐私边界 /privacy;试玩包 /export', ); expect(storeMessage.textContent).toContain( '发布说明:exports/README.md · 已生成', ); expect(storeMessage.textContent).toContain( '首发范围:本地 Web 原型;小规模试玩;免费体验;不承诺账号、云存档、排行榜或付费', ); expect(storeMessage.textContent).toContain( '上架前检查:30 秒玩法可懂;首屏不空白;重开清楚;截图不含内部路径;素材来源可说明', ); expect(storeMessage.textContent).toContain( '参考:/publish;/listing;/screenshots;/credits;/privacy;/share', ); expect(storeMessage.textContent).toContain( '边界:只准备上架资料清单;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', ); expect(storeMessage.textContent).toContain('建议:/run'); const storeMessageList = document.querySelector('.message-list'); expect(storeMessageList).not.toBeNull(); const storeDraftButtons = within(storeMessageList as HTMLElement) .getAllByRole('button', { name: '启动试玩' }); fireEvent.click(storeDraftButtons[storeDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeStore.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeStore.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeStore.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeStore.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeStore.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeStore.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeStore.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeStore.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeStore.controlAgentRun); const commandCountsBeforeMediaKit = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/media-kit'); const mediaKitMessages = await screen.findAllByText(/媒体资料包:/); const mediaKitMessage = mediaKitMessages[mediaKitMessages.length - 1]; expect(mediaKitMessage.textContent).toContain('项目:未命名游戏原型'); expect(mediaKitMessage.textContent).toContain( '一句话:做一个厨房弹幕游戏', ); expect(mediaKitMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', ); expect(mediaKitMessage.textContent).toContain( '素材概况:视觉素材 1 个;音频素材 0 个;总资产 2 个;发布说明 exports/README.md · 已生成', ); expect(mediaKitMessage.textContent).toContain( '资料清单:作品页 /listing;宣传截图 /screenshots;短视频 /trailer;FAQ /faq;社区文案 /post;上架清单 /store', ); expect(mediaKitMessage.textContent).toContain( '缺口优先级:先跑 /run 确认可试玩;再补 /screenshots 和 /trailer;最后整理 /post 与 /store', ); expect(mediaKitMessage.textContent).toContain( '打包顺序:1. 确认首屏和核心玩法;2. 准备截图 / 视频 / FAQ;3. 汇总署名、隐私和发布说明', ); expect(mediaKitMessage.textContent).toContain( '参考:/screenshots;/trailer;/listing;/faq;/post;/store;/share', ); expect(mediaKitMessage.textContent).toContain( '边界:只准备媒体资料包清单;不截屏;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', ); expect(mediaKitMessage.textContent).toContain('建议:/run'); const mediaKitMessageList = document.querySelector('.message-list'); expect(mediaKitMessageList).not.toBeNull(); const mediaKitDraftButtons = within(mediaKitMessageList as HTMLElement) .getAllByRole('button', { name: '启动试玩' }); fireEvent.click( mediaKitDraftButtons[mediaKitDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeMediaKit.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeMediaKit.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeMediaKit.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeMediaKit.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeMediaKit.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeMediaKit.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeMediaKit.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeMediaKit.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeMediaKit.controlAgentRun); const commandCountsBeforeReleaseNotes = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/release-notes'); const releaseNotesMessages = await screen.findAllByText(/试玩更新说明:/); const releaseNotesMessage = releaseNotesMessages[releaseNotesMessages.length - 1]; expect(releaseNotesMessage.textContent).toContain('项目:未命名游戏原型'); expect(releaseNotesMessage.textContent).toContain( '一句话:做一个厨房弹幕游戏', ); expect(releaseNotesMessage.textContent).toContain( '当前版本:本地 Web 原型 · 小范围试玩 · 预览 未启动', ); expect(releaseNotesMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(releaseNotesMessage.textContent).toContain( '本轮变化:可试玩版本已通过 Evaluator', ); expect(releaseNotesMessage.textContent).toContain( '主要产物:exports/README.md;.agent/passes/pass-1/agenda.md', ); expect(releaseNotesMessage.textContent).toContain( '素材变化:视觉素材 1 个;音频素材 0 个', ); expect(releaseNotesMessage.textContent).toContain( '玩家可见说明:玩法目标;操作方式;胜负 / 重开反馈;当前已知限制', ); expect(releaseNotesMessage.textContent).toContain( '已知限制:本地原型;不承诺账号、云存档、排行榜、付费或长期兼容', ); expect(releaseNotesMessage.textContent).toContain( '搭配:/changes;/media-kit;/post;/store;/share', ); expect(releaseNotesMessage.textContent).toContain( '边界:只准备试玩更新说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', ); expect(releaseNotesMessage.textContent).toContain('建议:/run'); const releaseNotesMessageList = document.querySelector('.message-list'); expect(releaseNotesMessageList).not.toBeNull(); const releaseNotesDraftButtons = within( releaseNotesMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click( releaseNotesDraftButtons[releaseNotesDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeReleaseNotes.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeReleaseNotes.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeReleaseNotes.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeReleaseNotes.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeReleaseNotes.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeReleaseNotes.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeReleaseNotes.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeReleaseNotes.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeReleaseNotes.controlAgentRun); const commandCountsBeforeKnownIssues = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/known-issues'); const knownIssueMessages = await screen.findAllByText(/已知问题清单:/); const knownIssueMessage = knownIssueMessages[knownIssueMessages.length - 1]; expect(knownIssueMessage.textContent).toContain('项目:未命名游戏原型'); expect(knownIssueMessage.textContent).toContain( '当前状态:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed;预览 未启动', ); expect(knownIssueMessage.textContent).toContain( '已知问题:暂无明确失败任务;仍按早期原型标注限制', ); expect(knownIssueMessage.textContent).toContain( '试玩限制:本地 Web 原型;小范围 5-10 分钟试玩;不承诺账号、云存档、排行榜、付费或长期兼容', ); expect(knownIssueMessage.textContent).toContain( '反馈入口:单个问题走 /bug-report;整体体验走 /feedback;版本变化走 /release-notes', ); expect(knownIssueMessage.textContent).toContain( '发送前检查:可试玩状态先 /run;交付口径看 /share;对外资料看 /media-kit', ); expect(knownIssueMessage.textContent).toContain( '边界:只准备已知问题清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', ); expect(knownIssueMessage.textContent).toContain('建议:/run'); const knownIssueMessageList = document.querySelector('.message-list'); expect(knownIssueMessageList).not.toBeNull(); const knownIssueDraftButtons = within( knownIssueMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click( knownIssueDraftButtons[knownIssueDraftButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeKnownIssues.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeKnownIssues.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeKnownIssues.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeKnownIssues.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeKnownIssues.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeKnownIssues.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeKnownIssues.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeKnownIssues.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeKnownIssues.controlAgentRun); const commandCountsBeforeFeedback = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, }; submitChat('/feedback'); const feedbackMessages = await screen.findAllByText(/试玩反馈:/); const feedbackMessage = feedbackMessages[feedbackMessages.length - 1]; expect(feedbackMessage.textContent).toContain( 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', ); expect(feedbackMessage.textContent).toContain('预览:未启动 · 建议 /run'); expect(feedbackMessage.textContent).toContain( '反馈方向:操作手感;胜负目标;难度;视觉 / 音效;重开路径', ); expect(feedbackMessage.textContent).toContain( '反馈模板:/agent-resume 试玩反馈:保留…;调整…;新增…', ); expect(feedbackMessage.textContent).toContain( '参考:/playtest;/qa;/changes', ); expect(feedbackMessage.textContent).toContain('建议:/run'); const feedbackMessageList = document.querySelector('.message-list'); expect(feedbackMessageList).not.toBeNull(); const feedbackDraftButtons = within( feedbackMessageList as HTMLElement, ).getAllByRole('button', { name: '启动预览' }); fireEvent.click(feedbackDraftButtons[feedbackDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeFeedback.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeFeedback.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeFeedback.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeFeedback.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeFeedback.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeFeedback.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeFeedback.exportPackage); const commandCountsBeforeRetention = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, controlAgentRun: invoke.mock.calls.filter( ([command]) => command === 'control_agent_run', ).length, }; submitChat('/retention'); const retentionMessages = await screen.findAllByText(/复玩观察:/); const retentionMessage = retentionMessages[retentionMessages.length - 1]; expect(retentionMessage.textContent).toContain('项目:未命名游戏原型'); expect(retentionMessage.textContent).toContain( '目标:做一个厨房弹幕游戏', ); expect(retentionMessage.textContent).toContain( '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动;自检 已通过;试玩包 待导出', ); expect(retentionMessage.textContent).toContain( '首轮样本:3-5 名测试者;每人 5-10 分钟;先不解释玩法,观察是否能自己完成首局', ); expect(retentionMessage.textContent).toContain( '复玩信号:是否主动重开;失败后是否理解原因;第二局是否更快进入目标;是否愿意换难度/角色/关卡;是否能说出想保留的一点', ); expect(retentionMessage.textContent).toContain( '资产与包装:素材 2 个;发布说明 已生成', ); expect(retentionMessage.textContent).toContain('最近试玩证据:暂无'); expect(retentionMessage.textContent).toContain( '记录模板:保留 1 项;调弱/调强 1 项;新增 1 项;必须修 1 项;是否愿意再玩一局', ); expect(retentionMessage.textContent).toContain( '暂不做:真实埋点;留存报表;用户画像;A/B 实验;排行榜或账号留存', ); expect(retentionMessage.textContent).toContain( '参考:/playtest;/feedback;/survey;/share;/known-issues', ); expect(retentionMessage.textContent).toContain( '边界:只准备复玩观察清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', ); expect(retentionMessage.textContent).toContain('建议:/run'); const retentionMessageList = document.querySelector('.message-list'); expect(retentionMessageList).not.toBeNull(); const retentionDraftButtons = within( retentionMessageList as HTMLElement, ).getAllByRole('button', { name: '启动试玩' }); fireEvent.click(retentionDraftButtons[retentionDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty('value', '/run'); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeRetention.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeRetention.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeRetention.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeRetention.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeRetention.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeRetention.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeRetention.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeRetention.exportList); expect( invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), ).toHaveLength(commandCountsBeforeRetention.controlAgentRun); const commandCountsBeforeShare = { manifestRead: invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ).length, fileRead: invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length, fileList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ).length, runLocal: invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ).length, startPreview: invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ).length, openPreview: invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ).length, exportPackage: invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ).length, exportList: invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ).length, }; submitChat('/share'); const shareMessages = await screen.findAllByText(/试玩交付:/); const shareMessage = shareMessages[shareMessages.length - 1]; expect(shareMessage.textContent).toContain('项目:未命名游戏原型'); expect(shareMessage.textContent).toContain('目录:/tmp/authorized-game'); expect(shareMessage.textContent).toContain( '原型:最近 run 已通过 run-main-shortcut-trace', ); expect(shareMessage.textContent).toContain('本地预览:未启动 · /run'); expect(shareMessage.textContent).toContain('本地试玩包:待导出 · /export'); expect(shareMessage.textContent).toContain( '给测试者:玩法目标 / 操作 / 胜负 / 重开口径见 /rules', ); expect(shareMessage.textContent).toContain('反馈收集:/feedback'); expect(shareMessage.textContent).toContain( '交付边界:本地 ZIP 和本地预览;不上传云端;不生成公开分享链接', ); expect(shareMessage.textContent).toContain('建议:/export'); const shareMessageList = document.querySelector('.message-list'); expect(shareMessageList).not.toBeNull(); const shareDraftButtons = within( shareMessageList as HTMLElement, ).getAllByRole('button', { name: '导出试玩包' }); fireEvent.click(shareDraftButtons[shareDraftButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/export', ); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', ), ).toHaveLength(commandCountsBeforeShare.manifestRead); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(commandCountsBeforeShare.fileRead); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', ), ).toHaveLength(commandCountsBeforeShare.fileList); expect( invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', ), ).toHaveLength(commandCountsBeforeShare.runLocal); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(commandCountsBeforeShare.startPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_game_preview', ), ).toHaveLength(commandCountsBeforeShare.openPreview); expect( invoke.mock.calls.filter( ([command]) => command === 'export_local_project_package', ), ).toHaveLength(commandCountsBeforeShare.exportPackage); expect( invoke.mock.calls.filter( ([command]) => command === 'list_local_project_export_packages', ), ).toHaveLength(commandCountsBeforeShare.exportList); fireEvent.click(screen.getByRole('button', { name: '权限' })); expect( await screen.findByText(/策略:\.agent\/policy\.json/), ).not.toBeNull(); const composerInput = screen.getByLabelText('创作想法'); fireEvent.click(screen.getByRole('button', { name: '索引确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm project.index', ); fireEvent.click(screen.getByRole('button', { name: '资产登记确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm asset.register', ); fireEvent.click(screen.getByRole('button', { name: '记忆写入确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm memory.write', ); fireEvent.click(screen.getByRole('button', { name: '预览确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm preview.start', ); fireEvent.click(screen.getByRole('button', { name: '打开预览确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm preview.open', ); fireEvent.click(screen.getByRole('button', { name: '停止预览确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm preview.stop', ); fireEvent.click(screen.getByRole('button', { name: 'Agent确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm agent.run_status', ); fireEvent.click(screen.getByRole('button', { name: '读对话确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm conversation.read', ); fireEvent.click(screen.getByRole('button', { name: '存对话确认' })); expect(composerInput).toHaveProperty( 'value', '/policy-confirm conversation.write', ); expect(screen.queryByText('project.policy_write')).toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'write_project_permission_policy', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '审计' })); expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '资产' })); expect( await screen.findByText(/uploaded · assets\/uploads\/hero\.txt/), ).not.toBeNull(); expect( within(screen.getByLabelText('最近项目资产')).getByText( 'uploaded · text/plain · uploaded', ), ).not.toBeNull(); const artManifestReadCountBefore = invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === 'assets/manifest.art.json', ).length; submitChat('/art'); expect(await screen.findByText(/美术素材:1 个/)).not.toBeNull(); expect(screen.getByText(/来源:画板 1/)).not.toBeNull(); expect( screen.getByText(/assets\/canvas-sync\/tiles\.png · image\/png · 画板/), ).not.toBeNull(); const visualDraftButtons = screen.getAllByRole('button', { name: '读美术清单', }); fireEvent.click(visualDraftButtons[visualDraftButtons.length - 1]); expect(composerInput).toHaveProperty( 'value', '/read assets/manifest.art.json', ); expect( invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === 'assets/manifest.art.json', ), ).toHaveLength(artManifestReadCountBefore); const audioManifestReadCountBefore = invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === 'assets/manifest.audio.json', ).length; submitChat('/audio'); expect(await screen.findByText(/音频素材:暂无登记音频/)).not.toBeNull(); expect( screen.getByText(/可先登记项目内音效,或把已有画板音频作为素材导入/), ).not.toBeNull(); const audioDraftButtons = screen.getAllByRole('button', { name: '登记音效', }); fireEvent.click(audioDraftButtons[audioDraftButtons.length - 1]); expect(composerInput).toHaveProperty( 'value', '/asset-register assets/audio/sfx.wav audio audio/wav', ); expect( invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === 'assets/manifest.audio.json', ), ).toHaveLength(audioManifestReadCountBefore); for (const [buttonName, draftValue] of [ ['读入口', '/read game/index.html'], ['读设计', '/read game/game_design.md'], ['读数值', '/read game/balance.json'], ['读美术清单', '/read assets/manifest.art.json'], ['读音频清单', '/read assets/manifest.audio.json'], ['读发布说明', '/read exports/README.md'], ] as const) { fireEvent.click( within(screen.getByLabelText('预览快捷操作')).getByRole('button', { name: buttonName, }), ); expect(composerInput).toHaveProperty('value', draftValue); await waitFor(() => expect(document.activeElement).toBe(composerInput)); } submitChat('/artifacts'); expect(await screen.findByText(/常用生成产物:/)).not.toBeNull(); expect(screen.getByText(/读入口 · game\/index\.html/)).not.toBeNull(); expect(screen.getByText(/读发布说明 · exports\/README\.md/)).not.toBeNull(); const readEntryButtons = screen.getAllByRole('button', { name: '读入口' }); fireEvent.click(readEntryButtons[readEntryButtons.length - 1]); expect(composerInput).toHaveProperty('value', '/read game/index.html'); await waitFor(() => expect(document.activeElement).toBe(composerInput)); submitChat('/run-artifacts'); expect( await screen.findByText( /最近 Run 产物读取命令:[\s\S]*exports\/README\.md · 128B · fnv1a64:exports:\/read exports\/README\.md/, ), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '读取首个 Run 产物' })); expect(composerInput).toHaveProperty('value', '/read exports/README.md'); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.objectContaining({ relativePath: 'exports/README.md', }), ); const fileReadCountBeforePasses = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; submitChat('/passes'); const passArtifactMessages = await screen.findAllByText(/Agent 轮次产物读取命令:/); const passArtifactMessage = passArtifactMessages[passArtifactMessages.length - 1]; expect(passArtifactMessage.textContent).toContain( '.agent/passes/pass-1/agenda.md · 96B · fnv1a64:agenda', ); expect(passArtifactMessage.textContent).toContain( '.agent/passes/pass-1/groups/art/asset.md · 192B · fnv1a64:art-asset', ); const passArtifactButtons = screen.getAllByRole('button', { name: '读取首个轮次产物', }); fireEvent.click(passArtifactButtons[passArtifactButtons.length - 1]); expect(composerInput).toHaveProperty( 'value', '/read .agent/passes/pass-1/agenda.md', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(fileReadCountBeforePasses); submitChat('/logs'); expect(await screen.findByText(/常用日志读取命令:/)).not.toBeNull(); expect( screen.getByText(/读命令日志 · \.agent\/logs\/command\.log/), ).not.toBeNull(); expect( screen.getByText(/读 Agent 日志 · \.agent\/logs\/agent\.log/), ).not.toBeNull(); const commandLogReadCountBefore = invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === '.agent/logs/command.log', ).length; const messageList = document.querySelector('.message-list'); expect(messageList).not.toBeNull(); fireEvent.click( within(messageList as HTMLElement).getByRole('button', { name: '读命令日志', }), ); expect(composerInput).toHaveProperty( 'value', '/read .agent/logs/command.log', ); expect( invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === '.agent/logs/command.log', ), ).toHaveLength(commandLogReadCountBefore); fireEvent.click(screen.getByRole('button', { name: '打开画板' })); expect(composerInput).toHaveProperty('value', '/canvas '); await waitFor(() => expect(document.activeElement).toBe(composerInput)); fireEvent.click(screen.getByRole('button', { name: '同步画板' })); expect(composerInput).toHaveProperty('value', '/sync-canvas-project '); await waitFor(() => expect(document.activeElement).toBe(composerInput)); fireEvent.click(screen.getByRole('button', { name: '生成美术' })); expect(composerInput).toHaveProperty('value', '/generate-art '); await waitFor(() => expect(document.activeElement).toBe(composerInput)); fireEvent.click( within(screen.getByLabelText('预览快捷操作')).getByRole('button', { name: '登记音效', }), ); expect(composerInput).toHaveProperty( 'value', '/asset-register assets/audio/sfx.wav audio audio/wav', ); await waitFor(() => expect(document.activeElement).toBe(composerInput)); fireEvent.click(screen.getByRole('button', { name: '导入画板资产' })); expect(composerInput).toHaveProperty('value', '/import-canvas-asset '); await waitFor(() => expect(document.activeElement).toBe(composerInput)); fireEvent.click(screen.getByRole('button', { name: '导入画板音频' })); expect(composerInput).toHaveProperty( 'value', '/import-canvas-asset assets/audio/sfx.wav ', ); await waitFor(() => expect(document.activeElement).toBe(composerInput)); fireEvent.click( within(screen.getByLabelText('最近项目资产')).getByRole('button', { name: 'assets/uploads/hero.txt', }), ); expect( await screen.findByText(/文件:assets\/uploads\/hero\.txt/), ).not.toBeNull(); fireEvent.click( within(screen.getByLabelText('最近项目资产')).getByRole('button', { name: '填入读取 assets/uploads/hero.txt', }), ); expect(composerInput).toHaveProperty( 'value', '/read assets/uploads/hero.txt', ); fireEvent.click(screen.getByRole('button', { name: '任务' })); expect(await screen.findByText(/任务拆分:/)).not.toBeNull(); expect(screen.getByText(/下一步:策划组 \/ Director/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: 'Trace' })); expect( await screen.findByText( (content) => content.trimStart().startsWith('Run:run-main-shortcut-trace') && content.includes('产物快照:'), ), ).not.toBeNull(); expect(screen.getByText(/产物快照:/)).not.toBeNull(); expect( screen.getByText(/exports\/README\.md · fnv1a64:exports/), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '读取首个产物' })); expect(composerInput).toHaveProperty('value', '/read exports/README.md'); expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: 'exports/README.md', commandId: 'file.read', }); fireEvent.click(screen.getByRole('button', { name: '文件' })); expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); fireEvent.click( within(screen.getByLabelText('最近项目文件')).getByRole('button', { name: 'game/index.html', }), ); expect(await screen.findByText(/文件:game\/index\.html/)).not.toBeNull(); fireEvent.click( within(screen.getByLabelText('最近项目文件')).getByRole('button', { name: '填入读取 game/index.html', }), ); expect(composerInput).toHaveProperty('value', '/read game/index.html'); fireEvent.click( within(screen.getByLabelText('最近项目文件')).getByRole('button', { name: '登记资产 game/index.html', }), ); expect(composerInput).toHaveProperty( 'value', '/asset-register game/index.html document text/html', ); expect(invoke).not.toHaveBeenCalledWith( 'register_local_asset', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '索引' })); expect(await screen.findByText(/索引:2 个文件,384B/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '记忆' })); expect(await screen.findByText(/长期记忆:/)).not.toBeNull(); expect(screen.getByText(/保留厨房主题/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '短期记忆' })); expect(await screen.findByText(/短期记忆:/)).not.toBeNull(); expect(screen.getByText(/本轮偏动作反馈/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '黑板' })); expect(await screen.findByText(/黑板记忆:/)).not.toBeNull(); expect(screen.getByText(/跨 agent 共享约束/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '记到黑板' })); expect(composerInput).toHaveProperty('value', '/remember blackboard '); fireEvent.click(screen.getByRole('button', { name: '覆盖黑板' })); expect(composerInput).toHaveProperty('value', '/memory-set blackboard '); fireEvent.click(screen.getByRole('button', { name: '清空黑板' })); expect(composerInput).toHaveProperty('value', '/forget-memory blackboard'); fireEvent.click(screen.getByRole('button', { name: '快照' })); expect(await screen.findByText('project.checkpoint')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText(/已保存 checkpoint:checkpoint-main/), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '快照列表' })); const checkpointList = await screen.findByLabelText('最近 checkpoint'); expect(within(checkpointList).getByText('checkpoint-main')).not.toBeNull(); expect(within(checkpointList).getByText(/3 个文件 · 256B/)).not.toBeNull(); expect( within(checkpointList).getByText(/createdAt 1700000002/), ).not.toBeNull(); expect( within(checkpointList).getByRole('button', { name: '对比 checkpoint-main', }), ).not.toBeNull(); fireEvent.click( within(checkpointList).getByRole('button', { name: '填入对比 checkpoint-main', }), ); expect(composerInput).toHaveProperty('value', '/diff checkpoint-main'); fireEvent.click( within(checkpointList).getByRole('button', { name: '填入回滚 checkpoint-main', }), ); expect(composerInput).toHaveProperty('value', '/restore checkpoint-main'); fireEvent.click( within(checkpointList).getByRole('button', { name: '回滚 checkpoint-main', }), ); expect(await screen.findByText(/project\.restore/)).not.toBeNull(); expect( screen.getByText( '从 checkpoint-main 恢复 /tmp/authorized-game 的已跟踪项目文件', ), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '取消' })); fireEvent.click(screen.getByRole('button', { name: '白名单' })); expect(await screen.findByText(/可运行受限命令:/)).not.toBeNull(); expect( screen.getByText(/game\.static_smoke · 静态入口自检/), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '静态自检' })); expect(await screen.findByText('准备运行静态入口自检。')).not.toBeNull(); expect(screen.getByText(/command\.run_limited/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText(/static smoke passed/)).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '运行' })); expect(await screen.findByText('game.run_local')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText( /运行通过,预览已启动:http:\/\/127\.0\.0\.1:3210\//, ), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '导出' })); expect(await screen.findByText('project.export_package')).not.toBeNull(); expect( screen.getByText( '从 /tmp/authorized-game/game、assets 和 exports/README.md 导出本地试玩包', ), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText( /已导出本地试玩包:exports\/playtest-package-unit\.zip/, ), ).not.toBeNull(); const exportRevealButtons = screen.getAllByRole('button', { name: '显示目录', }); fireEvent.click(exportRevealButtons[exportRevealButtons.length - 1]); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/open-project', ); submitChat('/export'); await waitFor(() => expect( screen.getAllByText('准备导出本地试玩包。').length, ).toBeGreaterThan(1), ); expect( screen.getAllByText('project.export_package').length, ).toBeGreaterThan(0); fireEvent.click(screen.getByRole('button', { name: '取消' })); submitChat('/exports'); expect(await screen.findByText(/本地试玩包:/)).not.toBeNull(); expect( screen.getByText(/exports\/playtest-package-002\.zip · 2048B/), ).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', }); const exportListRevealButtons = screen.getAllByRole('button', { name: '显示目录', }); fireEvent.click( exportListRevealButtons[exportListRevealButtons.length - 1], ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/open-project', ); fireEvent.click(screen.getByRole('button', { name: '启动预览' })); expect(await screen.findByText('preview.start')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText(/预览已启动:http:\/\/127\.0\.0\.1:3210\//), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '打开预览' })); expect(await screen.findByText('preview.open')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开当前本地预览:http://127.0.0.1:3210/'), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '预览状态' })); expect( await screen.findByText('预览运行中:http://127.0.0.1:3210/'), ).not.toBeNull(); expect(screen.getByText('preview: 运行中:127.0.0.1:3210')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '停止预览' })); expect(await screen.findByText('预览已停止。')).not.toBeNull(); expect(screen.getByText('preview: 已停止')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('run_limited_local_command', { projectPath: '/tmp/authorized-game', commandId: 'game.static_smoke', }); expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { projectPath: '/tmp/authorized-game', commandId: 'asset.list', }); expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { projectPath: '/tmp/authorized-game', commandId: 'task.list', }); expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { projectPath: '/tmp/authorized-game', commandId: 'agent.audit', }); expect(invoke).toHaveBeenCalledWith('get_game_creation_agent_capabilities'); expect(invoke).toHaveBeenCalledWith('list_local_project_files', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('build_local_project_index', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { projectPath: '/tmp/authorized-game', scope: 'long', }); expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { projectPath: '/tmp/authorized-game', scope: 'short', }); expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { projectPath: '/tmp/authorized-game', scope: 'blackboard', }); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/run.latest.json', commandId: 'agent.trace_read', }); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: 'game/index.html', commandId: 'file.read', }); expect(invoke).toHaveBeenCalledWith('create_local_project_checkpoint', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('export_local_project_package', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('open_local_game_preview', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('stop_local_game_preview', { projectPath: '/tmp/authorized-game', }); }, 10000); it('shows and refreshes the current project run status in the main window header', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let runReadCount = 0; const makeTrace = ( status: string, passes: number, stopReason: string, lifecycleStatus?: string, ) => ({ schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-header-status', commandId: 'game.generate_draft', status, lifecycleStatus, passes, maxPasses: 3, toolCallCount: 0, maxToolCalls: 128, stopReason, goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [], artifacts: Array.from({ length: 9 }, (_, index) => ({ path: `exports/artifact-${index + 1}.json`, sizeBytes: index + 1, checksum: `fnv1a64:artifact-${index + 1}`, })), taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: ['preview-playtest'], activeTaskIds: ['code-prototype'], carriedTaskIds: ['design-director'], repairFocus: [], repairRoutes: [ { issue: '缺少输入监听', taskIds: ['code-prototype', 'quality-review'], reason: 'code-runtime', }, ], tasks: createGameCreationAppSeedTasks(), }, passPlans: [ { pass: 1, mode: 'repair', summary: '继续程序返工', activeTaskIds: ['code-prototype', 'quality-review'], carriedTaskIds: ['design-director'], dependencyWaves: [['code-prototype'], ['quality-review']], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', taskIds: ['code-prototype', 'quality-review'], reason: 'code-runtime', }, ], }, ], nextStep: 'preview', error: null, updatedAt: runReadCount, }) satisfies GameCreationAgentRunTrace; 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 === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'read_local_project_file') { runReadCount += 1; return { path: '.agent/run.latest.json', absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, content: JSON.stringify( runReadCount > 1 ? makeTrace('passed', 2, 'evaluator-passed') : makeTrace('running', 1, 'planning', 'pending'), ), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); expect( await screen.findByText('run: running / pending · 1/3 轮 · planning'), ).not.toBeNull(); expect(screen.getByLabelText('Agent task graph').textContent).toContain( 'active: 程序组 / Code 生成可运行原型(code-prototype)', ); expect(screen.getByLabelText('Agent task graph').textContent).toContain( 'carry-over: 策划组 / Director 拆解创作方向(design-director)', ); expect(screen.getByLabelText('Agent task graph').textContent).toContain( 'ready: 程序组 / Playtest 预览并试玩验收(preview-playtest)', ); expect(screen.getByLabelText('Agent task graph').textContent).toContain( 'route: 程序组 / Code 生成可运行原型(code-prototype), 程序组 / Review 执行质量评审(quality-review) · code-runtime', ); expect(screen.getByLabelText('Agent pass plans').textContent).toContain( 'pass 1: repair · active 程序组 / Code 生成可运行原型(code-prototype), 程序组 / Review 执行质量评审(quality-review) · carry 策划组 / Director 拆解创作方向(design-director) · waves 程序组 / Code 生成可运行原型(code-prototype) / 程序组 / Review 执行质量评审(quality-review) · repair 缺少输入监听 · routes code-runtime: 程序组 / Code 生成可运行原型(code-prototype), 程序组 / Review 执行质量评审(quality-review)', ); expect(screen.getByLabelText('Agent artifacts').textContent).toContain( 'exports/artifact-8.json', ); expect(screen.getByLabelText('Agent artifacts').textContent).not.toContain( 'exports/artifact-9.json', ); expect(screen.getByLabelText('Agent artifacts').textContent).toContain( '还有 1 个产物', ); fireEvent.click( within(screen.getByLabelText('Agent artifacts')).getByRole('button', { name: '填入读取产物 exports/artifact-1.json', }), ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read exports/artifact-1.json', ); fireEvent.click(screen.getByRole('button', { name: '刷新状态' })); expect( await screen.findByText('run: passed · 2/3 轮 · evaluator-passed'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/run.latest.json', commandId: 'agent.trace_read', }); }); it('requires project policy confirmation before refreshing trace from the panel', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-trace-panel-confirm', commandId: 'game.generate_draft', status: 'running', lifecycleStatus: 'running', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'planning', goal: '做一个厨房弹幕游戏', coordination: 'filesystem', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'planner', 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_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } 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), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); expect( await screen.findByText('run: running / running · 1/3 轮 · planning'), ).not.toBeNull(); invoke.mockClear(); fireEvent.click( within(screen.getByLabelText('Agent run trace')).getByRole('button', { name: '刷新', }), ); expect(await screen.findByText('agent.trace_read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); 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('normalizes older run traces before rendering trace panels', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const legacyTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-legacy-trace', commandId: 'game.generate_draft', status: 'running', passes: 1, toolCallCount: 0, stopReason: 'planning', goal: '做一个厨房弹幕游戏', coordination: 'legacy', steps: [ { pass: 1, agent: 'Generator', phase: 'generate', taskId: 'code-prototype', group: 'code', role: 'Code', status: 'completed', summary: '旧 trace 没有路径和工具数组', }, ], taskGraph: { goal: '做一个厨房弹幕游戏', activeTaskIds: ['code-prototype'], tasks: createGameCreationAppSeedTasks(), }, passPlans: [ { pass: 1, mode: 'repair', summary: '旧 pass plan', activeTaskIds: ['code-prototype'], }, ], nextStep: 'continue', error: null, updatedAt: 1, }; 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_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') { return { path: '.agent/run.latest.json', absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, content: JSON.stringify(legacyTrace), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); expect( await screen.findByText('run: running · 1/3 轮 · planning'), ).not.toBeNull(); expect(screen.getByLabelText('Agent task graph').textContent).toContain( 'active: 程序组 / Code 生成可运行原型(code-prototype)', ); expect(screen.getByLabelText('Agent task graph').textContent).toContain( 'carry-over: none', ); expect(screen.getByLabelText('Agent run trace').textContent).toContain( 'generate · 程序组 / Code 生成可运行原型(code-prototype)', ); expect(screen.getByLabelText('Agent pass plans').textContent).toContain( 'pass 1: repair · active 程序组 / Code 生成可运行原型(code-prototype) · carry none · waves none', ); }); it('reports malformed optional trace arrays before rendering panels', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const malformedTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-malformed-trace', commandId: 'game.generate_draft', status: 'running', passes: 1, goal: '做一个厨房弹幕游戏', coordination: 'legacy', steps: [ { pass: 1, agent: 'Generator', phase: 'generate', taskId: 'code-prototype', group: 'code', role: 'Code', status: 'completed', summary: '坏 trace', toolCalls: 'bad', }, ], nextStep: 'continue', 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_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'read_local_project_file') { return { path: '.agent/run.latest.json', absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, content: JSON.stringify(malformedTrace), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); expect( await screen.findByText('Agent run trace 格式不正确'), ).not.toBeNull(); }); it('treats a missing latest run trace as no recent 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_local_conversation') { return { path: '/tmp/new-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'read_local_project_file') { throw new Error( `读取文件元数据失败:${String( args?.projectPath ?? '', )}/.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('/?main&projectPath=%2Ftmp%2Fnew-game'); expect( await screen.findByText('run: 还没有最近一次 Agent run'), ).not.toBeNull(); }); it('loads recent run history in the developer project window', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const makeTrace = ( runId: string, status: string, passes: number, stopReason: string, lifecycleStatus?: string, updatedAt = passes, ) => ({ schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId, commandId: 'game.generate_draft', status, lifecycleStatus, passes, maxPasses: 3, toolCallCount: 0, maxToolCalls: 128, stopReason, goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', error: null, updatedAt, }) satisfies GameCreationAgentRunTrace; const traces = new Map([ [ '.agent/run.latest.json', makeTrace('run-current', 'running', 1, 'planning'), ], [ '.agent/runs/2026-07-02-new.json', makeTrace('run-new', 'passed', 2, 'evaluator-passed', 'done', 600), ], [ '.agent/runs/2026-07-01-old.json', makeTrace( 'run-old', 'failed', 3, 'max-passes-exhausted', undefined, 700, ), ], [ '.agent/runs/2026-06-30-mid.json', makeTrace('run-mid', 'running', 1, 'planning', undefined, 500), ], [ '.agent/runs/2026-06-29-four.json', makeTrace( 'run-four', 'failed', 3, 'max-passes-exhausted', undefined, 400, ), ], [ '.agent/runs/2026-06-28-five.json', makeTrace('run-five', 'passed', 1, 'preview', 'done', 300), ], [ '.agent/runs/2026-06-27-hidden.json', makeTrace( 'run-hidden', 'failed', 3, 'max-passes-exhausted', undefined, 200, ), ], [ '.agent/runs/2026-06-26-hidden.json', makeTrace( 'run-older-hidden', 'failed', 3, 'max-passes-exhausted', undefined, 100, ), ], ]); 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_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/runs/2026-07-02-new.json', kind: 'file', size: 120, }, { path: '.agent/runs/2026-07-01-old.json', kind: 'file', size: 110, }, { path: '.agent/runs/2026-06-30-mid.json', kind: 'file', size: 100, }, { path: '.agent/runs/2026-06-29-four.json', kind: 'file', size: 90, }, { path: '.agent/runs/2026-06-28-five.json', kind: 'file', size: 80, }, { path: '.agent/runs/2026-06-27-hidden.json', kind: 'file', size: 70, }, { path: '.agent/runs/2026-06-26-hidden.json', kind: 'file', size: 60, }, ], }; } if (command === 'read_local_project_file') { const trace = traces.get(String(args?.relativePath ?? '')); if (!trace) { throw new Error( `missing trace ${String(args?.relativePath ?? '')}`, ); } return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(trace), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); const runHistory = await screen.findByLabelText('Agent run history'); expect(screen.getByText(/run-new/)).not.toBeNull(); expect( screen.getByText(/passed \/ done · 2\/3 轮 · evaluator-passed/), ).not.toBeNull(); const oldRunButton = within(runHistory).getByText( 'run-old · failed · 3/3 轮 · max-passes-exhausted · updated: 700 · .agent/runs/2026-07-01-old.json · 110B', ); expect(oldRunButton).not.toBeNull(); expect(screen.getByText(/updated: 700/)).not.toBeNull(); expect( screen.getByText(/\.agent\/runs\/2026-07-01-old\.json/), ).not.toBeNull(); expect(screen.queryByText('还有 2 个历史 run')).toBeNull(); expect(screen.getByText(/run-hidden/)).not.toBeNull(); expect(runHistory.querySelector('button')?.textContent).toContain( 'run-old', ); expect(runHistory.querySelector('[aria-current="true"]')).toBeNull(); fireEvent.click(oldRunButton.closest('button') as Element); expect( await screen.findByText('run: failed · 3/3 轮 · max-passes-exhausted'), ).not.toBeNull(); expect(oldRunButton.closest('button')?.getAttribute('aria-current')).toBe( 'true', ); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/runs/2026-07-01-old.json', commandId: 'agent.trace_read', }); }); it('uses run history for agent status cards when the latest trace is missing', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-history-only', commandId: 'game.generate_draft', status: 'running', lifecycleStatus: 'running', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'planning', goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [ { pass: 1, agent: 'Generator', phase: 'generate', taskId: 'code-prototype', group: 'code', role: 'Code', status: 'running', inputPaths: ['.agent/spec.md'], outputPaths: ['game/index.html'], summary: 'Generator 历史草案生成', toolCalls: [], }, ], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: ['code-prototype'], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'generator', 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_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/runs/run-history-only.json', kind: 'file', size: 120, }, ], }; } if (command === 'read_local_project_file') { const relativePath = String(args?.relativePath ?? ''); if (relativePath === '.agent/run.latest.json') { throw new Error( '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', ); } return { path: relativePath, absolutePath: `/tmp/authorized-game/${relativePath}`, content: JSON.stringify(trace), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); const runHistory = await screen.findByLabelText('Agent run history'); expect(within(runHistory).getByText(/run-history-only/)).not.toBeNull(); expect(screen.getByText('Generator 历史草案生成')).not.toBeNull(); }); it('keeps run history out of the regular project chat window', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-hidden-from-chat', commandId: 'game.generate_draft', status: 'passed', lifecycleStatus: 'done', passes: 1, maxPasses: 3, toolCallCount: 0, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', error: null, updatedAt: 1, } satisfies GameCreationAgentRunTrace; 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 === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/runs/run-hidden-from-chat.json', kind: 'file', size: 100, }, ], }; } if (command === 'read_local_project_file') { return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(trace), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('list_local_project_files', { projectPath: '/tmp/authorized-game', }); }); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/runs/run-hidden-from-chat.json', commandId: 'agent.trace_read', }); }); expect(screen.queryByLabelText('最近 run')).toBeNull(); expect(screen.queryByText(/run-hidden-from-chat/)).toBeNull(); const runReadCountBeforeRunsCommand = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ).length; submitChat('/runs'); expect(await screen.findByText(/Run 历史读取命令:/)).not.toBeNull(); expect( screen.getByText( /run-hidden-from-chat .* \.agent\/runs\/run-hidden-from-chat\.json .*:\/read \.agent\/runs\/run-hidden-from-chat\.json/, ), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '读取首个历史 Run' })); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/runs/run-hidden-from-chat.json', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', ), ).toHaveLength(runReadCountBeforeRunsCommand); }); it('confirms before opening a run history trace when policy requires it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const makeTrace = (runId: string, status: string, updatedAt: number) => ({ schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId, commandId: 'game.generate_draft', status, lifecycleStatus: status === 'passed' ? 'done' : undefined, passes: 1, maxPasses: 3, toolCallCount: 0, maxToolCalls: 128, stopReason: status === 'passed' ? 'evaluator-passed' : 'planning', goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', error: null, updatedAt, }) satisfies GameCreationAgentRunTrace; const traces = new Map([ ['.agent/run.latest.json', makeTrace('run-current', 'running', 500)], ['.agent/runs/2026-07-01-old.json', makeTrace('run-old', 'passed', 400)], ]); 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_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: ['agent.trace_read'], }, }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/runs/2026-07-01-old.json', kind: 'file', size: 110, }, ], }; } if (command === 'read_local_project_file') { const trace = traces.get(String(args?.relativePath ?? '')); if (!trace) { throw new Error( `missing trace ${String(args?.relativePath ?? '')}`, ); } return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(trace), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText(/run-old/)).not.toBeNull(); invoke.mockClear(); fireEvent.click(screen.getByText(/run-old/).closest('button') as Element); expect(await screen.findByText('agent.trace_read')).not.toBeNull(); expect( screen.getByText( '读取 /tmp/authorized-game 的 .agent/runs/2026-07-01-old.json', ), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/runs/2026-07-01-old.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/runs/2026-07-01-old.json', commandId: 'agent.trace_read', }); }); }); it('shows more run history entries on demand', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const makeTrace = (runId: string, updatedAt: number) => ({ schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId, commandId: 'game.generate_draft', status: 'passed', lifecycleStatus: 'done', passes: 1, maxPasses: 3, toolCallCount: 0, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', error: null, updatedAt, }) satisfies GameCreationAgentRunTrace; const runFiles = Array.from({ length: 22 }, (_, index) => { const number = String(index + 1).padStart(2, '0'); return { path: `.agent/runs/run-${number}.json`, kind: 'file', size: 100 + index, }; }); const traces = new Map([ ['.agent/run.latest.json', makeTrace('run-current', 1000)], ...runFiles.map( (file, index) => [ file.path, makeTrace(`run-${String(index + 1).padStart(2, '0')}`, index + 1), ] as const, ), ]); 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 === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: runFiles, }; } if (command === 'read_local_project_file') { const trace = traces.get(String(args?.relativePath ?? '')); if (!trace) { throw new Error( `missing trace ${String(args?.relativePath ?? '')}`, ); } return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(trace), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); const runHistory = await screen.findByLabelText('Agent run history'); expect(screen.getByText(/run-03/)).not.toBeNull(); expect(screen.queryByText(/run-02/)).toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/runs/run-02.json', }); expect( screen.getByRole('button', { name: '显示更多 · 还有 2 个历史 run', }), ).not.toBeNull(); fireEvent.scroll(runHistory); expect(await screen.findByText(/run-02/)).not.toBeNull(); expect(screen.getByText(/run-01/)).not.toBeNull(); expect(runHistory.querySelector('button')?.textContent).toContain('run-22'); expect( screen.queryByRole('button', { name: /显示更多/, }), ).toBeNull(); }); it('loads the first run history page by file modified time before reading traces', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const makeTrace = (runId: string, updatedAt: number) => ({ schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId, commandId: 'game.generate_draft', status: 'passed', lifecycleStatus: 'done', passes: 1, maxPasses: 3, toolCallCount: 0, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个厨房弹幕游戏', coordination: 'Planner -> Generator', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', error: null, updatedAt, }) satisfies GameCreationAgentRunTrace; const runFiles = [ { path: '.agent/runs/000-latest.json', kind: 'file', size: 200, modifiedAt: 10_000, }, ...Array.from({ length: 20 }, (_, index) => { const number = String(index + 1).padStart(2, '0'); return { path: `.agent/runs/z-${number}.json`, kind: 'file', size: 100 + index, modifiedAt: index + 1, }; }), ]; const traces = new Map([ ['.agent/run.latest.json', makeTrace('run-current', 20_000)], ['.agent/runs/000-latest.json', makeTrace('run-latest', 10_000)], ...runFiles .slice(1) .map( (file, index) => [ file.path, makeTrace(`run-${String(index + 1).padStart(2, '0')}`, index + 1), ] as const, ), ]); 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 === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: runFiles, }; } if (command === 'read_local_project_file') { const trace = traces.get(String(args?.relativePath ?? '')); if (!trace) { throw new Error( `missing trace ${String(args?.relativePath ?? '')}`, ); } return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(trace), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByLabelText('Agent run history'); expect(screen.getByText(/run-latest/)).not.toBeNull(); expect(screen.queryByText(/run-01/)).toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/runs/z-01.json', }); }); it('loads project conversation history in 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 === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '历史需求:做一个厨房弹幕游戏', agentId: null, updatedAt: 1, }, { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '历史回复:已生成第一版', agentId: null, updatedAt: 2, }, ], }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect( await screen.findByText('历史需求:做一个厨房弹幕游戏'), ).not.toBeNull(); expect(screen.getByText('历史回复:已生成第一版')).not.toBeNull(); expect(screen.queryByLabelText('工作区管理')).toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: null, }); expect(window.localStorage.length).toBe(1); expect( window.localStorage.getItem(window.localStorage.key(0) ?? ''), ).toContain('/tmp/authorized-game'); }); it('loads project conversation history after opening from chat command', 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_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '历史需求:保留弹幕厨房', agentId: null, updatedAt: 1, }, { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '历史回复:继续做第二版', agentId: null, updatedAt: 2, }, ], }; } 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('历史需求:保留弹幕厨房')).not.toBeNull(); expect(screen.getByText('历史回复:继续做第二版')).not.toBeNull(); expect( screen.queryByText('已设置本地项目:/tmp/authorized-game'), ).toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: null, }); expect(invoke).not.toHaveBeenCalledWith( 'append_local_conversation_message', expect.anything(), ); }); it('reloads project conversation history from chat on demand', 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: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '重载历史需求', agentId: null, updatedAt: 1, }, { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '重载历史回复', agentId: null, updatedAt: 2, }, ], }; } 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('重载历史需求')).not.toBeNull(); submitChat('临时未保存的输入'); expect(await screen.findByText('临时未保存的输入')).not.toBeNull(); submitChat('/history'); expect(await screen.findByText('重载历史回复')).not.toBeNull(); expect(screen.queryByText('临时未保存的输入')).toBeNull(); expect(screen.getByText('已读取项目对话历史:2 条')).not.toBeNull(); submitChat('另一条临时未保存的输入'); expect(await screen.findByText('另一条临时未保存的输入')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '历史' })); expect(await screen.findByText('重载历史回复')).not.toBeNull(); expect(screen.queryByText('另一条临时未保存的输入')).toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: null, }); }); 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('/'); 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', }); }); 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('confirms before loading project conversation history when policy requires 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: ['conversation.read'], }, }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '受保护历史需求', agentId: null, updatedAt: 1, }, ], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('想做什么游戏?')).not.toBeNull(); expect(screen.queryByText('受保护历史需求')).toBeNull(); expect(await screen.findByText('conversation.read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: null, }); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('受保护历史需求')).not.toBeNull(); expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: null, }); }); it('keeps the project chat usable after cancelling conversation history read', 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: ['conversation.read'], }, }; } if (command === 'read_local_conversation') { throw new Error('should wait for conversation confirmation'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('想做什么游戏?')).not.toBeNull(); const conversationReadCommand = await screen.findByText('conversation.read'); fireEvent.click( within( conversationReadCommand.closest('.pending-command') as HTMLElement, ).getByRole('button', { name: '取消' }), ); expect(await screen.findByText('已取消读取项目对话')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: null, }); }); it('reports conversation history read failure 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: ['conversation.read'], }, }; } if (command === 'read_local_conversation') { throw new Error('conversation read failed'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('想做什么游戏?')).not.toBeNull(); expect(await screen.findByText('conversation.read')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('项目对话读取失败:conversation read failed'), ).not.toBeNull(); expect(screen.getByText('想做什么游戏?')).not.toBeNull(); }); it('shows project conversation history in recent batches', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const messages = Array.from({ length: 25 }, (_, index) => ({ schemaVersion: 'game-creator-conversation.v1', role: index % 2 === 0 ? ('user' as const) : ('assistant' as const), content: `历史对话 ${String(index + 1).padStart(2, '0')}`, agentId: null, updatedAt: 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 emptyProjectPolicy(); } 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: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('历史对话 06')).not.toBeNull(); expect(screen.getByText('历史对话 25')).not.toBeNull(); expect(screen.queryByText('历史对话 05')).toBeNull(); fireEvent.click( screen.getByRole('button', { name: '显示更早 · 还有 5 条对话', }), ); expect(screen.getByText('历史对话 01')).not.toBeNull(); expect(screen.getByText('历史对话 05')).not.toBeNull(); expect(screen.queryByRole('button', { name: /显示更早/ })).toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'append_local_conversation_message', expect.anything(), ); }); it('retries unsaved project 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 === '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 screen.findByText('想做什么游戏?'); submitChat('第一条创作需求'); await waitFor(() => { expect(appendAttempts).toBe(1); }); expect( await screen.findByText( '项目对话保存失败:conversation append failed once', ), ).not.toBeNull(); submitChat('第二条创作需求'); await waitFor(() => { expect(releaseRetryAppend).not.toBeNull(); }); await act(async () => { releaseRetryAppend?.(); }); await waitFor(() => { expect(savedContents).toEqual([ '第一条创作需求', '主聊天回复:第一条创作需求', '第二条创作需求', '主聊天回复:第二条创作需求', ]); }); expect( screen.queryByText('项目对话保存失败:conversation append failed once'), ).toBeNull(); expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull(); }); it('confirms before saving project 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'); await screen.findByText('想做什么游戏?'); invoke.mockClear(); 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([ '需要确认保存的需求', '主聊天回复:需要确认保存的需求', ]); }); }); it('does not immediately re-prompt after cancelling project 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'); await screen.findByText('想做什么游戏?'); invoke.mockClear(); 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('已打开:/tmp/authorized-game')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'append_local_conversation_message', expect.anything(), ); 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([ '先不保存的需求', '主聊天回复:先不保存的需求', '继续补充一条', '主聊天回复:继续补充一条', ]); }); }); it('persists project 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 screen.findByText('想做什么游戏?'); submitChat('第一条创作需求'); await waitFor(() => { expect(releaseFirstAppend).not.toBeNull(); }); submitChat('第二条创作需求'); releaseFirstAppend?.(); await waitFor(() => { expect(savedContents).toEqual([ '第一条创作需求', '主聊天回复:第一条创作需求', '第二条创作需求', '主聊天回复:第二条创作需求', ]); }); }); 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 === '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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/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( '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', ), ).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: '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', agentId: null, }, }); 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, }); fireEvent.click(within(agentDialog).getByRole('button', { name: '刷新' })); expect(await screen.findByText('刷新后外部记录')).not.toBeNull(); expect(agentConversationReadCount).toBeGreaterThanOrEqual(2); fireEvent.click(screen.getByRole('button', { name: '填入同步命令' })); expect(screen.queryByLabelText('Agent 对话')).toBeNull(); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/spec.md', ); expect(invoke).not.toHaveBeenCalledWith( 'sync_canvas_project_assets', expect.anything(), ); }); it('requires confirmation before reading a specific agent conversation when 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: ['conversation.read'], }, }; } if (command === 'read_local_conversation') { 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: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); expect(await screen.findByText('准备读取 Agent 对话。')).not.toBeNull(); expect(screen.getByText('conversation.read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: 'design-director', }); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText( '已读取 0 条:/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', ), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: 'design-director', }); }); it('leaves the agent conversation panel usable after cancelling read 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: ['conversation.read'], }, }; } if (command === 'read_local_conversation') { 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: [], }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const conversationReadCommand = await screen.findByText('conversation.read'); fireEvent.click( within( conversationReadCommand.closest('.pending-command') as HTMLElement, ).getByRole('button', { name: '取消' }), ); await waitFor(() => { expect(screen.queryByText('conversation.read')).toBeNull(); }); expect(screen.getAllByText('已取消读取 Agent 对话').length).toBeGreaterThan( 0, ); expect(screen.getByText('暂无对话')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: 'design-director', }); }); it('keeps loaded agent conversation after cancelling private memory read 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: ['memory.read'], }, }; } if (command === 'read_local_conversation') { 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 ? [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '已读到 Agent 对话', agentId: 'design-director', updatedAt: 1, }, ] : [], }; } if (command === 'read_local_agent_memory') { throw new Error('should wait for memory confirmation'); } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); expect(await screen.findByText('已读到 Agent 对话')).not.toBeNull(); const memoryReadCommand = await screen.findByText('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('已读到 Agent 对话')).not.toBeNull(); expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain( '已取消读取 Agent 私有记忆', ); expect(invoke).not.toHaveBeenCalledWith('read_local_agent_memory', { projectPath: '/tmp/authorized-game', taskId: 'design-director', }); }); it('requires confirmation before writing a specific agent conversation when policy asks for it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const agentMessages: Array<{ schemaVersion: string; role: 'user' | 'assistant'; content: string; agentId: string | null; updatedAt: number; }> = []; 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 === 'read_local_conversation') { 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 : [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } 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: message.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 : [], }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('已打开:/tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const input = await screen.findByLabelText('Agent 对话内容'); invoke.mockClear(); fireEvent.change(input, { target: { value: '先记住这个方向' } }); fireEvent.submit(input.closest('form') as HTMLFormElement); expect(await screen.findByText('准备保存 Agent 对话。')).not.toBeNull(); expect(screen.getByText('conversation.write')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'append_local_conversation_message', expect.objectContaining({ agentId: 'design-director' }), ); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText(/已保存 2 条/)).not.toBeNull(); expect(screen.getByText('先记住这个方向')).not.toBeNull(); expect( screen.getByText( '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', ), ).not.toBeNull(); 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: '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', agentId: null, }, }); }); it('shows agent conversation history in recent batches', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const agentMessages = Array.from({ length: 25 }, (_, index) => ({ schemaVersion: 'game-creator-conversation.v1', role: index % 2 === 0 ? ('user' as const) : ('assistant' as const), content: `Agent 历史 ${String(index + 1).padStart(2, '0')}`, agentId: 'design-director', updatedAt: 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 emptyProjectPolicy(); } if (command === 'read_local_conversation') { 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 : [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('/tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const agentDialog = await screen.findByLabelText('Agent 对话'); expect(agentDialog.textContent).toContain('Agent 历史 06'); expect(agentDialog.textContent).toContain('Agent 历史 25'); expect(agentDialog.textContent).not.toContain('Agent 历史 05'); fireEvent.click( screen.getByRole('button', { name: '显示更早 · 还有 5 条对话', }), ); expect(agentDialog.textContent).toContain('Agent 历史 01'); expect(agentDialog.textContent).toContain('Agent 历史 05'); }); it('does not submit duplicate agent messages while a save is running', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const agentMessages: Array<{ schemaVersion: string; role: 'user' | 'assistant'; content: string; agentId: string | null; updatedAt: number; }> = []; let releaseUserAppend: (() => void) | null = null; let userAppendCount = 0; const makeAgentConversationResult = () => ({ path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: agentMessages, }); 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 { path: args?.agentId ? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl' : '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: args?.agentId ?? null, messages: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }; if (message.role === 'user') { userAppendCount += 1; return await new Promise((resolve) => { releaseUserAppend = () => { agentMessages.push({ schemaVersion: 'game-creator-conversation.v1', role: message.role, content: message.content, agentId: message.agentId, updatedAt: agentMessages.length + 1, }); resolve(makeAgentConversationResult()); }; }); } agentMessages.push({ schemaVersion: 'game-creator-conversation.v1', role: message.role, content: message.content, agentId: message.agentId, updatedAt: agentMessages.length + 1, }); return makeAgentConversationResult(); } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click( await screen.findByRole('button', { name: /拆解创作方向/ }), ); const input = await screen.findByLabelText('Agent 对话内容'); const form = input.closest('form') as HTMLFormElement; fireEvent.change(input, { target: { value: '只保存一次' } }); fireEvent.submit(form); await screen.findByText('正在保存'); fireEvent.submit(form); expect((form.querySelector('button') as HTMLButtonElement).disabled).toBe( true, ); expect(userAppendCount).toBe(1); await act(async () => { releaseUserAppend?.(); }); expect(await screen.findByText(/已保存 2 条/)).not.toBeNull(); expect(userAppendCount).toBe(1); }); it('does not show unsaved agent messages when persistence fails', 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_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } if (command === 'append_local_conversation_message') { throw new Error('保存 Agent 对话失败'); } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const input = await screen.findByLabelText('Agent 对话内容'); fireEvent.change(input, { target: { value: '这条不应该显示成已保存' } }); fireEvent.submit(input.closest('form') as HTMLFormElement); expect(await screen.findByText('保存 Agent 对话失败')).not.toBeNull(); expect(screen.getByText('暂无对话')).not.toBeNull(); expect(screen.queryByText('这条不应该显示成已保存')).toBeNull(); expect(screen.getByLabelText('Agent 对话内容')).toHaveProperty( 'value', '这条不应该显示成已保存', ); }); it('keeps the saved user message visible when the local agent receipt fails', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const agentMessages: Array<{ schemaVersion: string; role: 'user' | 'assistant'; content: string; agentId: string | null; updatedAt: number; }> = []; 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 { 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: '', exists: false, }; } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }; if (message.role === 'assistant') { throw new Error('保存 Agent 回执失败'); } agentMessages.push({ schemaVersion: 'game-creator-conversation.v1', role: message.role, content: message.content, agentId: message.agentId, updatedAt: 1, }); return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: agentMessages, }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const input = await screen.findByLabelText('Agent 对话内容'); fireEvent.change(input, { target: { value: '先保留这个方向' } }); fireEvent.submit(input.closest('form') as HTMLFormElement); expect( await screen.findByText(/已保存用户消息;Agent 回执失败/), ).not.toBeNull(); expect(screen.getByText('先保留这个方向')).not.toBeNull(); expect( screen.queryByText( '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', ), ).toBeNull(); expect(screen.getByLabelText('Agent 对话内容')).toHaveProperty('value', ''); }); it('reports Tauri availability when saving an agent conversation without invoke', 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_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const input = await screen.findByLabelText('Agent 对话内容'); fireEvent.change(input, { target: { value: '确认运行环境提示' } }); delete window.__TAURI__; fireEvent.submit(input.closest('form') as HTMLFormElement); expect(await screen.findByText('需要在 Tauri App 内运行')).not.toBeNull(); expect(screen.queryByText('请先初始化本地项目')).toBeNull(); }); it('keeps a saved agent user message visible after saving', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const savedAgentMessages: Array<{ schemaVersion: string; role: 'user' | 'assistant'; content: string; agentId: string | null; updatedAt: number; }> = []; 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 { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: savedAgentMessages, }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }; savedAgentMessages.push({ schemaVersion: 'game-creator-conversation.v1', role: message.role, content: message.content, agentId: message.agentId, updatedAt: 1, }); return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: savedAgentMessages, }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const input = await screen.findByLabelText('Agent 对话内容'); fireEvent.change(input, { target: { value: '先记住这个方向' } }); fireEvent.submit(input.closest('form') as HTMLFormElement); expect(await screen.findByText(/已保存 2 条/)).not.toBeNull(); expect(screen.getByText('先记住这个方向')).not.toBeNull(); expect( screen.getByText( '已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。', ), ).not.toBeNull(); expect(screen.getByLabelText('Agent 对话内容')).toHaveProperty('value', ''); }); it('clears stale agent messages when another agent conversation fails to load', 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_conversation') { if (args?.agentId === null) { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (args?.agentId === 'design-director') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '旧 Agent 历史消息', agentId: 'design-director', updatedAt: 1, }, ], }; } throw new Error('读取 Agent 对话失败'); } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: `/tmp/authorized-game/memory/agents/${String( args?.taskId ?? '', )}.md`, content: '', exists: false, }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); expect(await screen.findByText('旧 Agent 历史消息')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '关闭' })); fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ })); expect(await screen.findByText('读取 Agent 对话失败')).not.toBeNull(); expect(screen.getByText('暂无对话')).not.toBeNull(); expect(screen.queryByText('旧 Agent 历史消息')).toBeNull(); }); it('ignores stale agent conversation reads after switching agents', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let releaseOldConversation: (() => 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') { if (args?.agentId === null) { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (args?.agentId === 'design-director') { return await new Promise((resolve) => { releaseOldConversation = () => resolve({ path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '旧 Agent 慢速消息', agentId: 'design-director', updatedAt: 1, }, ], }); }); } return { path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl', agentId: args?.agentId, messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '新 Agent 历史消息', agentId: String(args?.agentId ?? ''), updatedAt: 2, }, ], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: `/tmp/authorized-game/memory/agents/${String( args?.taskId ?? '', )}.md`, content: args?.taskId === 'art-director' ? '新 Agent 私有记忆' : '旧 Agent 私有记忆', exists: true, }; } 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); await waitFor(() => { expect(releaseOldConversation).not.toBeNull(); }); fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ })); expect(await screen.findByText('新 Agent 历史消息')).not.toBeNull(); expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain( '新 Agent 私有记忆', ); await act(async () => { releaseOldConversation?.(); }); expect(screen.queryByText('旧 Agent 慢速消息')).toBeNull(); expect(screen.getByText('新 Agent 历史消息')).not.toBeNull(); expect(screen.getByLabelText('Agent 私有记忆').textContent).not.toContain( '旧 Agent 私有记忆', ); }); it('ignores stale agent conversation saves after switching agents', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let releaseOldSave: (() => 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') { if (args?.agentId === null) { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (args?.agentId === 'art-director') { return { path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl', agentId: 'art-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '新 Agent 留存消息', agentId: 'art-director', updatedAt: 2, }, ], }; } return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: `/tmp/authorized-game/memory/agents/${String( args?.taskId ?? '', )}.md`, content: '', exists: false, }; } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }; return await new Promise((resolve) => { releaseOldSave = () => resolve({ path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: message.role, content: message.content, agentId: message.agentId, 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('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); const input = await screen.findByLabelText('Agent 对话内容'); fireEvent.change(input, { target: { value: '旧 Agent 保存回包' } }); fireEvent.submit(input.closest('form') as HTMLFormElement); await screen.findByText('正在保存'); fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ })); expect(await screen.findByText('新 Agent 留存消息')).not.toBeNull(); await act(async () => { releaseOldSave?.(); }); expect(screen.getByText('新 Agent 留存消息')).not.toBeNull(); expect(screen.queryByText('旧 Agent 保存回包')).toBeNull(); expect(screen.queryByText(/已保存 1 条/)).toBeNull(); }); it('ignores stale agent reads after closing the agent dialog', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let releaseConversation: (() => 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') { if (args?.agentId === null) { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } return await new Promise((resolve) => { releaseConversation = () => resolve({ path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '关闭后不该写入界面状态', agentId: 'design-director', updatedAt: 1, }, ], }); }); } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '关闭后不该读取私有记忆', exists: true, }; } 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('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); await waitFor(() => { expect( ( screen.getByRole('button', { name: '刷新 Agent', }) as HTMLButtonElement ).disabled, ).toBe(false); }); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); await waitFor(() => { expect(releaseConversation).not.toBeNull(); }); fireEvent.click(screen.getByRole('button', { name: '关闭' })); expect(screen.queryByLabelText('Agent 对话')).toBeNull(); await act(async () => { releaseConversation?.(); }); expect(screen.queryByText('关闭后不该写入界面状态')).toBeNull(); expect(screen.queryByText('conversation.read')).toBeNull(); expect(screen.queryByText('memory.agent.read')).toBeNull(); }); it('updates the open agent dialog when agent status is refreshed', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let runReadCount = 0; const makeTrace = (withAgentStep: boolean) => ({ schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-open-agent-refresh', commandId: 'game.generate_draft', status: 'running', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'running', goal: '做一个厨房弹幕游戏', coordination: 'Planner', steps: withAgentStep ? [ { 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.refresh', status: 'ok', inputPaths: ['memory/session.md'], outputPaths: ['.agent/spec.md'], summary: '刷新后工具调用', }, ...Array.from({ length: 5 }, (_, index) => ({ toolId: `llm.extra.${index + 1}`, status: 'ok', inputPaths: [], outputPaths: [], summary: `额外工具调用 ${index + 1}`, })), ], }, ] : [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: withAgentStep ? ['design-director'] : [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'continue', error: null, updatedAt: runReadCount, }) satisfies GameCreationAgentRunTrace; 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: args?.agentId ? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl' : '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: args?.agentId ?? null, messages: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'read_local_project_file') { runReadCount += 1; return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(makeTrace(runReadCount > 1)), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('已打开:/tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); expect(await screen.findByLabelText('Agent 对话')).not.toBeNull(); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( '暂无最近运行证据', ); fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); expect(await screen.findByText('刷新后的拆解方向')).not.toBeNull(); await waitFor(() => { 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.refresh · ok · 刷新后工具调用', ); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( 'tool: llm.planner.refresh · ok · 刷新后工具调用 · in memory/session.md · out .agent/spec.md', ); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( '还有 1 个工具调用', ); }); it('confirms before refreshing agents when trace read policy requires it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-agent-refresh-confirm', commandId: 'game.generate_draft', status: 'running', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'running', goal: '做一个厨房弹幕游戏', coordination: 'Planner', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'continue', 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_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } 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), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('想做什么游戏?')).not.toBeNull(); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); expect(await screen.findByText('agent.trace_read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); 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('cancels agent run trace refresh policy confirmation from the panel', 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_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: ['agent.trace_read'], }, }; } if (command === 'read_local_project_file') { throw new Error('should wait for trace confirmation'); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('想做什么游戏?')).not.toBeNull(); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); const traceReadCommand = await screen.findByText('agent.trace_read'); fireEvent.click( within( traceReadCommand.closest('.pending-command') as HTMLElement, ).getByRole('button', { name: '取消' }), ); expect( await screen.findByText('run: 已取消读取 Agent trace'), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); }); it('edits the published runtime config without leaking API keys into chat', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_game_creator_app_config') { return { path: '/home/test/AppData/game-creator.config.json', config: { llm: { apiKey: 'unit-loaded-secret-value', baseUrl: 'https://llm.example.test/v1', model: 'gpt-test', apiKind: 'openai_responses', stream: false, requestTimeoutMs: 180000, maxRetries: 0, retryBackoffMs: 500, }, agentLlm: { planner: { apiKey: 'planner-loaded-secret', baseUrl: 'https://api.anthropic.com', model: 'claude-3-5-sonnet-latest', apiKind: 'anthropic', stream: false, }, 'art-asset-plan': { apiKey: 'art-loaded-secret', baseUrl: 'https://api.deepseek.com', model: 'deepseek-chat', apiKind: 'openai_chat', stream: true, }, }, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: 'editor-loaded-secret', }, }, }; } if (command === 'write_game_creator_app_config') { return { path: '/home/test/AppData/game-creator.config.json', config: args?.config, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); fireEvent.click(screen.getByRole('button', { name: '配置' })); expect(await screen.findByDisplayValue('gpt-test')).not.toBeNull(); expect( screen.getByText('/home/test/AppData/game-creator.config.json'), ).not.toBeNull(); expect(screen.getByLabelText('聊天').textContent).not.toContain( 'unit-loaded-secret-value', ); expect(screen.getByLabelText('聊天').textContent).not.toContain( 'planner-loaded-secret', ); expect(screen.getByLabelText('LLM API Key')).toHaveProperty( 'type', 'password', ); expect( screen.getByLabelText('LLM API Key').getAttribute('autocomplete'), ).toBe('off'); expect(screen.getByLabelText('画板 API Key')).toHaveProperty( 'type', 'password', ); expect( screen.getByLabelText('画板 API Key').getAttribute('autocomplete'), ).toBe('off'); expect(screen.getByLabelText('Planner LLM API Key')).toHaveProperty( 'type', 'password', ); expect( screen.getByLabelText('Planner LLM API Key').getAttribute('autocomplete'), ).toBe('off'); expect( screen .getByLabelText('规划美术资产 (art/Asset) LLM API Key') .getAttribute('autocomplete'), ).toBe('off'); expect(screen.getByLabelText('Planner LLM Provider')).toHaveProperty( 'value', 'anthropic', ); expect(screen.getByLabelText('Planner LLM 模型')).toHaveProperty( 'value', 'claude-3-5-sonnet-latest', ); expect(screen.getByLabelText('Planner LLM 流式请求')).toHaveProperty( 'value', 'false', ); expect( screen.getByLabelText('规划美术资产 (art/Asset) LLM Provider'), ).toHaveProperty('value', 'deepseek'); expect( screen.getByLabelText('规划美术资产 (art/Asset) LLM 流式请求'), ).toHaveProperty('value', 'true'); fireEvent.change(screen.getByLabelText('LLM API Key'), { target: { value: 'unit-new-secret-value' }, }); fireEvent.change(screen.getByLabelText('LLM Base URL'), { target: { value: 'https://new-llm.example.test/v1' }, }); fireEvent.change(screen.getByLabelText('LLM 模型'), { target: { value: 'gpt-next' }, }); fireEvent.change(screen.getByLabelText('LLM API 类型'), { target: { value: 'openai_chat' }, }); fireEvent.click(screen.getByLabelText('LLM 流式请求')); fireEvent.change(screen.getByLabelText('LLM 超时 ms'), { target: { value: '90000' }, }); fireEvent.change(screen.getByLabelText('LLM 重试次数'), { target: { value: '3' }, }); fireEvent.change(screen.getByLabelText('LLM 退避 ms'), { target: { value: '800' }, }); fireEvent.change(screen.getByLabelText('Generator LLM API Key'), { target: { value: 'generator-new-secret' }, }); fireEvent.change(screen.getByLabelText('Generator LLM Provider'), { target: { value: 'deepseek' }, }); fireEvent.change(screen.getByLabelText('Generator LLM 流式请求'), { target: { value: 'true' }, }); fireEvent.change( screen.getByLabelText('规划美术资产 (art/Asset) LLM Provider'), { target: { value: 'ark' }, }, ); fireEvent.change(screen.getByLabelText('画板 API Base URL'), { target: { value: 'http://127.0.0.1:8099' }, }); fireEvent.change(screen.getByLabelText('画板 API Key'), { target: { value: 'editor-new-secret' }, }); fireEvent.click(screen.getByRole('button', { name: '保存' })); expect(await screen.findByText(/已保存:/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_game_creator_app_config'); expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', { config: { llm: { apiKey: 'unit-new-secret-value', baseUrl: 'https://new-llm.example.test/v1', model: 'gpt-next', apiKind: 'openai_chat', stream: true, requestTimeoutMs: 90000, maxRetries: 3, retryBackoffMs: 800, }, agentLlm: { planner: { apiKey: 'planner-loaded-secret', baseUrl: 'https://api.anthropic.com', model: 'claude-3-5-sonnet-latest', apiKind: 'anthropic', stream: false, }, generator: { apiKey: 'generator-new-secret', baseUrl: 'https://api.deepseek.com', model: 'deepseek-chat', apiKind: 'openai_chat', stream: true, }, 'art-asset-plan': { apiKey: 'art-loaded-secret', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', model: 'doubao-seed-1-6', apiKind: 'openai_chat', stream: true, }, }, editorApi: { baseUrl: 'http://127.0.0.1:8099', apiKey: 'editor-new-secret', }, }, }); expect(screen.getByLabelText('聊天').textContent).not.toContain( 'unit-new-secret-value', ); expect(screen.getByLabelText('聊天').textContent).not.toContain( 'generator-new-secret', ); fireEvent.change(screen.getByLabelText('LLM 超时 ms'), { target: { value: '' }, }); fireEvent.change(screen.getByLabelText('LLM 重试次数'), { target: { value: '-2' }, }); fireEvent.change(screen.getByLabelText('LLM 退避 ms'), { target: { value: '0' }, }); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', { config: expect.objectContaining({ llm: expect.objectContaining({ requestTimeoutMs: 1000, maxRetries: 0, retryBackoffMs: 1, }), }), }); }); fireEvent.click(screen.getByRole('button', { name: '恢复默认' })); expect(screen.getByText('已恢复默认配置,保存后生效')).not.toBeNull(); expect(screen.getByLabelText('LLM API Key')).toHaveProperty('value', ''); expect(screen.getByLabelText('LLM Base URL')).toHaveProperty( 'value', 'https://api.openai.com/v1', ); expect(screen.getByLabelText('LLM 模型')).toHaveProperty( 'value', 'gpt-4.1', ); expect(screen.getByLabelText('画板 API Base URL')).toHaveProperty( 'value', 'http://127.0.0.1:8082', ); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', { config: { llm: { apiKey: '', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1', apiKind: 'openai_responses', stream: false, requestTimeoutMs: 180000, maxRetries: 0, retryBackoffMs: 500, }, agentLlm: {}, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '', }, }, }); }); }); it('keeps multiline chat evidence readable', () => { const styles = readFileSync( resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toMatch(/\.message\s*\{[^}]*white-space:\s*pre-wrap/s); expect(styles).toMatch(/\.message\s*\{[^}]*overflow-wrap:\s*anywhere/s); }); it('shows developer panels only in dev mode', () => { renderAppAt('/?dev'); expect(screen.getByLabelText('开发环境')).not.toBeNull(); expect(screen.getByLabelText('任务')).not.toBeNull(); expect(screen.getByText('Agent 能力')).not.toBeNull(); expect(screen.getByText('编排 Trace')).not.toBeNull(); expect(screen.getByText('项目文件')).not.toBeNull(); expect(screen.getByText('预览')).not.toBeNull(); }); it('writes project files from the developer file panel', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'write_local_project_file') { return { path: String(args?.relativePath ?? ''), absolutePath: `/tmp/genarrative-ai-game-draft/${String(args?.relativePath ?? '')}`, deleted: false, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const confirm = vi.spyOn(window, 'confirm'); renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/debug-note.txt' }, }); fireEvent.change(screen.getByLabelText('项目文件内容'), { target: { value: 'hello file panel' }, }); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '保存', }), ); expect(screen.getByText('file.write')).not.toBeNull(); expect( screen.getByText( '保存 /tmp/genarrative-ai-game-draft/game/debug-note.txt', ), ).not.toBeNull(); expect(confirm).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已保存:game/debug-note.txt'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('write_local_project_file', { projectPath: '/tmp/genarrative-ai-game-draft', relativePath: 'game/debug-note.txt', content: 'hello file panel', }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/genarrative-ai-game-draft', event: 'permission.pending', commandId: 'file.write', }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/genarrative-ai-game-draft', event: 'permission.confirm', commandId: 'file.write', }); }); it('blocks developer file write confirmation when project policy denies it', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'read_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: ['file.write'], confirmCommands: [], }, }; } if (command === 'write_local_project_file') { throw new Error('should not write file after deny'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/debug-note.txt' }, }); fireEvent.change(screen.getByLabelText('项目文件内容'), { target: { value: 'should not save' }, }); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '保存', }), ); expect(screen.getByText('file.write')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect( within(screen.getByLabelText('项目文件')).getByText( '项目权限策略拒绝执行:file.write', ), ).not.toBeNull(); }); expect(invoke).not.toHaveBeenCalledWith( 'write_local_project_file', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'append_local_permission_log', expect.objectContaining({ event: 'permission.confirm', commandId: 'file.write', }), ); }); it('keeps developer file confirmations usable when permission log append fails', async () => { const invoke = vi.fn((command: string, args?: Record) => { if (command === 'append_local_permission_log') { if (args?.event === 'permission.pending') { throw new Error('pending log failed'); } return Promise.reject(new Error('confirm log failed')); } if (command === 'write_local_project_file') { return { path: String(args?.relativePath ?? ''), absolutePath: `/tmp/genarrative-ai-game-draft/${String(args?.relativePath ?? '')}`, deleted: false, }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/debug-note.txt' }, }); fireEvent.change(screen.getByLabelText('项目文件内容'), { target: { value: 'hello despite log failure' }, }); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '保存', }), ); expect(screen.getByText('file.write')).not.toBeNull(); expect( await screen.findByText( 'permission.log.failed file.write: pending log failed', ), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已保存:game/debug-note.txt'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('write_local_project_file', { projectPath: '/tmp/genarrative-ai-game-draft', relativePath: 'game/debug-note.txt', content: 'hello despite log failure', }); expect( await screen.findByText( 'permission.log.failed file.write: confirm log failed', ), ).not.toBeNull(); }); it('cancels developer file write confirmations without mutating files', () => { const invoke = vi.fn(async (command: string) => { if (command === 'append_local_permission_log') { return {}; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/debug-note.txt' }, }); fireEvent.change(screen.getByLabelText('项目文件内容'), { target: { value: 'should not save' }, }); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '保存', }), ); expect(screen.getByText('file.write')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '取消' })); expect(screen.queryByText('file.write')).toBeNull(); expect(screen.getByText('已取消保存项目文件')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'write_local_project_file', expect.anything(), ); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/genarrative-ai-game-draft', event: 'permission.pending', commandId: 'file.write', }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/genarrative-ai-game-draft', event: 'permission.cancel', commandId: 'file.write', }); }); it('rejects unsafe developer file panel paths before confirmation or invoke', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const filePanel = within(screen.getByLabelText('项目文件')); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: '../outside.txt' }, }); fireEvent.click(filePanel.getByRole('button', { name: '读取' })); expect(screen.getByText('文件路径必须是项目内相对路径。')).not.toBeNull(); fireEvent.click(filePanel.getByRole('button', { name: '保存' })); fireEvent.click(filePanel.getByRole('button', { name: '删除' })); expect( screen.getAllByText('文件路径必须是项目内相对路径。').length, ).toBeGreaterThanOrEqual(1); expect(screen.queryByText('file.write')).toBeNull(); expect(screen.queryByText('file.delete')).toBeNull(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); 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('rejects unsafe developer file panel project paths before confirmation or invoke', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const filePanel = within(screen.getByLabelText('项目文件')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: 'relative-project' }, }); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/debug-note.txt' }, }); fireEvent.click(filePanel.getByRole('button', { name: '列出' })); expect(screen.getByText('请提供本地项目绝对路径。')).not.toBeNull(); fireEvent.click(filePanel.getByRole('button', { name: '读取' })); fireEvent.click(filePanel.getByRole('button', { name: '保存' })); fireEvent.click(filePanel.getByRole('button', { name: '删除' })); expect(screen.queryByText('file.write')).toBeNull(); expect(screen.queryByText('file.delete')).toBeNull(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/bad\u0007project' }, }); fireEvent.click(filePanel.getByRole('button', { name: '读取' })); expect(screen.getByText('本地项目路径不能包含控制字符。')).not.toBeNull(); expect(invoke).not.toHaveBeenCalled(); }); it('requires project policy confirmation before listing files from the developer panel', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } 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('/?dev'); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '列出', }), ); expect(await screen.findByText('file.list')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'list_local_project_files', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已列出 1 项')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('list_local_project_files', { projectPath: '/tmp/genarrative-ai-game-draft', }); }); it('cancels developer file list policy confirmation without listing files', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'read_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: ['file.list'], }, }; } if (command === 'list_local_project_files') { throw new Error('should wait for file list confirmation'); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '列出', }), ); const fileListCommand = await screen.findByText('file.list'); fireEvent.click( within( fileListCommand.closest('.pending-command') as HTMLElement, ).getByRole('button', { name: '取消' }), ); expect(await screen.findByText('已取消读取项目文件')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'list_local_project_files', expect.anything(), ); }); it('requires project policy confirmation before reading files from the developer panel', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } 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('/?dev'); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/index.html' }, }); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '读取', }), ); expect(await screen.findByText('file.read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已读取:game/index.html')).not.toBeNull(); expect(screen.getByLabelText('项目文件内容')).toHaveProperty( 'value', '', ); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/genarrative-ai-game-draft', relativePath: 'game/index.html', commandId: 'file.read', }); }); it('cancels developer file read policy confirmation without reading files', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'read_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: ['file.read'], }, }; } if (command === 'read_local_project_file') { throw new Error('should wait for file read confirmation'); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/index.html' }, }); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '读取', }), ); const fileReadCommand = await screen.findByText('file.read'); fireEvent.click( within( fileReadCommand.closest('.pending-command') as HTMLElement, ).getByRole('button', { name: '取消' }), ); expect(await screen.findByText('已取消读取项目文件')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); }); it('deletes project files from the developer file panel', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'delete_local_project_file') { return { path: String(args?.relativePath ?? ''), absolutePath: `/tmp/genarrative-ai-game-draft/${String(args?.relativePath ?? '')}`, deleted: true, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const confirm = vi.spyOn(window, 'confirm'); renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('项目文件路径'), { target: { value: 'game/debug-note.txt' }, }); fireEvent.change(screen.getByLabelText('项目文件内容'), { target: { value: 'delete me' }, }); fireEvent.click( within(screen.getByLabelText('项目文件')).getByRole('button', { name: '删除', }), ); expect(screen.getByText('file.delete')).not.toBeNull(); expect( screen.getByText( '删除 /tmp/genarrative-ai-game-draft/game/debug-note.txt', ), ).not.toBeNull(); expect(confirm).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已删除:game/debug-note.txt'), ).not.toBeNull(); expect( (screen.getByLabelText('项目文件内容') as HTMLTextAreaElement).value, ).toBe(''); expect(invoke).toHaveBeenCalledWith('delete_local_project_file', { projectPath: '/tmp/genarrative-ai-game-draft', relativePath: 'game/debug-note.txt', }); }); it('reads project blackboard memory from the developer memory panel', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'read_local_game_memory') { return { scope: args?.scope, path: 'memory/blackboard.md', content: '# 项目黑板\n- 保留跨 agent 决策\n', exists: true, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const memoryPanel = screen.getByLabelText('记忆'); fireEvent.change(memoryPanel.querySelector('select') as HTMLSelectElement, { target: { value: 'blackboard' }, }); fireEvent.click( Array.from(memoryPanel.querySelectorAll('button')).find( (button) => button.textContent === '读取', ) as HTMLButtonElement, ); expect( await screen.findByText(/已读取:memory\/blackboard\.md/), ).not.toBeNull(); expect(screen.getByLabelText('记忆内容')).toHaveProperty( 'value', '# 项目黑板\n- 保留跨 agent 决策\n', ); expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { projectPath: '/tmp/genarrative-ai-game-draft', scope: 'blackboard', }); }); it('requires project policy confirmation before reading memory from the developer panel', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } 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/blackboard.md', content: '# 项目黑板\n', exists: true, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const memoryPanel = screen.getByLabelText('记忆'); fireEvent.change(memoryPanel.querySelector('select') as HTMLSelectElement, { target: { value: 'blackboard' }, }); fireEvent.click( Array.from(memoryPanel.querySelectorAll('button')).find( (button) => button.textContent === '读取', ) as HTMLButtonElement, ); expect(await screen.findByText('memory.read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_game_memory', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText(/已读取:memory\/blackboard\.md/), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { projectPath: '/tmp/genarrative-ai-game-draft', scope: 'blackboard', }); }); it('cancels project memory read confirmation from the developer panel', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'append_local_permission_log') { return {}; } 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('/?dev'); const memoryPanel = screen.getByLabelText('记忆'); fireEvent.click(within(memoryPanel).getByRole('button', { name: '读取' })); const memoryReadCommand = await screen.findByText('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 unsafe developer memory project paths before confirmation or invoke', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const memoryPanel = within(screen.getByLabelText('记忆')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: 'relative-project' }, }); fireEvent.click(memoryPanel.getByRole('button', { name: '读取' })); expect(screen.getByText('请提供本地项目绝对路径。')).not.toBeNull(); fireEvent.click(memoryPanel.getByRole('button', { name: '保存' })); fireEvent.click(memoryPanel.getByRole('button', { name: '删除' })); expect(screen.queryByText('memory.write')).toBeNull(); expect(screen.queryByText('memory.delete')).toBeNull(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/bad\u0007project' }, }); fireEvent.click(memoryPanel.getByRole('button', { name: '读取' })); expect(screen.getByText('本地项目路径不能包含控制字符。')).not.toBeNull(); expect(invoke).not.toHaveBeenCalled(); }); it('lists standard agent capabilities from chat without opening dev panels', () => { renderAppAt('/'); submitChat('/capabilities'); const capabilities = screen.getByText(/Agent 能力清单:/); expect(capabilities.textContent).toMatch(/任务拆分/); expect(capabilities.textContent).toMatch(/任务编排/); expect(capabilities.textContent).toMatch( /Planner \/ Generator \/ Evaluator 循环/, ); expect(capabilities.textContent).toMatch(/多智能体协作/); expect(capabilities.textContent).toMatch(/短期记忆/); expect(capabilities.textContent).toMatch(/长期记忆/); expect(capabilities.textContent).toMatch(/本地 HTTP 预览/); expect(screen.queryByLabelText('开发环境')).toBeNull(); expect(screen.queryByText('编排 Trace')).toBeNull(); }); it('loads agent capabilities from the native runtime when available', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'get_game_creation_agent_capabilities') { return [ { id: 'native-only-capability', area: 'agent-runtime', title: 'Native Runtime 能力', }, ]; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/capabilities'); expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull(); expect(screen.queryByText(/任务拆分/)).toBeNull(); expect(invoke).toHaveBeenCalledWith('get_game_creation_agent_capabilities'); }); it('falls back to standard agent capabilities when the native runtime returns none', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'get_game_creation_agent_capabilities') { return []; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/capabilities'); expect(await screen.findByText(/任务拆分/)).not.toBeNull(); expect(screen.getByText(/本地 HTTP 预览/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('get_game_creation_agent_capabilities'); }); it('exposes the audit command from chat help', () => { renderAppAt('/'); submitChat('/help'); expect( screen.getByText(/\/audit:审计当前项目的 Agent 能力证据/), ).not.toBeNull(); expect( screen.getByText(/\/llm-routes:查看 Agent LLM 路由清单/), ).not.toBeNull(); expect(screen.getByText(/\/checkpoint:保存本地项目快照/)).not.toBeNull(); expect(screen.getByText(/\/export:导出本地试玩包/)).not.toBeNull(); expect(screen.getByText(/\/exports:列出本地试玩包/)).not.toBeNull(); expect( screen.getByText(/\/checkpoints:列出最近 checkpoint/), ).not.toBeNull(); expect( screen.getByText(/\/restore checkpoint-id:回滚项目文件到 checkpoint/), ).not.toBeNull(); expect( screen.getByText(/\/policy-deny 命令:拒绝项目内某个内置命令/), ).not.toBeNull(); expect( screen.getByText(/\/policy-confirm 命令:执行前每次确认/), ).not.toBeNull(); expect(screen.getByText(/\/policy-auto 命令:恢复自动执行/)).not.toBeNull(); expect( screen.getByText(/\/agents:查看每个 Agent 的当前状态/), ).not.toBeNull(); expect( screen.getByText(/\/agent-conversations:列出 Agent 对话读取命令/), ).not.toBeNull(); expect( screen.getByText(/\/agent-memories:列出 Agent 私有记忆读取命令/), ).not.toBeNull(); expect( screen.getByText(/\/agent-status:查看最近 run 生命周期/), ).not.toBeNull(); expect( screen.getByText(/\/agent-kill:标记最近 run 为 killed/), ).not.toBeNull(); expect( screen.getByText(/\/history:重新读取当前项目对话历史/), ).not.toBeNull(); expect( screen.getByText(/\/open-project:在系统文件管理器中显示项目目录/), ).not.toBeNull(); expect( screen.getByText(/\/switch-project:回到首页项目组切换工作区/), ).not.toBeNull(); expect(screen.getByText(/\/brief:生成当前项目简报/)).not.toBeNull(); expect(screen.getByText(/\/goal:查看创作目标/)).not.toBeNull(); expect( screen.getByText(/\/guide:查看普通用户操作导引/), ).not.toBeNull(); expect(screen.getByText(/\/progress:查看项目进度/)).not.toBeNull(); expect(screen.getByText(/\/spec:查看创作规格包/)).not.toBeNull(); expect(screen.getByText(/\/mvp:查看本轮最小可玩范围/)).not.toBeNull(); expect(screen.getByText(/\/pitch:查看试玩定位与卖点/)).not.toBeNull(); expect(screen.getByText(/\/demo:准备 30 秒试玩讲解稿/)).not.toBeNull(); expect(screen.getByText(/\/rules:查看玩法操作与规则/)).not.toBeNull(); expect(screen.getByText(/\/tutorial:查看新手引导检查/)).not.toBeNull(); expect(screen.getByText(/\/mobile:查看移动试玩检查/)).not.toBeNull(); expect(screen.getByText(/\/compatibility:准备兼容性说明/)).not.toBeNull(); expect( screen.getByText(/\/accessibility:查看可读性与无障碍检查/), ).not.toBeNull(); expect( screen.getByText(/\/localization:查看本地化与文案检查/), ).not.toBeNull(); expect( screen.getByText(/\/performance:查看性能与加载检查/), ).not.toBeNull(); expect(screen.getByText(/\/polish:查看试玩前打磨清单/)).not.toBeNull(); expect(screen.getByText(/\/risks:查看当前项目风险/)).not.toBeNull(); expect(screen.getByText(/\/blockers:查看当前阻塞项/)).not.toBeNull(); expect(screen.getByText(/\/ready:查看试玩就绪度/)).not.toBeNull(); expect( screen.getByText(/\/evidence:查看当前验证证据台账/), ).not.toBeNull(); expect(screen.getByText(/\/deps:查看任务依赖链/)).not.toBeNull(); expect( screen.getByText(/\/revise:准备下一轮改版说明草稿/), ).not.toBeNull(); expect( screen.getByText(/\/privacy:查看隐私与导出边界/), ).not.toBeNull(); expect(screen.getByText(/\/audience:查看首批试玩对象/)).not.toBeNull(); expect(screen.getByText(/\/invite:准备试玩邀请文案/)).not.toBeNull(); expect( screen.getByText(/\/bug-report:准备缺陷复现记录/), ).not.toBeNull(); expect(screen.getByText(/\/survey:准备试玩问卷问题/)).not.toBeNull(); expect(screen.getByText(/\/cover:准备封面与缩略图检查/)).not.toBeNull(); expect(screen.getByText(/\/screenshots:准备宣传截图清单/)).not.toBeNull(); expect(screen.getByText(/\/trailer:准备试玩短视频脚本/)).not.toBeNull(); expect(screen.getByText(/\/faq:准备试玩常见问答/)).not.toBeNull(); expect(screen.getByText(/\/post:准备社区发布文案/)).not.toBeNull(); expect(screen.getByText(/\/store:准备上架资料清单/)).not.toBeNull(); expect(screen.getByText(/\/media-kit:准备媒体资料包清单/)).not.toBeNull(); expect(screen.getByText(/\/release-notes:准备试玩更新说明/)).not.toBeNull(); expect(screen.getByText(/\/known-issues:准备已知问题清单/)).not.toBeNull(); expect(screen.getByText(/\/criteria:查看当前任务验收标准/)).not.toBeNull(); expect(screen.getByText(/\/groups:查看专业组进度/)).not.toBeNull(); expect(screen.getByText(/\/balance:查看数值与难度口径/)).not.toBeNull(); expect(screen.getByText(/\/budget:查看最近 run 预算/)).not.toBeNull(); expect(screen.getByText(/\/qa:查看质量检查清单/)).not.toBeNull(); expect(screen.getByText(/\/changes:查看最近生成变更/)).not.toBeNull(); expect( screen.getByText(/\/review:查看 Evaluator 评审和返工焦点/), ).not.toBeNull(); expect(screen.getByText(/\/context:查看生成上下文来源/)).not.toBeNull(); expect(screen.getByText(/\/timeline:查看项目活动时间线/)).not.toBeNull(); expect(screen.getByText(/\/handoff:生成当前项目交接摘要/)).not.toBeNull(); expect(screen.getByText(/\/next:查看下一步建议/)).not.toBeNull(); expect(screen.getByText(/\/plan:查看下一轮分工计划/)).not.toBeNull(); expect(screen.getByText(/\/todo:查看下一轮小步清单/)).not.toBeNull(); expect(screen.getByText(/\/publish:查看发布准备清单/)).not.toBeNull(); expect(screen.getByText(/\/listing:准备作品页文案清单/)).not.toBeNull(); expect(screen.getByText(/\/playtest:查看试玩状态与下一步/)).not.toBeNull(); expect(screen.getByText(/\/test-plan:准备手动测试计划/)).not.toBeNull(); expect( screen.getByText(/\/feedback:准备试玩反馈和修改说明/), ).not.toBeNull(); expect( screen.getByText(/\/retention:准备首轮复玩\/留存观察清单/), ).not.toBeNull(); expect(screen.getByText(/\/share:准备试玩交付清单/)).not.toBeNull(); expect( screen.getByText(/\/run-files:列出 Agent 运行辅助文件读取命令/), ).not.toBeNull(); expect( screen.getByText(/\/passes:列出 Agent 轮次产物读取命令/), ).not.toBeNull(); expect( screen.getByText(/\/internals:列出项目内部真相源读取命令/), ).not.toBeNull(); expect( screen.getByText( /\/asset-register 路径 \[kind\] \[mediaType\]:登记项目内已有资产/, ), ).not.toBeNull(); expect( screen.getByText(/\/artifacts:列出常用生成产物读取命令/), ).not.toBeNull(); expect(screen.getByText(/\/credits:查看素材署名与来源/)).not.toBeNull(); expect(screen.getByText(/\/art:查看美术素材与下一步草稿/)).not.toBeNull(); expect( screen.getByText(/\/audio:查看音频素材与下一步草稿/), ).not.toBeNull(); expect( screen.getByText(/\/run-artifacts:列出最近 Run 产物读取命令/), ).not.toBeNull(); expect( screen.getByText(/\/runs:列出已加载 Run 历史读取命令/), ).not.toBeNull(); expect(screen.getByText(/\/logs:列出常用日志读取命令/)).not.toBeNull(); expect( screen.getByText(/\/canvas 画板项目ID:打开本机画板项目/), ).not.toBeNull(); expect( screen.getByText(/\/commands:查看可运行的受限命令白名单/), ).not.toBeNull(); expect(screen.queryByLabelText('开发环境')).toBeNull(); }); it('opens chat help from the header command button', () => { renderAppAt('/'); fireEvent.click(screen.getByRole('button', { name: '命令' })); expect( screen.getByText(/\/audit:审计当前项目的 Agent 能力证据/), ).not.toBeNull(); expect(screen.getByText(/\/config:打开运行时配置/)).not.toBeNull(); expect( screen.getByText( /\/generate-art 提示词:通过平台 External Editor API 生成首版美术素材/, ), ).not.toBeNull(); expect(screen.queryByLabelText('开发环境')).toBeNull(); }); it('audits agent capability evidence from chat without opening dev panels', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const auditedTasks = createGameCreationAppSeedTasks(); auditedTasks.forEach((task) => { task.status = 'completed'; }); const auditedManifest = { ...manifest, goal: '做一个反弹弹幕厨房游戏', tasks: auditedTasks, assets: [ { id: 'canvas-hero', kind: 'character', mediaType: 'image/png', localPath: 'assets/canvas-sync/hero.png', source: { kind: 'canvas' as const, canvasProjectId: 'canvas-1', resourceId: 'resource-1', }, }, ], preview: { status: 'running' as const, url: 'http://127.0.0.1:3210/', port: 3210, }, commandRuns: [ { commandId: 'game.static_smoke', status: 'completed' as const, output: 'ok', logPath: '.agent/logs/command.log', updatedAt: 1, }, ], }; const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-chat-audit', commandId: 'game.generate_draft', status: 'passed', passes: 2, maxPasses: 3, toolCallCount: 42, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个反弹弹幕厨房游戏', coordination: 'Planner -> Orchestrator -> Generator -> Evaluator', steps: [ { pass: 1, agent: 'Planner', phase: 'plan', taskId: 'design-director', group: 'design', role: 'Director', status: 'completed', inputPaths: ['memory/session.md', 'memory/project.md'], outputPaths: ['.agent/spec.md'], summary: '拆解创作目标', toolCalls: [], }, { pass: 2, agent: 'Orchestrator', phase: 'plan', taskId: 'code-director', group: 'code', role: 'Director', status: 'completed', inputPaths: ['.agent/findings.md'], outputPaths: ['.agent/passes/pass-2/task-graph.json'], summary: '按返工路由重跑程序链路', toolCalls: [], }, { pass: 2, agent: '数值组 / Difficulty', phase: 'role-brief', taskId: 'balance-seed', group: 'balance', role: 'Difficulty', status: 'completed', inputPaths: ['.agent/manifest.json'], outputPaths: ['.agent/passes/pass-2/groups/balance/difficulty.md'], summary: '生成数值约束', toolCalls: [], }, { pass: 2, agent: '美术组 / Asset', phase: 'role-brief', taskId: 'art-asset-plan', group: 'art', role: 'Asset', status: 'completed', inputPaths: ['.agent/manifest.json'], outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], summary: '规划画板回流资产', toolCalls: [], }, { pass: 2, agent: '音乐组 / SFX', phase: 'role-brief', taskId: 'audio-asset-plan', group: 'audio', role: 'SFX', status: 'completed', inputPaths: ['.agent/manifest.json'], outputPaths: ['.agent/passes/pass-2/groups/audio/sfx.md'], summary: '规划音乐音效', toolCalls: [], }, { pass: 2, agent: 'Generator', phase: 'generate', taskId: 'code-prototype', group: 'code', role: 'Code', status: 'completed', inputPaths: ['.agent/spec.md', '.agent/findings.md'], outputPaths: ['.agent/passes/pass-2/draft.json'], summary: '生成可运行原型', toolCalls: [], }, { pass: 2, agent: 'Evaluator', phase: 'evaluate', taskId: 'quality-review', group: 'code', role: 'Review', status: 'passed', inputPaths: ['.agent/passes/pass-2/draft.json'], outputPaths: ['.agent/findings.md'], summary: '通过静态验收', toolCalls: [], }, { pass: 2, agent: '运营组 / Publish', phase: 'handoff', taskId: 'publish-package', group: 'publishing', role: 'Publish', status: 'completed', inputPaths: ['.agent/passes/pass-2/handoff.md'], outputPaths: ['exports/README.md'], summary: '整理发布包装', toolCalls: [], }, ], artifacts: [ { path: 'game/index.html', sizeBytes: 1024, checksum: 'fnv1a64:game', }, { path: 'game/game_design.md', sizeBytes: 256, checksum: 'fnv1a64:design', }, { path: 'game/balance.json', sizeBytes: 128, checksum: 'fnv1a64:balance', }, { path: 'assets/manifest.art.json', sizeBytes: 128, checksum: 'fnv1a64:art', }, { path: 'assets/manifest.audio.json', sizeBytes: 128, checksum: 'fnv1a64:audio', }, { path: 'exports/README.md', sizeBytes: 128, checksum: 'fnv1a64:exports', }, ], taskGraph: { goal: '做一个反弹弹幕厨房游戏', readyTaskIds: [], activeTaskIds: ['code-prototype'], carriedTaskIds: ['design-director'], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', taskIds: ['code-prototype', 'quality-review', 'preview-readiness'], reason: 'code-runtime', }, ], tasks: auditedTasks, }, passPlans: [ { pass: 1, mode: 'initial', summary: '第 1 轮全量调度', activeTaskIds: auditedTasks.map((task) => task.id), carriedTaskIds: [], dependencyWaves: [['design-director']], repairFocus: [], repairRoutes: [], }, { pass: 2, mode: 'repair', summary: '第 2 轮返工', 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: '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 emptyProjectPolicy(); } if (command === 'get_local_game_manifest') { return auditedManifest; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: 'game/index.html', kind: 'file', size: 1024 }, { path: 'memory/session.md', kind: 'file', size: 64 }, { path: 'memory/project.md', kind: 'file', size: 64 }, { path: 'memory/blackboard.md', kind: 'file', size: 64 }, { path: 'memory/agents/design/director.md', kind: 'file', size: 64, }, { path: '.agent/logs/command.log', kind: 'file', size: 64 }, ], }; } if (command === 'read_local_project_file') { if (args?.relativePath === '.agent/logs/command.log') { return { path: '.agent/logs/command.log', absolutePath: `${String(args?.projectPath ?? '')}/.agent/logs/command.log`, content: '1 permission.pending preview.start\n2 permission.confirm preview.start\n', }; } 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/audit'); expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); expect(screen.getByText(/能力\/命令契约:通过/)).not.toBeNull(); expect(screen.getByText(/6 组任务配置:通过/)).not.toBeNull(); expect(screen.getByText(/6 组协作证据:通过/)).not.toBeNull(); expect(screen.getByText(/角色任务 16 个/)).not.toBeNull(); expect( screen.getByText(/Loop trace:通过 · run run-chat-audit/), ).not.toBeNull(); expect( screen.getByText(/Planner\/Orchestrator\/Generator\/Evaluator:通过/), ).not.toBeNull(); expect(screen.getByText(/返工路由\/Carry-over:通过/)).not.toBeNull(); expect(screen.getByText(/记忆:通过/)).not.toBeNull(); expect(screen.getByText(/本地产物:通过/)).not.toBeNull(); expect(screen.getByText(/本地 HTTP 预览:通过/)).not.toBeNull(); expect(screen.getByText(/画板回流:通过/)).not.toBeNull(); expect(screen.getByText(/权限 Gate\/命令日志:通过/)).not.toBeNull(); expect(screen.queryByLabelText('开发环境')).toBeNull(); expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { projectPath: '/tmp/authorized-game', commandId: 'agent.audit', }); expect(invoke).toHaveBeenCalledWith('list_local_project_files', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/run.latest.json', commandId: 'agent.trace_read', }); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/logs/command.log', commandId: 'file.read', }); }); it('requires trace read confirmation before audit reads run trace', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-audit-trace-confirm', commandId: 'game.generate_draft', status: 'passed', lifecycleStatus: 'done', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个厨房弹幕游戏', coordination: 'filesystem', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', 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 === '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: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: args?.relativePath === '.agent/run.latest.json' ? JSON.stringify(trace) : '1 command.auto preview.status\n', }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); submitChat('/audit'); expect( await screen.findByText('准备确认审计读取:agent.trace_read'), ).not.toBeNull(); expect(screen.getByText('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: '确认' })); expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/run.latest.json', commandId: 'agent.trace_read', }); }); it('requires file read confirmation before audit reads command logs', 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 === 'get_local_game_manifest') { return manifest; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/logs/command.log', kind: 'file', size: 64 }, ], }; } if (command === 'read_local_project_file') { if (args?.relativePath === '.agent/logs/command.log') { return { path: '.agent/logs/command.log', absolutePath: `${String(args?.projectPath ?? '')}/.agent/logs/command.log`, content: '1 command.auto preview.status\n', }; } throw new Error('missing trace'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); submitChat('/audit'); expect( await screen.findByText('准备确认审计读取:file.read'), ).not.toBeNull(); expect(screen.getByText('file.read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/logs/command.log', commandId: 'file.read', }); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/logs/command.log', commandId: 'file.read', }); }); it('confirms each audit read policy instead of one confirmation unlocking all reads', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-audit-multi-confirm', commandId: 'game.generate_draft', status: 'passed', lifecycleStatus: 'done', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个厨房弹幕游戏', coordination: 'filesystem', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'preview', 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: ['file.read', 'agent.trace_read'], }, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/logs/command.log', kind: 'file', size: 64 }, ], }; } if (command === 'read_local_project_file') { return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: args?.relativePath === '.agent/run.latest.json' ? JSON.stringify(trace) : '1 command.auto preview.status\n', }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); submitChat('/audit'); expect( await screen.findByText('准备确认审计读取:file.read'), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('准备确认审计读取:agent.trace_read'), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/logs/command.log', commandId: 'file.read', }); 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: '确认' })); expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/run.latest.json', commandId: 'agent.trace_read', }); }); it('does not mark permission gate as passed without durable permission events', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const auditedManifest = { ...manifest, commandRuns: [ { commandId: 'game.static_smoke', status: 'completed' as const, output: 'ok', logPath: '.agent/logs/command.log', 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 auditedManifest; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/logs/command.log', kind: 'file', size: 64 }, ], }; } if (command === 'read_local_project_file') { if (args?.relativePath === '.agent/logs/command.log') { return { path: '.agent/logs/command.log', absolutePath: `${String(args?.projectPath ?? '')}/.agent/logs/command.log`, content: '1 command.run_limited game.static_smoke: ok\n', }; } throw new Error('missing trace'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/audit'); expect(await screen.findByText(/权限 Gate\/命令日志:待补/)).not.toBeNull(); expect( screen.getByText(/缺 permission\.pending 或确认\/取消记录/), ).not.toBeNull(); }); it('marks permission gate as passed with durable auto command logs', 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; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/logs/command.log', kind: 'file', size: 64 }, ], }; } if (command === 'read_local_project_file') { if (args?.relativePath === '.agent/logs/command.log') { return { path: '.agent/logs/command.log', absolutePath: `${String(args?.projectPath ?? '')}/.agent/logs/command.log`, content: '1 command.auto preview.status\n', }; } throw new Error('missing trace'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/audit'); expect(await screen.findByText(/权限 Gate\/命令日志:通过/)).not.toBeNull(); expect(screen.getByText(/含 auto 权限记录/)).not.toBeNull(); }); it('does not mark multi-agent collaboration as passed before a run trace exists', 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; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [], }; } if (command === 'read_local_project_file') { throw new Error('missing trace'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/audit'); expect(await screen.findByText(/6 组任务配置:通过/)).not.toBeNull(); expect(screen.getByText(/6 组协作证据:待生成/)).not.toBeNull(); expect( screen.getByText( /Loop trace:待生成 · 还没有 \.agent\/run\.latest\.json/, ), ).not.toBeNull(); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); 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', () => { renderAppAt('/?dev'); 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', () => { renderAppAt('/?dev'); 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', () => { renderAppAt('/'); submitChat('/project'); expect(screen.getByText('格式:/project /绝对路径')).not.toBeNull(); submitChat('/remember'); expect(screen.getByText('请提供要追加的记忆内容。')).not.toBeNull(); submitChat('/sync-canvas-project'); expect(screen.getByText('请提供画板项目 ID。')).not.toBeNull(); 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', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); 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(); 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('/'); 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('/'); 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: 'local-project-draft', name: '未命名游戏原型', }); }); expect(confirm).not.toHaveBeenCalled(); }); it('records a project.create confirmation after developer project init succeeds', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { 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'); renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/dev-authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '初始化' })); expect(screen.getByText('project.create')).not.toBeNull(); expect(screen.getByText('创建 /tmp/dev-authorized-game')).not.toBeNull(); expect(confirm).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已初始化')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/dev-authorized-game', event: 'permission.confirm', commandId: 'project.create', }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/diff ../checkpoint-1'); expect(screen.getByText('checkpoint id 非法。')).not.toBeNull(); 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'], }, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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(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'], }; 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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'], }, }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/authorized-game', event: 'permission.confirm', commandId: 'project.policy_write', }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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'], }, }); 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: [], }, }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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: [], }, }); }); 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'], }, }); }); 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'], }, }); }); 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'], }, }); }); 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', ], }, }); }); 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', ], }, }); }); 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', ], }, }); }); 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', ], }, }); }); 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', ], }, }); }); 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', ], }, }); }); 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']), }), }); }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/policy-allow file.write'); expect( await screen.findByText('命令不在拒绝列表中:file.write'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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'], }, }); }); 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'], }, }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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'], }, }); }); 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 === '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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); submitChat('/policy-confirm file.write'); expect( await screen.findByText( '当前仅支持确认 project.index、project.status、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、agent.run_status、agent.kill、agent.retry、agent.resume、agent.audit、agent.trace_read、preview.status、preview.start、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 === '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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'read_project_permission_policy', ).length, ).toBeGreaterThanOrEqual(2); }); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('stops preview from the developer panel 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 === 'stop_local_game_preview') { return { status: 'stopped', url: null, port: null, root: null, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '初始化' })); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull(); }); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/other-game' }, }); fireEvent.click( within(screen.getByLabelText('预览')).getByRole('button', { name: '停止', }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('stop_local_game_preview', { projectPath: '/tmp/authorized-game', }); }); expect(invoke).not.toHaveBeenCalledWith('stop_local_game_preview', { projectPath: '/tmp/other-game', }); }); it('does not show preview status before a local project is initialized', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); 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', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); 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 === 'open_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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/open-preview'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开当前本地预览:http://127.0.0.1:3210/'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('open_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 === 'open_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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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( 'open_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('open_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 === 'open_local_game_preview') { throw new Error('should not open preview after cancel'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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( 'open_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 === 'open_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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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.getByText(/已交给外部浏览器打开。/)).not.toBeNull(); expect(screen.queryByTitle('本地游戏预览')).toBeNull(); expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('open_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 === 'open_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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('starts preview from the developer panel 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 === 'is_local_project_directory_non_empty') { return false; } 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 === 'start_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('/?dev'); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '初始化' })); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull(); }); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/other-game' }, }); fireEvent.click( within(screen.getByLabelText('预览')).getByRole('button', { name: '启动', }), ); expect( screen.getByText('启动 /tmp/authorized-game/game/ 并交给外部浏览器'), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { projectPath: '/tmp/authorized-game', }); }); expect(invoke).not.toHaveBeenCalledWith('start_local_game_preview', { projectPath: '/tmp/other-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('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'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/preview-status'); expect( await screen.findByText('预览运行中:http://127.0.0.1:3210/'), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '填入打开预览命令' })); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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 === 'open_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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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(screen.getByText(/已交给外部浏览器打开。/)).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).toHaveBeenCalledWith('open_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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findAllByText('本地项目路径无效')).toHaveLength(2); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); 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('runs limited commands from the developer panel through the authorized local project path', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'is_local_project_directory_non_empty') { return false; } 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 === '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('/?dev'); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '初始化' })); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull(); }); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/other-game' }, }); fireEvent.click(screen.getByRole('button', { name: '静态入口自检' })); expect( screen.getByText('运行 静态入口自检 于 /tmp/authorized-game'), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText(/static smoke passed/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('run_limited_local_command', { projectPath: '/tmp/authorized-game', commandId: 'game.static_smoke', }); expect(invoke).not.toHaveBeenCalledWith('run_limited_local_command', { projectPath: '/tmp/other-game', commandId: 'game.static_smoke', }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/authorized-game', event: 'permission.pending', commandId: 'command.run_limited', }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath: '/tmp/authorized-game', event: 'permission.confirm', commandId: 'command.run_limited', }); }); it('cancels limited commands from the developer panel without running them', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'is_local_project_directory_non_empty') { return false; } 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 === 'run_limited_local_command') { throw new Error('should not run limited command after cancel'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '初始化' })); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect(screen.getByText('已打开:/tmp/authorized-game')).not.toBeNull(); }); fireEvent.click(screen.getByRole('button', { name: '静态入口自检' })); const command = screen.getByText('command.run_limited'); fireEvent.click( within(command.closest('.pending-command') as HTMLElement).getByRole( 'button', { name: '取消' }, ), ); expect(screen.getByText('已取消运行')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'run_limited_local_command', expect.anything(), ); }); it('refreshes limited commands from the native runtime in the developer panel', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'is_local_project_directory_non_empty') { return false; } if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { return { projectPath: '/tmp/authorized-game', manifestPath: '/tmp/authorized-game/.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') { if (args?.relativePath === '.agent/logs/command.log') { return { path: '.agent/logs/command.log', absolutePath: '/tmp/authorized-game/.agent/logs/command.log', content: 'permission.pending preview.start\npermission.confirm preview.start\n', }; } if (args?.relativePath === '.agent/logs/preview.log') { return { path: '.agent/logs/preview.log', absolutePath: '/tmp/authorized-game/.agent/logs/preview.log', content: 'preview.start http://127.0.0.1:3210/\n', }; } if (args?.relativePath === '.agent/logs/agent.log') { return { path: '.agent/logs/agent.log', absolutePath: '/tmp/authorized-game/.agent/logs/agent.log', content: 'Planner 正在整理规格\nGenerator 已写入草案\n', }; } 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: '/tmp/authorized-game', files: [] }; } if (command === 'get_limited_local_commands') { return [{ id: 'game.custom_smoke', title: '自定义自检' }]; } if (command === 'run_limited_local_command') { return { commandId: 'game.custom_smoke', status: 'completed', output: 'custom smoke passed', logPath: '.agent/logs/custom.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('/?dev'); const logPanel = within(screen.getByLabelText('日志')); fireEvent.click(logPanel.getByRole('button', { name: '刷新' })); expect( await logPanel.findByRole('button', { name: '自定义自检' }), ).not.toBeNull(); expect(logPanel.queryByRole('button', { name: '静态入口自检' })).toBeNull(); expect(screen.getByText('已读取 1 个内置命令')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('get_limited_local_commands'); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '初始化' })); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); fireEvent.click(logPanel.getByRole('button', { name: '命令日志' })); expect( await screen.findByText('已读取:.agent/logs/command.log'), ).not.toBeNull(); expect( screen.getByText(/permission\.confirm preview\.start/), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/logs/command.log', commandId: 'file.read', }); fireEvent.click(logPanel.getByRole('button', { name: '预览日志' })); expect( await screen.findByText('已读取:.agent/logs/preview.log'), ).not.toBeNull(); expect( screen.getByText(/preview\.start http:\/\/127\.0\.0\.1:3210\//), ).not.toBeNull(); fireEvent.click(logPanel.getByRole('button', { name: 'Agent日志' })); expect( await screen.findByText('已读取:.agent/logs/agent.log'), ).not.toBeNull(); expect(screen.getByText(/Generator 已写入草案/)).not.toBeNull(); fireEvent.click(logPanel.getByRole('button', { name: '自定义自检' })); expect( screen.getByText('运行 自定义自检 于 /tmp/authorized-game'), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText(/custom smoke passed/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('run_limited_local_command', { projectPath: '/tmp/authorized-game', commandId: 'game.custom_smoke', }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/files'); expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); expect(screen.getByText(/- game\//)).not.toBeNull(); expect(screen.getByText(/- game\/index\.html/)).not.toBeNull(); expect(screen.getByText(/- assets\/uploads\/hero\.png/)).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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(); expect(screen.getByText(/- 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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 { configured: true, apiKeyPresent: false, baseUrl: 'https://llm.example.test/v1', model: 'gpt-test', apiKind: 'openai_responses', stream: false, error: null, agents: [ { agentId: 'planner', label: 'Planner', configured: true, apiKeyPresent: true, baseUrl: 'https://planner.example.test/v1', model: 'planner-model', apiKind: 'anthropic', stream: true, error: null, }, { agentId: 'generator', label: 'Generator', configured: false, apiKeyPresent: false, baseUrl: 'https://generator.example.test/v1', model: 'generator-model', apiKind: 'openai_chat', stream: false, error: 'LLM 未配置:请在 agentLlm.generator.apiKey 中设置 API Key', }, ], }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/llm-status'); expect(await screen.findByText(/LLM 已配置:gpt-test/)).not.toBeNull(); expect(screen.getByLabelText('聊天').textContent).toContain( 'LLM 已配置:gpt-test @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 未读取。', ); expect(screen.getByLabelText('聊天').textContent).toContain( 'Planner:已配置,planner-model @ https://planner.example.test/v1,anthropic,流式 开启,API Key 已读取', ); expect(screen.getByLabelText('聊天').textContent).toContain( 'Generator:未就绪,generator-model @ https://generator.example.test/v1,openai_chat,流式 关闭,API Key 未读取,错误:LLM 未配置:请在 agentLlm.generator.apiKey 中设置 API Key', ); 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 { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-main', apiKind: 'openai_responses', stream: false, error: null, agents: [ { agentId: 'planner', label: 'Planner', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-main', apiKind: 'openai_responses', stream: false, error: null, }, { agentId: 'generator', label: 'Generator', configured: true, apiKeyPresent: true, baseUrl: 'https://generator.example.test/v1', model: 'generator-model', apiKind: 'openai_chat', stream: true, error: null, }, { agentId: 'audio-sfx', label: '音效规划', configured: false, apiKeyPresent: false, baseUrl: 'https://llm.example.test/v1', model: 'gpt-main', apiKind: 'openai_responses', stream: false, error: 'LLM 未配置:请在 agentLlm.audio-sfx.apiKey 中设置 API Key', }, ], }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/llm-routes'); expect(await screen.findByText(/Agent LLM 路由:/)).not.toBeNull(); const chatText = screen.getByLabelText('聊天').textContent ?? ''; expect(chatText).toContain( '默认路由:gpt-main @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 已读取', ); expect(chatText).toContain('Agent:2/3 就绪 · 1 个单独路由 · 1 个缺口'); expect(chatText).toContain( 'Planner:已配置 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 已读取', ); expect(chatText).toContain( 'Generator:已配置 · 单独路由 · generator-model @ https://generator.example.test/v1,openai_chat,流式 开启,API Key 已读取', ); expect(chatText).toContain( '音效规划:未就绪 · 解析后与全局一致 · gpt-main @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 未读取 · 错误:LLM 未配置:请在 agentLlm.audio-sfx.apiKey 中设置 API Key', ); expect(chatText).toContain( '边界:只读取运行时配置解析结果;不请求上游;不显示 API Key;不写项目', ); 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(), ); }); it('checks LLM config from the main window shortcut without leaking the API key value', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-test', apiKind: 'openai_responses', stream: false, error: null, agents: [], }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); fireEvent.click(screen.getByRole('button', { name: 'LLM状态' })); expect(await screen.findByText(/LLM 已配置:gpt-test/)).not.toBeNull(); expect(screen.getByLabelText('聊天').textContent).toContain( 'LLM 已配置:gpt-test @ https://llm.example.test/v1,openai_responses,流式 关闭,API Key 已读取。', ); expect(screen.queryByText(/sk-test-secret/)).toBeNull(); expect(invoke).toHaveBeenCalledWith('check_game_creator_llm_config'); }); it('uses the authorized local project path for ordinary chat agent replies', 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_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_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'chat_with_game_creator_agent') { return { replyText: `主聊天回复:${String(args?.prompt ?? '')}`, }; } if (command !== 'init_local_game_project') { throw new Error(`unexpected invoke ${command}`); } const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:/tmp/authorized-game')).not.toBeNull(); await act(async () => { submitChat('做一个反弹弹幕厨房游戏'); }); expect( await screen.findByText('主聊天回复:做一个反弹弹幕厨房游戏'), ).not.toBeNull(); expect(screen.queryByText('game.generate_draft')).toBeNull(); expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_agent', { projectPath: '/tmp/authorized-game', prompt: '做一个反弹弹幕厨房游戏', }); expect(invoke).not.toHaveBeenCalledWith( 'generate_local_game_draft', expect.anything(), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', { projectPath: '/tmp/authorized-game', agentId: null, message: { role: 'user', content: '做一个反弹弹幕厨房游戏', agentId: null, }, }); }); }); it('streams agent generation progress into the user chat', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let progressHandler: | ((event: { payload: { projectPath: string; stage: string; message: string }; }) => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } 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( '读取文件元数据失败:/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 === '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}`); }, ); const listen = vi.fn( async (_event: string, handler: typeof progressHandler) => { progressHandler = handler; return vi.fn(); }, ); window.__TAURI__ = { core: { invoke }, event: { listen } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); await act(async () => { progressHandler?.({ payload: { projectPath: '/tmp/other-game', stage: 'llm.planner', message: '不应该显示', }, }); progressHandler?.({ payload: { projectPath: '/tmp/authorized-game', stage: 'llm.planner', message: 'Planner 正在调用 LLM 整理规格和专业组分工', }, }); }); expect(listen).toHaveBeenCalledWith( 'game-creator-agent-progress', expect.any(Function), ); expect(screen.queryByText('不应该显示')).toBeNull(); expect( screen.getByText('Planner 正在调用 LLM 整理规格和专业组分工'), ).not.toBeNull(); }); it('shows agent loop evidence in chat after generation completes', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const traceTasks = createGameCreationAppSeedTasks(); traceTasks[0]!.status = 'completed'; traceTasks[1]!.status = 'completed'; traceTasks[9]!.status = 'completed'; traceTasks[10]!.status = 'completed'; traceTasks[11]!.status = 'completed'; const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-chat-generate', commandId: 'game.generate_draft', status: 'passed', passes: 2, maxPasses: 3, toolCallCount: 36, maxToolCalls: 128, stopReason: 'evaluator-passed', goal: '做一个反弹弹幕厨房游戏', coordination: 'filesystem', steps: [ { pass: 0, agent: 'Planner', phase: 'planning', taskId: 'design-director', group: 'design', role: 'Director', status: 'completed', inputPaths: [ 'memory/session.md', 'memory/project.md', '.agent/manifest.json', ], outputPaths: ['.agent/spec.md'], summary: '完成玩法规格和专业组分工', toolCalls: [ { toolId: 'llm.chat.planner', status: 'completed', inputPaths: ['memory/session.md', 'memory/project.md'], outputPaths: ['.agent/spec.md'], summary: 'Planner 规格生成', }, ], }, { pass: 2, agent: 'Generator', phase: 'generate', taskId: 'code-prototype', group: 'code', role: 'Code', status: 'completed', inputPaths: [ '.agent/spec.md', '.agent/findings.md', '.agent/passes/pass-2/agenda.md', ], outputPaths: ['.agent/passes/pass-2/draft.json'], summary: '生成结构化游戏草案', toolCalls: [ { toolId: 'llm.chat.generator', status: 'completed', inputPaths: ['.agent/spec.md', '.agent/findings.md'], outputPaths: ['.agent/passes/pass-2/draft.json'], summary: 'Generator 草案生成', }, ], }, { pass: 2, agent: 'Evaluator', phase: 'evaluation', taskId: 'quality-review', group: 'code', role: 'Review', status: 'passed', inputPaths: ['.agent/passes/pass-2/draft.json'], outputPaths: ['.agent/findings.md'], summary: '质量评审通过', toolCalls: [ { toolId: 'evaluator.quality_review', status: 'completed', inputPaths: ['.agent/passes/pass-2/draft.json'], outputPaths: ['.agent/findings.md'], summary: 'Evaluator 质量评审', }, ], }, { pass: 2, agent: '美术组 / Asset', phase: 'role-brief', taskId: 'art-asset-plan', group: 'art', role: 'Asset', status: 'completed', inputPaths: ['.agent/manifest.json'], outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], summary: '需要回流画板角色素材', toolCalls: [ { toolId: 'agent.tool.suggest.canvas.project_sync', status: 'suggested', inputPaths: ['.agent/manifest.json'], outputPaths: [], summary: '项目还没有画板回流资产;建议用户确认 /sync-canvas-project <画板项目ID>。', }, ], }, ], artifacts: [ { path: 'game/index.html', sizeBytes: 1024, checksum: 'fnv1a64:game', }, { path: 'game/game_design.md', sizeBytes: 256, checksum: 'fnv1a64:design', }, { path: 'assets/manifest.art.json', sizeBytes: 128, checksum: 'fnv1a64:art', }, ], taskGraph: { goal: '做一个反弹弹幕厨房游戏', readyTaskIds: ['preview-playtest'], activeTaskIds: [ 'code-director', 'code-prototype', 'quality-review', 'preview-readiness', ], carriedTaskIds: ['design-director', 'design-foundation'], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', taskIds: [ 'code-director', 'code-prototype', 'quality-review', 'preview-readiness', ], reason: 'code-runtime', }, ], tasks: traceTasks, }, passPlans: [ { pass: 2, mode: 'repair', summary: '第 2 轮按 Evaluator 反馈返工', activeTaskIds: [ 'code-director', 'code-prototype', 'quality-review', 'preview-readiness', ], carriedTaskIds: ['design-director', 'design-foundation'], dependencyWaves: [ ['code-director'], ['code-prototype'], ['quality-review'], ['preview-readiness'], ], repairFocus: ['缺少输入监听'], repairRoutes: [ { issue: '缺少输入监听', taskIds: [ 'code-director', 'code-prototype', 'quality-review', 'preview-readiness', ], reason: 'code-runtime', }, ], }, ], nextStep: 'preview-playtest', error: null, updatedAt: 1, }; const generatedManifest = { ...manifest, goal: '做一个反弹弹幕厨房游戏', tasks: traceTasks, }; 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 === 'generate_local_game_draft') { return { projectPath: String(args?.projectPath ?? ''), gameIndexPath: `${String(args?.projectPath ?? '')}/game/index.html`, designPath: `${String(args?.projectPath ?? '')}/game/game_design.md`, shortMemoryPath: `${String(args?.projectPath ?? '')}/memory/session.md`, longMemoryPath: `${String(args?.projectPath ?? '')}/memory/project.md`, manifest: generatedManifest, }; } if (command === 'read_local_project_file') { return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(trace), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: '.agent/runs/run-chat-generate.json', kind: 'file', size: 2048, }, ], }; } if (command === 'start_local_game_preview') { return { status: 'running', url: 'http://127.0.0.1:3210/', port: 3210, root: String(args?.projectPath ?? ''), }; } if (command === 'open_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 generatedManifest; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/generate 做一个反弹弹幕厨房游戏'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText(/开始调用 LLM:Planner 正在整理规格。/), ).not.toBeNull(); expect(screen.getByText(/Generator 生成代码和资产清单/)).not.toBeNull(); expect( await screen.findByText( /已保存并启动本地预览:http:\/\/127\.0\.0\.1:3210\//, ), ).not.toBeNull(); expect(screen.getByText(/已交给外部浏览器打开。/)).not.toBeNull(); expect(screen.getByText(/Run:run-chat-generate/)).not.toBeNull(); expect( screen.getByText(/状态:passed · 2\/3 轮 · evaluator-passed/), ).not.toBeNull(); expect(screen.getByText(/工具调用:36\/128/)).not.toBeNull(); expect(screen.getByText(/LLM 对话:/)).not.toBeNull(); expect( screen.getByText( /Planner #0 · completed · planning · llm\.chat\.planner/, ), ).not.toBeNull(); expect( screen.getByText( /Generator #2 · completed · generate · llm\.chat\.generator/, ), ).not.toBeNull(); expect( screen.getByText( /active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/, ), ).not.toBeNull(); expect(screen.getByText(/carry-over 任务:策划组/)).not.toBeNull(); expect(screen.getByText(/返工焦点:缺少输入监听/)).not.toBeNull(); expect( screen.getByText(/agent\.tool\.suggest\.canvas\.project_sync/), ).not.toBeNull(); expect( screen.getByText(/\/sync-canvas-project <画板项目ID>/), ).not.toBeNull(); expect(screen.getByText(/编排轮次:/)).not.toBeNull(); expect(screen.getByText(/产物快照:/)).not.toBeNull(); expect( screen.getByText(/- game\/index\.html · fnv1a64:game/), ).not.toBeNull(); expect(screen.getByText(/最近步骤:/)).not.toBeNull(); expect( screen.getByText(/Evaluator #2 · passed · evaluation/), ).not.toBeNull(); expect(screen.getByText(/完整 trace:\/trace/)).not.toBeNull(); expect(screen.queryByLabelText('开发环境')).toBeNull(); expect(screen.queryByText('编排 Trace')).toBeNull(); expect(invoke).toHaveBeenCalledWith('generate_local_game_draft', { projectPath: '/tmp/authorized-game', prompt: '做一个反弹弹幕厨房游戏', }); expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/run.latest.json', commandId: 'agent.trace_read', }); expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { projectPath: '/tmp/authorized-game', }); expect(invoke).toHaveBeenCalledWith('open_local_game_preview', { projectPath: '/tmp/authorized-game', }); }); it('rejects unsafe generated project paths before preview side effects', 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 === 'generate_local_game_draft') { return { projectPath: 'relative-game', gameIndexPath: 'relative-game/game/index.html', designPath: 'relative-game/game/game_design.md', shortMemoryPath: 'relative-game/memory/session.md', longMemoryPath: 'relative-game/memory/project.md', manifest, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); submitChat('/generate 做一个厨房弹幕游戏'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('生成结果项目路径无效')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('generate_local_game_draft', { projectPath: '/tmp/authorized-game', prompt: '做一个厨房弹幕游戏', }); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'start_local_game_preview', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'open_local_game_preview', expect.anything(), ); }); it('opens runtime config when game generation is missing LLM configuration', 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 === 'generate_local_game_draft') { throw new Error( 'LLM 未配置:请在 /tmp/game-creator.config.json 的 llm.apiKey 中设置 API Key', ); } if (command === 'read_local_project_file') { throw new Error('trace missing'); } if (command === 'read_game_creator_app_config') { return { path: '/tmp/game-creator.config.json', config: { llm: { apiKey: '', baseUrl: 'https://api.example.test/v1', model: 'gpt-4.1', apiKind: 'openai_responses', stream: false, requestTimeoutMs: 60000, maxRetries: 0, retryBackoffMs: 500, }, editorApi: { baseUrl: 'https://editor.example.test', apiKey: '', }, }, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/generate 做一个厨房弹幕游戏'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText( 'LLM 未配置:请在 /tmp/game-creator.config.json 的 llm.apiKey 中设置 API Key', ), ).not.toBeNull(); expect( await screen.findByRole('dialog', { name: '运行时配置' }), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_game_creator_app_config'); }); 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('/'); 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], }); submitChat('/assets'); expect( await screen.findByText(/upload · assets\/uploads\/hero\.png · uploaded/), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '读取首个资产' })); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('confirms asset registration from the developer asset panel', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'register_local_asset') { const projectPath = String(args?.projectPath ?? ''); return { id: 'asset-registered', localPath: String(args?.localPath ?? ''), absolutePath: `${projectPath}/${String(args?.localPath ?? '')}`, manifestPath: `${projectPath}/.agent/manifest.json`, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'get_local_game_manifest') { return manifest; } throw new Error(`unexpected invoke ${command}`); }, ); const confirm = vi.spyOn(window, 'confirm'); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.click(screen.getByRole('button', { name: '登记资产' })); expect(screen.getByText('asset.register')).not.toBeNull(); expect( screen.getByText('登记 /tmp/genarrative-ai-game-draft/assets/hero.png'), ).not.toBeNull(); expect(confirm).not.toHaveBeenCalled(); 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/genarrative-ai-game-draft', localPath: 'assets/hero.png', kind: 'asset', mediaType: 'application/octet-stream', sourceKind: 'generated', canvasProjectId: '', resourceId: '', assetObjectId: '', taskId: '', prompt: '', model: '', }); }); it('cancels asset registration from the developer asset panel', () => { const invoke = vi.fn(async (command: string) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'register_local_asset') { throw new Error('should not register asset after cancel'); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); fireEvent.click(screen.getByRole('button', { name: '登记资产' })); const command = screen.getByText('asset.register'); 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(), ); }); it('rejects unsafe developer asset registration before confirmation or invoke', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: 'relative-project' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '登记资产' })); expect(screen.getByText('请提供本地项目绝对路径。')).not.toBeNull(); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/project' }, }); fireEvent.change(screen.getByLabelText('资产路径'), { target: { value: '../hero.png' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '登记资产' })); expect(screen.getByText('资产路径必须是项目内相对路径。')).not.toBeNull(); fireEvent.change(screen.getByLabelText('资产路径'), { target: { value: 'assets/hero.png' }, }); fireEvent.change(screen.getByLabelText('资产来源'), { target: { value: 'canvas' }, }); fireEvent.change(screen.getByLabelText('画板项目'), { target: { value: 'canvas-project-1' }, }); fireEvent.change(screen.getByLabelText('资源 ID'), { target: { value: '' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '登记资产' })); expect(screen.getByText('请提供资源 ID 或资产对象 ID。')).not.toBeNull(); expect(screen.queryByText('asset.register')).toBeNull(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); expect(invoke).not.toHaveBeenCalled(); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/read game/index.html'); expect(await screen.findByText(/文件:game\/index\.html/)).not.toBeNull(); expect(screen.getByText(/<\/canvas>/)).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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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(); expect(screen.getByText(/<\/canvas>/)).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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/read ../secret.txt'); expect(screen.getByText('文件路径必须是项目内相对路径。')).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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', }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); 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(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/output.jsonl', ); fireEvent.click(screen.getByRole('button', { name: '活动' })); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/activity.jsonl', ); fireEvent.click(screen.getByRole('button', { name: '上下文包' })); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/context.bundle.json', ); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.objectContaining({ relativePath: '.agent/context.bundle.json' }), ); 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(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/read .agent/output.jsonl', ); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.objectContaining({ relativePath: '.agent/output.jsonl' }), ); fireEvent.change(screen.getByLabelText('创作想法'), { target: { value: '' }, }); 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', }); 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, }); 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, }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); 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(composerInput).toHaveProperty('value', '/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('/'); 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('/'); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/memory'); expect(await screen.findByText(/长期记忆:/)).not.toBeNull(); 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', }); 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: '覆盖后的长期记忆', }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/memory typo'); expect( await screen.findByText('格式:/memory [short|long|blackboard]'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/memory blackboard'); expect(await screen.findByText(/黑板记忆:/)).not.toBeNull(); 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', }); 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: '统一使用俯视角', }); 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', }); }); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); 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('rejects invalid dev canvas project ids before confirmation', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.click(assetPanel.getByRole('button', { name: '打开画板' })); expect(screen.getByText('请提供画板项目 ID。')).not.toBeNull(); expect(screen.queryByText('canvas.project_open')).toBeNull(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); fireEvent.change(screen.getByLabelText('画板项目'), { target: { value: 'bad\u0007canvas' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '打开画板' })); expect(screen.getByText('画板项目 ID 不能包含控制字符。')).not.toBeNull(); expect(screen.queryByText('canvas.project_open')).toBeNull(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'append_local_permission_log', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'open_canvas_project', expect.anything(), ); }); it('rejects invalid dev canvas asset imports before confirmation', () => { const invoke = vi.fn(); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: 'relative-project' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '导入画板资产' })); expect(screen.getByText('请提供本地项目绝对路径。')).not.toBeNull(); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/project' }, }); fireEvent.change(screen.getByLabelText('资产路径'), { target: { value: '../hero.png' }, }); fireEvent.change(screen.getByLabelText('画板项目'), { target: { value: 'canvas-project-1' }, }); fireEvent.change(screen.getByLabelText('资源 ID'), { target: { value: 'resource-1' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '导入画板资产' })); expect( screen.getByText('画板资产路径必须是项目内相对路径。'), ).not.toBeNull(); fireEvent.change(screen.getByLabelText('资产路径'), { target: { value: 'assets/hero.png' }, }); fireEvent.change(screen.getByLabelText('画板项目'), { target: { value: 'bad\u0007canvas' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '导入画板资产' })); expect(screen.getByText('画板项目 ID 不能包含控制字符。')).not.toBeNull(); fireEvent.change(screen.getByLabelText('画板项目'), { target: { value: 'canvas-project-1' }, }); fireEvent.change(screen.getByLabelText('资源 ID'), { target: { value: '' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '导入画板资产' })); expect(screen.getByText('请提供资源 ID 或资产对象 ID。')).not.toBeNull(); expect(screen.queryByText('canvas.asset_import')).toBeNull(); expect(screen.queryByRole('button', { name: '确认' })).toBeNull(); expect(invoke).not.toHaveBeenCalled(); }); it('generates platform art assets from the developer asset panel after confirmation', async () => { 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 === 'generate_platform_art_asset') { return { id: 'generated-art-panel', localPath: 'assets/canvas-generated/generated-art-panel.png', absolutePath: '/tmp/project/assets/canvas-generated/generated-art-panel.png', manifestPath: '/tmp/project/.agent/manifest.json', }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/project' }, }); fireEvent.change(screen.getByLabelText('美术生成提示词'), { target: { value: '像素风厨房主角' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '生成美术资产' })); expect(screen.getByText('canvas.asset_generate')).not.toBeNull(); expect(screen.getByText('生成 /tmp/project 的首版美术素材')).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-panel.png', ), ).not.toBeNull(); expect( screen.getByText('assets/canvas-generated/generated-art-panel.png'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('generate_platform_art_asset', { projectPath: '/tmp/project', prompt: '像素风厨房主角', }); }); it('opens runtime config when platform art generation is missing configuration', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'generate_platform_art_asset') { throw new Error( 'LLM 未配置:请在 /tmp/game-creator.config.json 的 llm.apiKey 中设置 API Key', ); } if (command === 'read_game_creator_app_config') { return { path: '/tmp/game-creator.config.json', config: { llm: { apiKey: '', baseUrl: 'https://api.example.test/v1', model: 'gpt-image-2', apiKind: 'openai_responses', stream: true, requestTimeoutMs: 60000, maxRetries: 1, retryBackoffMs: 500, }, editorApi: { baseUrl: 'https://editor.example.test', apiKey: '', }, }, }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/project' }, }); fireEvent.change(screen.getByLabelText('美术生成提示词'), { target: { value: '像素风厨房主角' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '生成美术资产' })); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText( 'LLM 未配置:请在 /tmp/game-creator.config.json 的 llm.apiKey 中设置 API Key', ), ).not.toBeNull(); expect( await screen.findByRole('dialog', { name: '运行时配置' }), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_game_creator_app_config'); }); it('cancels pending platform art generation from the developer asset panel', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'generate_platform_art_asset') { throw new Error('should not generate art after cancel'); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/project' }, }); fireEvent.change(screen.getByLabelText('美术生成提示词'), { target: { value: '像素风厨房主角' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '生成美术资产' })); const assetCommand = screen.getByText('canvas.asset_generate'); fireEvent.click( within(assetCommand.closest('.pending-command') as HTMLElement).getByRole( 'button', { name: '取消' }, ), ); expect(await screen.findByText('已取消生成美术素材')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'generate_platform_art_asset', expect.anything(), ); }); it('syncs canvas project assets from the developer asset panel after confirmation', async () => { 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 === 'sync_canvas_project_assets') { return { canvasProjectId: String(args?.canvasProjectId ?? ''), importRoot: 'assets/canvas-sync/canvas-project-1-1', importedCount: 1, assets: [ { id: 'canvas-panel-1', localPath: 'assets/canvas-sync/canvas-project-1-1/res-1.png', absolutePath: '/tmp/project/assets/canvas-sync/canvas-project-1-1/res-1.png', manifestPath: '/tmp/project/.agent/manifest.json', }, ], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/project' }, }); fireEvent.change(screen.getByLabelText('画板项目'), { target: { value: 'canvas-project-1' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '同步画板项目' })); expect(screen.getByText('canvas.project_sync')).not.toBeNull(); expect( screen.getByText('同步画板项目资源:canvas-project-1'), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'sync_canvas_project_assets', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText( '已同步画板项目:1 个资产,assets/canvas-sync/canvas-project-1-1', ), ).not.toBeNull(); expect( screen.getByText('assets/canvas-sync/canvas-project-1-1/res-1.png'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('sync_canvas_project_assets', { projectPath: '/tmp/project', canvasProjectId: 'canvas-project-1', }); }); it('imports canvas export packages from the developer asset panel after confirmation', async () => { 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 === 'import_canvas_export') { return { canvasProjectId: String(args?.canvasProjectId ?? ''), importRoot: 'assets/canvas-imports/canvas-project-1-1', importedCount: 2, assets: [ { id: 'canvas-export-panel-1', localPath: 'assets/canvas-imports/canvas-project-1-1/export-1.png', absolutePath: '/tmp/project/assets/canvas-imports/canvas-project-1-1/export-1.png', manifestPath: '/tmp/project/.agent/manifest.json', }, ], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev'); const assetPanel = within(screen.getByLabelText('文件和资产')); fireEvent.change(screen.getByLabelText('本地项目目录'), { target: { value: '/tmp/project' }, }); fireEvent.change(screen.getByLabelText('画板项目'), { target: { value: 'canvas-project-1' }, }); fireEvent.change(screen.getByLabelText('画板导出 ZIP'), { target: { value: '/tmp/canvas-export.zip' }, }); fireEvent.click(assetPanel.getByRole('button', { name: '导入导出包' })); expect(screen.getByText('canvas.export_import')).not.toBeNull(); expect( screen.getByText('导入画板导出包:/tmp/canvas-export.zip'), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'import_canvas_export', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText( '已导入画板导出包:2 个资产,assets/canvas-imports/canvas-project-1-1', ), ).not.toBeNull(); expect( screen.getByText('assets/canvas-imports/canvas-project-1-1/export-1.png'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('import_canvas_export', { projectPath: '/tmp/project', exportPath: '/tmp/canvas-export.zip', canvasProjectId: 'canvas-project-1', }); }); it('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'); await screen.findByText('想做什么游戏?'); fireEvent.click(screen.getByRole('button', { name: '导入画板包' })); expect( await screen.findByText('已选择画板导出包:/tmp/canvas-export.zip'), ).not.toBeNull(); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); expect(await screen.findByText(/还没有最近一次 Agent run/)).not.toBeNull(); submitChat( '/import-canvas-asset /tmp/hero.png canvas-project-1 resource-1', ); expect( screen.getByText('画板资产路径必须是项目内相对路径。'), ).not.toBeNull(); submitChat('/import-canvas-asset ../hero.png canvas-project-1 resource-1'); expect( screen.getAllByText('画板资产路径必须是项目内相对路径。').length, ).toBe(2); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); submitChat('/canvas bad\u0007canvas'); submitChat('/sync-canvas-project bad\u0007canvas'); submitChat( '/import-canvas-asset assets/hero.png bad\u0007canvas resource-1', ); 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('/'); 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(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已打开:/tmp/authorized-game'), ).not.toBeNull(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); invoke.mockClear(); 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('/'); submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect( await screen.findByText('已设置本地项目:/tmp/authorized-game'), ).not.toBeNull(); 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', }); }); });