import fs from 'node:fs'; import { APP_NAME, APP_VERSION } from '../../src/app/appMetadata'; import { repoPath } from '../repoPath'; import { act, createGameCreationAppManifest, createGameCreationAppSeedTasks, deriveAgentStatusCards, expect, fireEvent, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, type GameCreationAgentRunTrace, it, renderLauncherAt, screen, vi, waitFor, within, } from './harness'; export function registerAgentStatusDerivationTests() { it('keeps fixed overlays below the in-page window title bar', () => { const styles = fs.readFileSync( repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toContain('--window-chrome-height: 50px;'); expect(styles).toContain('.fixed.inset-0 {'); expect(styles).toContain('top: var(--window-chrome-height);'); expect(styles).toContain('.settings-overlay {'); expect(styles).toContain('.launcher-dialog-backdrop {'); expect(styles).toContain('.game-approval-backdrop {'); expect(styles).toContain('> .launcher-shell .launcher-main {'); expect(styles).toContain('padding-bottom: 0;'); expect(styles).toContain('> .platform-theme {'); }); 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 runtimeTask = { schemaVersion: 'game-creator-agent-runtime-task.v1', agentId: 'art-asset-plan', taskId: 'art-asset-plan', sessionId: 'agent-session-art-asset-plan', runId: 'runtime-art-asset-plan-1', source: 'agent-background-task', task: '补齐主角规范图资产列表', status: 'pending', phase: 'queued', currentAction: '等待当前任务完成', error: null, updatedAt: 1234, }; const runtimeTaskQueue = { total: 1, pending: 1, running: 0, completed: 0, failed: 0, latestRunId: 'runtime-art-asset-plan-1', updatedAt: 1234, }; const cards = deriveAgentStatusCards(manifest, trace, { 'art-asset-plan': { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'art-asset-plan', taskId: 'art-asset-plan', sessionId: 'agent-session-art-asset-plan', runId: 'runtime-art-asset-plan-1', source: 'agent-background-task', status: 'running', phase: 'planning', currentTask: '补齐主角规范图资产列表', currentAction: '拆解素材规格', loopIteration: 2, maxLoopIterations: 3, toolActionBudget: 3, plan: ['读取项目上下文'], planSteps: [ { index: 0, title: '读取项目上下文', status: 'active', detail: '拆解素材规格', updatedAt: 1235, }, ], activePlanStepIndex: 0, observations: ['已创建本轮 Agent Runtime run。'], allowedTools: ['conversation.read'], lastResponse: null, error: null, updatedAt: 1234, taskQueue: runtimeTaskQueue, recentTasks: [runtimeTask], }, }); 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-plan')).toMatchObject({ taskId: 'art-asset-plan', status: 'failed', summary: '美术资产生成失败', hasRecentEvidence: true, runtimeStatus: 'running', runtimePhase: 'planning', runtimeAction: '拆解素材规格', runtimeTask: '补齐主角规范图资产列表', runtimeRunId: 'runtime-art-asset-plan-1', runtimeLoopIteration: 2, runtimeMaxLoopIterations: 3, runtimeToolActionBudget: 3, runtimeActivePlanStep: '#1 active · 读取项目上下文 · 拆解素材规格', runtimeTaskQueue, runtimeRecentTasks: [runtimeTask], }); expect(cards.find((card) => card.id === 'code-prototype')).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', }); }); } export function registerRuntimeSettingsTests() { it('distinguishes loading the client extension list from an empty list', async () => { let resolveExtensions!: (items: unknown[]) => void; const extensionsRead = new Promise((resolve) => { resolveExtensions = resolve; }); const invoke = vi.fn((command: string) => { if (command === 'read_game_creator_app_config') { return Promise.resolve({ path: '/home/test/AppData/game-creator.config.json', config: { agentMode: 'codex_app_server', llm: { apiKey: '', baseUrl: 'https://llm.example.test/v1', model: 'gpt-settings-extensions', apiKind: 'openai_responses', reasoningEffort: 'high', stream: false, webSearchEnabled: false, requestTimeoutMs: 180000, maxRetries: 0, retryBackoffMs: 500, }, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '', }, }, }); } if (command === 'list_client_extensions') { return extensionsRead; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '配置' })); await screen.findByRole('dialog', { name: '运行时配置' }); fireEvent.click(screen.getByRole('button', { name: /^扩展/ })); expect(await screen.findByText('正在加载扩展')).not.toBeNull(); expect(screen.queryByText('还没有导入扩展')).toBeNull(); await act(async () => { resolveExtensions([]); }); expect(await screen.findByText('还没有导入扩展')).not.toBeNull(); expect(screen.queryByText('正在加载扩展')).toBeNull(); }); it('shows the client version on the About settings page', 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: { agentMode: 'codex_app_server', llm: { apiKey: '', baseUrl: 'https://llm.example.test/v1', model: 'gpt-about', apiKind: 'openai_responses', reasoningEffort: 'high', stream: true, webSearchEnabled: false, contextWindowTokens: 128000, autoCompactTokenLimit: 64000, toolOutputTokenLimit: 12000, requestTimeoutMs: 180000, maxRetries: 2, retryBackoffMs: 500, }, agentLlm: {}, 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.click(screen.getByRole('button', { name: /关于/ })); expect(screen.getByText(APP_NAME)).not.toBeNull(); expect( screen.getByTestId('runtime-settings-app-logo').getAttribute('src'), ).toContain('taonier-product-ip.png'); expect(screen.getByTestId('runtime-settings-app-version').textContent).toBe( `v${APP_VERSION}`, ); expect(screen.getByText('桌面客户端')).not.toBeNull(); }); it('keeps the project creation directory in the workspace settings section', async () => { const creationDirectory = 'F:\\Projects\\陶泥儿游戏'; const storageKey = 'genarrative-ai-game-creator.project-creation-directory.v1'; const invoke = vi.fn(async (command: string) => { if (command === 'read_game_creator_app_config') { return { path: '/home/test/AppData/game-creator.config.json', config: { agentMode: 'codex_app_server', llm: { apiKey: '', baseUrl: 'https://llm.example.test/v1', model: 'gpt-workspace', apiKind: 'openai_responses', reasoningEffort: 'high', stream: true, webSearchEnabled: false, contextWindowTokens: 128000, autoCompactTokenLimit: 64000, toolOutputTokenLimit: 12000, requestTimeoutMs: 180000, maxRetries: 2, retryBackoffMs: 500, }, agentLlm: {}, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '' }, }, }; } if (command === 'pick_local_project_directory') { return creationDirectory; } 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: '运行时配置' }); fireEvent.click(within(dialog).getByRole('button', { name: /工作区/ })); // 不选目录时是默认位置,且本地不写任何偏好。 expect(within(dialog).getByText('默认位置')).not.toBeNull(); expect(window.localStorage.getItem(storageKey)).toBeNull(); fireEvent.click(within(dialog).getByRole('button', { name: '选择目录' })); expect(await within(dialog).findByText(creationDirectory)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('pick_local_project_directory', { title: '选择项目创建目录', }); expect(window.localStorage.getItem(storageKey)).toBe( JSON.stringify(creationDirectory), ); expect(within(dialog).getByText('已更新项目创建目录')).not.toBeNull(); fireEvent.click( within(dialog).getByRole('button', { name: '恢复默认位置' }), ); expect(within(dialog).getByText('默认位置')).not.toBeNull(); expect(window.localStorage.getItem(storageKey)).toBeNull(); expect(within(dialog).getByText('已恢复默认位置')).not.toBeNull(); }); it('locks the Agent mode and official LLM route while dropping legacy credentials', 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: { agentMode: 'codex_app_server', llm: { apiKey: 'preserved-provider-key', baseUrl: 'https://llm.example.test/v1', model: 'preserved-provider-model', apiKind: 'openai_responses', reasoningEffort: 'high', stream: false, webSearchEnabled: false, contextWindowTokens: 128000, autoCompactTokenLimit: 64000, toolOutputTokenLimit: 12000, requestTimeoutMs: 180000, maxRetries: 2, retryBackoffMs: 500, }, agentLlm: {}, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '', }, }, }; } 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: '配置' })); expect(await screen.findByText('陶泥儿智能创作(固定)')).not.toBeNull(); expect(screen.queryByLabelText('Agent 模式')).toBeNull(); expect(screen.getByText('官方账号服务(固定)')).not.toBeNull(); expect( screen.queryByText(/router\.genarrative\.world|gpt-5\.6-sol/), ).toBeNull(); expect(screen.queryByLabelText('LLM Provider')).toBeNull(); expect(screen.queryByLabelText('LLM API Key')).toBeNull(); expect(screen.queryByLabelText('LLM Base URL')).toBeNull(); expect(screen.queryByLabelText('LLM 模型')).toBeNull(); expect(screen.queryByLabelText('LLM API 类型')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', { config: expect.objectContaining({ agentMode: 'codex_app_server', llm: expect.objectContaining({ apiKey: '', baseUrl: '', model: '', apiKind: 'openai_responses', }), agentLlm: {}, }), }); }); }); it('migrates an unknown Agent mode to the fixed app-server mode on save', 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: { agentMode: 'future_runtime_mode', llm: { apiKey: '', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1', apiKind: 'openai_responses', reasoningEffort: 'high', stream: false, webSearchEnabled: false, contextWindowTokens: 128000, autoCompactTokenLimit: 64000, toolOutputTokenLimit: 12000, requestTimeoutMs: 180000, maxRetries: 2, retryBackoffMs: 500, }, agentLlm: {}, editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '', }, }, }; } 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: '配置' })); expect(await screen.findByText('陶泥儿智能创作(固定)')).not.toBeNull(); expect(screen.queryByLabelText('Agent 模式')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', { config: expect.objectContaining({ agentMode: 'codex_app_server', }), }); }); }); it('edits runtime tuning from the launcher without exposing provider fields', 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: { agentMode: 'provider', llm: { apiKey: 'launcher-loaded-secret', baseUrl: 'https://llm.example.test/v1', model: 'gpt-launcher', apiKind: 'legacy', stream: false, webSearchEnabled: 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(screen.getByText('官方账号服务(固定)')).not.toBeNull(); expect( screen.queryByText(/router\.genarrative\.world|gpt-5\.6-sol/), ).toBeNull(); expect(screen.queryByLabelText('LLM Provider')).toBeNull(); expect(screen.queryByLabelText('LLM API Key')).toBeNull(); expect(screen.queryByLabelText('LLM Base URL')).toBeNull(); expect(screen.queryByLabelText('LLM 模型')).toBeNull(); expect(screen.queryByLabelText('LLM API 类型')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: /高级参数/ })); expect(screen.getByLabelText('请求超时 ms')).toHaveProperty( 'value', '1000', ); expect(screen.getByLabelText('重试次数')).toHaveProperty('value', '0'); expect(screen.getByLabelText('重试退避 ms')).toHaveProperty('value', '1'); expect(screen.getByLabelText('上下文窗口 tokens')).toHaveProperty( 'value', '128000', ); expect(screen.getByLabelText('自动压缩阈值 tokens')).toHaveProperty( 'value', '64000', ); expect(screen.getByLabelText('工具输出上限 tokens')).toHaveProperty( 'value', '12000', ); fireEvent.click(screen.getByRole('button', { name: '保存' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', { config: expect.objectContaining({ llm: expect.objectContaining({ apiKey: '', baseUrl: '', model: '', apiKind: 'openai_responses', requestTimeoutMs: 1000, maxRetries: 0, retryBackoffMs: 1, }), }), }); }); expect( ( await screen.findByText( '已保存:/home/test/AppData/game-creator.config.json', ) ).getAttribute('data-tone'), ).toBe('success'); fireEvent.mouseDown(dialog.parentElement as HTMLElement); expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull(); }); it('locks page scrolling while runtime settings are open and restores it after close', 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: { agentMode: 'codex_app_server', llm: { apiKey: '', baseUrl: 'https://llm.example.test/v1', model: 'gpt-scroll-lock', apiKind: 'openai_responses', stream: true, webSearchEnabled: false, requestTimeoutMs: 180000, maxRetries: 2, retryBackoffMs: 500, }, }, }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; document.documentElement.style.overflow = 'auto'; document.body.style.overflow = 'scroll'; renderLauncherAt('/?launcher'); fireEvent.click(screen.getByRole('button', { name: '配置' })); await screen.findByRole('dialog', { name: '运行时配置' }); expect(document.documentElement.style.overflow).toBe('hidden'); expect(document.body.style.overflow).toBe('hidden'); fireEvent.click(screen.getByRole('button', { name: '关闭 Agent 设置' })); expect(document.documentElement.style.overflow).toBe('auto'); expect(document.body.style.overflow).toBe('scroll'); }); 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: { agentMode: 'provider', llm: { apiKey: '', baseUrl: 'https://llm.example.test/v1', model: 'gpt-test', apiKind: 'openai_responses', stream: false, webSearchEnabled: 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('联网检索'), { 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: { agentMode: 'provider'; llm: { apiKey: string; baseUrl: string; model: string; apiKind: string; stream: boolean; webSearchEnabled: boolean; requestTimeoutMs: number; maxRetries: number; retryBackoffMs: number; }; editorApi: { baseUrl: string; apiKey: string }; }; }) => void) | undefined; let readCount = 0; let modelReadResolved = false; const invoke = vi.fn((command: string) => { if (command === 'read_game_creator_app_config') { readCount += 1; // 首页模型选择器会在挂载时读取一次配置;让首次读取立即完成, // 以免一直处于 pending 干扰运行时配置对话框的读取计数。 if (!modelReadResolved) { modelReadResolved = true; return Promise.resolve({ config: { selectedModelId: 'quality' } }); } return new Promise((resolve) => { resolveRead = resolve as typeof resolveRead; }); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAt('/?launcher'); // 首页模型选择器挂载后会在目录请求之后读取一次配置,等它完成再打开对话框。 await waitFor(() => expect(readCount).toBe(1)); 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, ); // 首页模型选择器挂载时会读取一次配置(上面已让首次读取立即完成), // 这里只校验:对话框处于「正在读取」时点击读取/保存不会新增读取请求。 const readsWhileReading = readCount; fireEvent.click(screen.getByRole('button', { name: '读取' })); fireEvent.click(screen.getByRole('button', { name: '保存' })); expect(readCount).toBe(readsWhileReading); await act(async () => { resolveRead?.({ path: '/home/test/AppData/game-creator.config.json', config: { agentMode: 'provider', llm: { apiKey: '', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1', apiKind: 'openai_responses', stream: false, webSearchEnabled: 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, ); }); }