import { act, agentRuntimeUserInputRequest, cleanup, createGameCreationAppManifest, createGameCreationAppSeedTasks, emptyProjectPolicy, expect, fireEvent, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, type GameCreationAgentRunTrace, it, renderAppAt, renderLauncherAgentChatAt, screen, selectDeveloperAgentChatMode, submitChat, vi, waitFor, within, } from './harness'; export function registerDeveloperAgentWindowTests() { it('opens the standalone Project Supervisor chat window with the absolute project path', async () => { const projectPath = '/tmp/supervisor-chat-window-game'; 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', reasoningEffort: 'high', stream: true, webSearchEnabled: false, error: null, agents: [], }; } if (command === 'open_project_supervisor_chat_window') { return null; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '总控对话' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'open_project_supervisor_chat_window', { projectPath }, ); }); expect(screen.getByText('已打开项目总控对话')).not.toBeNull(); }); it('persists and shows an upstream stream error without issuing a normal retry', async () => { const persistedMessages: Array<{ role: 'user' | 'assistant' | 'tool'; content: string; agentId: string | null; }> = []; const upstreamError = '上游余额不足'; const persistedError = `已保存用户消息;Agent 回复失败:${upstreamError}`; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages, }; } if (command === 'chat_with_game_creator_role_agent_stream') { throw new Error(upstreamError); } if (command === 'chat_with_game_creator_role_agent') { throw new Error('upstream errors must not issue a normal retry'); } if (command === 'append_local_conversation_message') { persistedMessages.push( args?.message as { role: 'user' | 'assistant' | 'tool'; content: string; agentId: string | null; }, ); return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: index + 1, })), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: vi.fn(async () => () => {}) }, }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '不要重复请求上游' }, }); selectDeveloperAgentChatMode('chat'); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect( await within(screen.getByLabelText('Agent 聊天记录')).findByText( persistedError, ), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_role_agent', expect.anything(), ); expect(invoke).toHaveBeenCalledWith( 'chat_with_game_creator_role_agent_stream', expect.objectContaining({ projectPath: '/tmp/authorized-game', agentId: 'design-director', prompt: '不要重复请求上游', }), ); expect(persistedMessages.at(-1)).toEqual({ role: 'assistant', content: persistedError, agentId: null, }); }); it('keeps the launcher usable and persists a normal reply when runtime listens reject', async () => { const persistedMessages: Array<{ role: 'user' | 'assistant' | 'tool'; content: string; agentId: string | null; }> = []; let releaseNormalReply: (() => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'list_game_creator_agent_sessions') { return { activeSessionId: null, sessions: [], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: index + 1, })), }; } if (command === 'read_game_creator_agent_runtime') { throw new Error('runtime snapshot unavailable'); } if (command === 'chat_with_game_creator_role_agent') { await new Promise((resolve) => { releaseNormalReply = resolve; }); return { replyText: '普通回复已落盘。' }; } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant' | 'tool'; content: string; agentId: string | null; }; persistedMessages.push(message); return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((record, index) => ({ schemaVersion: '1', ...record, updatedAt: index + 1, })), }; } throw new Error(`unexpected invoke ${command}`); }, ); const listen = vi.fn(async () => { throw new Error('core:event:allow-listen denied'); }); window.__TAURI__ = { core: { invoke }, event: { listen } }; renderLauncherAgentChatAt('/?agent-chat'); expect( await screen.findByText( 'Runtime 订阅不可用:core:event:allow-listen denied', ), ).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '监听失败后继续回复' }, }); selectDeveloperAgentChatMode('chat'); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect( await screen.findAllByText('实时状态不可用,正在使用普通回复模式'), ).not.toHaveLength(0); expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', { projectPath: '/tmp/authorized-game', agentId: 'design-director', prompt: '监听失败后继续回复', }); await act(async () => { releaseNormalReply?.(); }); expect(await screen.findByText('普通回复已落盘。')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', { projectPath: '/tmp/authorized-game', agentId: 'design-director', message: { role: 'assistant', content: '普通回复已落盘。', agentId: null, }, }); }); it('asks before recovering interrupted runtime tasks in the developer Agent chat', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'resume_game_creator_agent_runtime_tasks') { throw new Error('项目权限策略要求用户确认:agent.resume'); } if (command === 'confirm_resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); const detail = await screen.findByText( '恢复 /tmp/authorized-game 中未完成的 Agent Runtime 任务', ); const confirmation = detail.closest('.pending-command'); expect(confirmation).not.toBeNull(); const resumeSlot = confirmation?.closest('.launcher-agent-resume-slot'); expect(resumeSlot).not.toBeNull(); const agentChatMain = confirmation?.closest('.launcher-agent-chat-main'); const chatMessages = agentChatMain?.querySelector( '.launcher-agent-chat-messages', ); expect(chatMessages).not.toBeNull(); expect( Array.from(agentChatMain?.children ?? []).indexOf( resumeSlot as HTMLElement, ), ).toBeLessThan( Array.from(agentChatMain?.children ?? []).indexOf( chatMessages as HTMLElement, ), ); expect(screen.getByText('agent.resume')).not.toBeNull(); expect( invoke.mock.calls.filter( ([command]) => command === 'confirm_resume_game_creator_agent_runtime_tasks', ), ).toHaveLength(0); fireEvent.click( within(confirmation as HTMLElement).getByRole('button', { name: '确认', }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'confirm_resume_game_creator_agent_runtime_tasks', { projectPath: '/tmp/authorized-game' }, ); expect( screen.queryByText( '恢复 /tmp/authorized-game 中未完成的 Agent Runtime 任务', ), ).toBeNull(); expect(screen.getByText('已确认恢复 Agent Runtime 任务')).not.toBeNull(); }); }); it('does not reload an old developer Agent project after recovery returns late', async () => { let resolveConfirmedResume: ((value: unknown[]) => void) | null = null; const confirmedResume = new Promise((resolve) => { resolveConfirmedResume = resolve; }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'resume_game_creator_agent_runtime_tasks') { throw new Error('项目权限策略要求用户确认:agent.resume'); } if (command === 'confirm_resume_game_creator_agent_runtime_tasks') { return confirmedResume; } if (command === 'read_local_conversation') { return { path: `${String(args?.projectPath ?? '')}/.agent/conversations/agents/design-director.jsonl`, agentId: args?.agentId, messages: [], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/project-a' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); const detail = await screen.findByText( '恢复 /tmp/project-a 中未完成的 Agent Runtime 任务', ); fireEvent.click( within(detail.closest('.pending-command') as HTMLElement).getByRole( 'button', { name: '确认' }, ), ); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/project-b' }, }); await act(async () => { resolveConfirmedResume?.([]); await confirmedResume; }); expect(screen.getAllByText('请选择并读取会话').length).toBeGreaterThan(0); expect( invoke.mock.calls.filter( ([command, args]) => command === 'read_local_conversation' && args?.projectPath === '/tmp/project-a', ), ).toHaveLength(1); }); it('creates, forks, switches, archives, and isolates developer Agent sessions', async () => { type SessionRecord = { sessionId: string; title: string; createdAt: number; updatedAt: number; archivedAt: number | null; messageCount: number; legacy: boolean; forkedFromSessionId?: string | null; forkedMessageCount?: number | null; }; const legacySessionId = 'agent-session-design-director'; const roleSessionId = 'agent-session-design-director-role-spec'; const createdSessionId = 'agent-session-design-director-created'; const forkedSessionId = 'agent-session-design-director-forked'; const archivedForkedSessionId = 'agent-session-design-director-archived-forked'; let activeSessionId = roleSessionId; let forkInvocationCount = 0; const sessions: SessionRecord[] = [ { sessionId: legacySessionId, title: '默认会话', createdAt: 1, updatedAt: 1, archivedAt: null, messageCount: 1, legacy: true, }, { sessionId: roleSessionId, title: '角色规范', createdAt: 2, updatedAt: 2, archivedAt: null, messageCount: 1, legacy: false, }, ]; const messages = new Map>([ [legacySessionId, [{ role: 'assistant', content: '默认会话历史' }]], [roleSessionId, [{ role: 'assistant', content: '角色规范历史' }]], ]); const sessionResult = () => ({ path: '/tmp/authorized-game/.agent/runtime/sessions/design-director.json', agentId: 'design-director', activeSessionId, sessions: sessions.map((session) => ({ ...session, messageCount: messages.get(session.sessionId)?.length ?? 0, })), }); const conversationResult = (sessionId: string) => ({ path: sessionId === legacySessionId ? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl' : `/tmp/authorized-game/.agent/conversations/agents/design-director/sessions/${sessionId}.jsonl`, agentId: 'design-director', sessionId, messages: (messages.get(sessionId) ?? []).map((message, index) => ({ schemaVersion: 'game-creator-conversation.v1', ...message, agentId: 'design-director', updatedAt: index + 1, })), }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'list_game_creator_agent_sessions') { return sessionResult(); } if (command === 'set_active_game_creator_agent_session') { activeSessionId = String(args?.sessionId); return sessionResult(); } if (command === 'create_game_creator_agent_session') { activeSessionId = createdSessionId; sessions.push({ sessionId: createdSessionId, title: '新会话', createdAt: 3, updatedAt: 3, archivedAt: null, messageCount: 0, legacy: false, }); messages.set(createdSessionId, []); return sessionResult(); } if (command === 'fork_game_creator_agent_session') { const sourceSessionId = String(args?.sourceSessionId); const source = sessions.find( (candidate) => candidate.sessionId === sourceSessionId, ); const nextForkedSessionId = forkInvocationCount === 0 ? forkedSessionId : archivedForkedSessionId; activeSessionId = nextForkedSessionId; forkInvocationCount += 1; sessions.push({ sessionId: nextForkedSessionId, title: `分支:${source?.title ?? '会话'}`, createdAt: 3, updatedAt: 3, archivedAt: null, messageCount: messages.get(sourceSessionId)?.length ?? 0, legacy: false, forkedFromSessionId: sourceSessionId, forkedMessageCount: messages.get(sourceSessionId)?.length ?? 0, }); messages.set(nextForkedSessionId, [ ...(messages.get(sourceSessionId) ?? []), ]); return sessionResult(); } if (command === 'archive_game_creator_agent_session') { const session = sessions.find( (candidate) => candidate.sessionId === args?.sessionId, ); if (session) { session.archivedAt = 4; } activeSessionId = legacySessionId; return sessionResult(); } if (command === 'read_local_conversation') { return conversationResult(String(args?.sessionId ?? legacySessionId)); } if (command === 'append_local_conversation_message') { const sessionId = String(args?.sessionId ?? legacySessionId); const message = args?.message as { role: string; content: string }; messages.get(sessionId)?.push(message); return conversationResult(sessionId); } if (command === 'chat_with_game_creator_role_agent') { return { replyText: '新会话回复' }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText('角色规范历史')).not.toBeNull(); expect(screen.queryByText('默认会话历史')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: /默认会话/ })); expect(await screen.findByText('默认会话历史')).not.toBeNull(); expect(screen.queryByText('角色规范历史')).toBeNull(); expect(invoke).toHaveBeenCalledWith( 'set_active_game_creator_agent_session', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId: legacySessionId, }, ); fireEvent.click( screen.getByRole('button', { name: '分叉当前 Agent 会话' }), ); expect(await screen.findByText('默认会话历史')).not.toBeNull(); expect(await screen.findByText(/分支自 默认会话(1 条)/)).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('fork_game_creator_agent_session', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sourceSessionId: legacySessionId, title: '', }); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '只属于分叉会话的问题' }, }); selectDeveloperAgentChatMode('chat'); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect(await screen.findByText('只属于分叉会话的问题')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId: forkedSessionId, prompt: '只属于分叉会话的问题', }); fireEvent.click(screen.getByRole('button', { name: /^默认会话/ })); expect(await screen.findByText('默认会话历史')).not.toBeNull(); expect(screen.queryByText('只属于分叉会话的问题')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: /^分支:默认会话/ })); expect(await screen.findByText('只属于分叉会话的问题')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '新建 Agent 会话' })); expect(await screen.findByText('暂无对话')).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '只属于新会话的问题' }, }); selectDeveloperAgentChatMode('chat'); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect(await screen.findByText('新会话回复')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId: createdSessionId, prompt: '只属于新会话的问题', }); fireEvent.click( screen.getByRole('button', { name: '归档当前 Agent 会话' }), ); expect(await screen.findByText('默认会话历史')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('archive_game_creator_agent_session', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId: createdSessionId, }); fireEvent.click(screen.getByRole('button', { name: /新会话\s+已归档/ })); expect(await screen.findByText('只属于新会话的问题')).not.toBeNull(); expect(screen.getByLabelText('Agent 聊天内容')).toHaveProperty( 'disabled', true, ); fireEvent.click( screen.getByRole('button', { name: '分叉当前 Agent 会话' }), ); expect(await screen.findByText(/分支自 新会话/)).not.toBeNull(); expect(screen.getByLabelText('Agent 聊天内容')).toHaveProperty( 'disabled', false, ); expect(invoke).toHaveBeenCalledWith('fork_game_creator_agent_session', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sourceSessionId: createdSessionId, title: '', }); const refreshCallStart = invoke.mock.calls.length; fireEvent.click(screen.getByRole('button', { name: '刷新状态' })); expect(await screen.findByText('只属于新会话的问题')).not.toBeNull(); await waitFor(() => { const runtimeReads = invoke.mock.calls .slice(refreshCallStart) .filter(([command]) => command === 'read_game_creator_agent_runtime'); expect(runtimeReads.at(-1)).toEqual([ 'read_game_creator_agent_runtime', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId: archivedForkedSessionId, }, ]); }); }); it('does not downgrade a real Agent session catalog error to legacy writes', async () => { const invoke = vi.fn(async (command: string) => { if (command === 'list_game_creator_agent_sessions') { throw new Error('读取 Agent Session 目录失败:permission denied'); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect( await screen.findAllByText( 'Agent 会话列表读取失败:读取 Agent Session 目录失败:permission denied', ), ).toHaveLength(2); expect(invoke).not.toHaveBeenCalledWith( 'read_local_conversation', expect.anything(), ); expect( screen.getByRole('button', { name: '新建 Agent 会话' }), ).toHaveProperty('disabled', true); }); it('shows developer agent chat LLM configuration gaps before sending', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: false, apiKeyPresent: false, baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1', apiKind: 'openai_responses', stream: false, webSearchEnabled: false, error: 'LLM 未配置:请在 game-creator.config.json 的 llm.apiKey 中设置 API Key', agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: false, apiKeyPresent: false, baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1', apiKind: 'openai_responses', stream: false, webSearchEnabled: false, error: 'LLM 未配置:请在 agentLlm.design-director.apiKey 中设置 API Key', }, ], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); const warning = await screen.findByRole('status'); expect(warning.textContent).toContain( '当前 Agent 智能服务未就绪:官方智能服务暂不可用,请稍后重试', ); expect(screen.getByLabelText('Agent 聊天内容')).toHaveProperty( 'disabled', true, ); expect(screen.getByRole('button', { name: '发送' })).toHaveProperty( 'disabled', true, ); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_role_agent', expect.anything(), ); }); it('streams developer agent chat status and draft reply before persisting', async () => { const persistedMessages: Array<{ role: 'user' | 'assistant'; content: string; agentId: string | null; }> = []; let releaseFirstDelta: (() => void) | null = null; let releaseStream: (() => void) | null = null; let streamHandler: | ((event: { payload: Record }) => void) | null = null; const startedRuntimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-chat-test', source: 'agent-chat', status: 'running', phase: 'llm', currentTask: '请流式回答', currentGoal: '请流式回答', currentAction: '请求 Agent LLM', waitingOn: 'Agent LLM 回复', plan: ['读取项目上下文', '按角色职责推理', '流式回复'], observations: ['已创建本轮 Agent Runtime run。'], allowedTools: ['conversation.read', 'conversation.write'], lastResponse: null, error: null, updatedAt: 3000, }; const completedRuntimeState = { ...startedRuntimeState, status: 'idle', phase: 'completed', currentAction: '等待下一轮输入', waitingOn: '开发者下一轮输入', lastResponse: '专业 Agent 已流式完成。', observations: [ '已创建本轮 Agent Runtime run。', 'Agent 已完成回复,assistant 消息等待或已经由前端落盘。', ], updatedAt: 3001, }; const loadedRuntimeEvents = [ { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-chat-loaded', source: 'agent-chat', eventType: 'response', status: 'idle', phase: 'completed', summary: '上一轮已完成回复', detail: null, updatedAt: 3002, }, ]; const listen = vi.fn( async ( eventName: string, handler: (event: { payload: Record }) => void, ) => { if (eventName === 'game-creator-role-agent-chat-stream') { streamHandler = handler; return () => { if (streamHandler === handler) { streamHandler = null; } }; } if (eventName === 'game-creator-agent-runtime-update') { return () => {}; } throw new Error(`unexpected listen ${eventName}`); }, ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: 1000 + index, })), }; } if (command === 'read_game_creator_agent_runtime') { return { state: completedRuntimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', recentEvents: loadedRuntimeEvents, }; } if (command === 'chat_with_game_creator_role_agent_stream') { const payloadBase = { projectPath: args?.projectPath, agentId: args?.agentId, runId: args?.runId, finishReason: null, }; streamHandler?.({ payload: { ...payloadBase, status: 'started', deltaText: '', accumulatedText: '', runtimeSummary: '请求 Agent LLM', runtimeState: startedRuntimeState, }, }); await new Promise((resolve) => { releaseFirstDelta = resolve; }); streamHandler?.({ payload: { ...payloadBase, status: 'delta', deltaText: '专业', accumulatedText: '专业', }, }); streamHandler?.({ payload: { ...payloadBase, status: 'delta', deltaText: ' Agent', accumulatedText: '专业 Agent', }, }); await new Promise((resolve) => { releaseStream = resolve; }); streamHandler?.({ payload: { ...payloadBase, status: 'completed', deltaText: '', accumulatedText: '专业 Agent 已流式完成。', runtimeSummary: '等待下一轮输入', runtimeState: completedRuntimeState, }, }); return { replyText: '专业 Agent 已流式完成。', }; } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }; persistedMessages.push(message); return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((record, index) => ({ schemaVersion: '1', ...record, updatedAt: 2000 + index, })), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke }, event: { listen } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '请流式回答' }, }); selectDeveloperAgentChatMode('chat'); fireEvent.click(screen.getByRole('button', { name: '发送' })); const waitingForFirstDelta = await within( screen.getByLabelText('Agent 聊天记录'), ).findByRole('status'); expect(waitingForFirstDelta.textContent).toContain('已连接 Agent LLM'); expect(waitingForFirstDelta.textContent).toContain('请求仍在进行中'); expect(screen.getByLabelText('Agent 聊天记录').getAttribute('role')).toBe( 'log', ); expect( screen.getByLabelText('Agent 聊天记录').getAttribute('aria-busy'), ).toBe('true'); expect(screen.getByLabelText('Agent 聊天记录').tabIndex).toBe(0); await act(async () => { releaseFirstDelta?.(); }); expect(await screen.findByText('专业 Agent')).not.toBeNull(); expect(await screen.findByLabelText('Agent Runtime 状态')).not.toBeNull(); expect(screen.getByText('running / llm')).not.toBeNull(); expect(screen.getByText('agent-session-design-director')).not.toBeNull(); expect(screen.getByText('当前目标:请流式回答')).not.toBeNull(); expect(screen.getAllByText('请求 Agent LLM').length).toBeGreaterThan(0); expect(screen.getByText('等待:Agent LLM 回复')).not.toBeNull(); expect(screen.getByText('最近事件')).not.toBeNull(); expect( screen.getByText('response · idle / completed · 上一轮已完成回复'), ).not.toBeNull(); expect(screen.getAllByText('正在接收 Agent 回复').length).toBeGreaterThan( 0, ); const waitingStatus = within( screen.getByLabelText('Agent 聊天记录'), ).getByRole('status'); expect(waitingStatus.textContent).toContain('正在接收 Agent 回复'); expect(invoke).toHaveBeenCalledWith( 'chat_with_game_creator_role_agent_stream', expect.objectContaining({ projectPath: '/tmp/authorized-game', agentId: 'design-director', prompt: '请流式回答', }), ); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_role_agent', expect.anything(), ); await act(async () => { releaseStream?.(); }); expect(await screen.findByText('专业 Agent 已流式完成。')).not.toBeNull(); expect(await screen.findByText('idle / completed')).not.toBeNull(); expect(screen.getAllByText('等待下一轮输入').length).toBeGreaterThan(0); expect(screen.getByText('等待:开发者下一轮输入')).not.toBeNull(); expect(await screen.findByText(/已保存 2 条/)).not.toBeNull(); expect( within(screen.getByLabelText('Agent 聊天记录')).queryByRole('status'), ).toBeNull(); expect( screen.getByLabelText('Agent 聊天记录').getAttribute('aria-busy'), ).toBe('false'); expect(persistedMessages).toEqual([ { role: 'user', content: '请流式回答', agentId: null, }, { role: 'assistant', content: '专业 Agent 已流式完成。', agentId: null, }, ]); }); it('follows new agent chat messages near the bottom without stealing an intentional scroll position', async () => { const persistedMessages: Array<{ role: 'user' | 'assistant'; content: string; agentId: string | null; }> = [ { role: 'assistant', content: '历史消息', agentId: null, }, ]; let streamHandler: | ((event: { payload: Record }) => void) | null = null; let streamRunId = ''; let releaseStream: (() => void) | null = null; const listen = vi.fn( async ( eventName: string, handler: (event: { payload: Record }) => void, ) => { if (eventName === 'game-creator-role-agent-chat-stream') { streamHandler = handler; return () => { if (streamHandler === handler) { streamHandler = null; } }; } return () => {}; }, ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: index + 1, })), }; } if (command === 'chat_with_game_creator_role_agent_stream') { streamRunId = String(args?.runId ?? ''); streamHandler?.({ payload: { projectPath: args?.projectPath, agentId: args?.agentId, runId: streamRunId, status: 'started', deltaText: '', accumulatedText: '', finishReason: null, runtimeSummary: '请求 Agent LLM', }, }); await new Promise((resolve) => { releaseStream = resolve; }); streamHandler?.({ payload: { projectPath: args?.projectPath, agentId: args?.agentId, runId: streamRunId, status: 'completed', deltaText: '', accumulatedText: '第一段第二段', finishReason: 'stop', runtimeSummary: '等待下一轮输入', }, }); return { replyText: '第一段第二段' }; } if (command === 'append_local_conversation_message') { persistedMessages.push( args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }, ); return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: index + 1, })), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke }, event: { listen } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText('历史消息')).not.toBeNull(); const messageList = screen.getByLabelText( 'Agent 聊天记录', ) as HTMLDivElement; let messageScrollHeight = 600; Object.defineProperty(messageList, 'scrollHeight', { configurable: true, get: () => messageScrollHeight, }); Object.defineProperty(messageList, 'clientHeight', { configurable: true, get: () => 100, }); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '测试消息列表滚动' }, }); selectDeveloperAgentChatMode('chat'); fireEvent.click(screen.getByRole('button', { name: '发送' })); await waitFor(() => expect(releaseStream).not.toBeNull()); messageList.scrollTop = 460; fireEvent.scroll(messageList); messageScrollHeight = 720; await act(async () => { streamHandler?.({ payload: { projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: streamRunId, status: 'delta', deltaText: '第一段', accumulatedText: '第一段', finishReason: null, }, }); }); expect(await screen.findByText('第一段')).not.toBeNull(); await waitFor(() => expect(messageList.scrollTop).toBe(720)); messageList.scrollTop = 120; fireEvent.scroll(messageList); messageScrollHeight = 780; await act(async () => { streamHandler?.({ payload: { projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: streamRunId, status: 'started', deltaText: '', accumulatedText: '第一段', finishReason: null, runtimeSummary: '上游正在重试', }, }); }); expect( await within(messageList).findByText('已连接 Agent LLM,上游正在重试'), ).not.toBeNull(); expect(messageList.scrollTop).toBe(120); messageScrollHeight = 840; await act(async () => { streamHandler?.({ payload: { projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: streamRunId, status: 'delta', deltaText: '第二段', accumulatedText: '第一段第二段', finishReason: null, }, }); }); expect(await screen.findByText('第一段第二段')).not.toBeNull(); expect(messageList.scrollTop).toBe(120); messageScrollHeight = 900; await act(async () => { releaseStream?.(); }); expect(await screen.findByText(/已保存 3 条/)).not.toBeNull(); expect(messageList.scrollTop).toBe(120); }); it('steers the current developer Agent run by default and keeps explicit queueing', async () => { const sessionId = 'agent-session-design-director'; const runId = 'launcher-agent-task-running'; const messages: Array<{ schemaVersion: string; role: 'user'; content: string; agentId: null; updatedAt: number; }> = []; const runningRuntimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId, runId, source: 'agent-background-task', status: 'running', phase: 'planning', currentTask: '整理角色规范', currentAction: '请求 Agent LLM', plan: ['整理要求', '回复开发者'], observations: [], taskQueue: { total: 1, pending: 0, running: 1, completed: 0, failed: 0, latestRunId: runId, updatedAt: 5000, }, allowedTools: ['conversation.read', 'conversation.write'], lastResponse: null, error: null, updatedAt: 5000, }; const runtimeResult = (state = runningRuntimeState) => ({ state, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: state.taskQueue, recentEvents: [], recentTasks: [ { schemaVersion: 'game-creator-agent-runtime-task.v1', agentId: 'design-director', taskId: 'design-director', sessionId, runId: state.runId, source: 'agent-background-task', task: state.currentTask, status: state.status, phase: state.phase, currentAction: state.currentAction, error: null, updatedAt: state.updatedAt, }, ], }); let steerCount = 0; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [], }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'list_game_creator_agent_sessions') { return { path: '/tmp/authorized-game/.agent/runtime/sessions/design-director.json', agentId: 'design-director', activeSessionId: sessionId, sessions: [ { sessionId, title: '角色规范', createdAt: 1, updatedAt: 2, archivedAt: null, messageCount: messages.length, legacy: false, }, ], }; } if (command === 'read_local_conversation') { return { path: `/tmp/authorized-game/.agent/conversations/agents/design-director/sessions/${sessionId}.jsonl`, agentId: 'design-director', sessionId, messages: [...messages], }; } if (command === 'read_game_creator_agent_runtime') { return runtimeResult(); } if (command === 'steer_game_creator_agent_runtime_task') { steerCount += 1; messages.push({ schemaVersion: 'game-creator-conversation.v1', role: 'user', content: String(args?.instruction ?? ''), agentId: null, updatedAt: 5000 + steerCount, }); return { runtime: runtimeResult(), steerId: String(args?.steerId), sequence: steerCount, status: 'queued', providerInterrupted: steerCount === 2, }; } if (command === 'start_game_creator_agent_runtime_task') { const queuedRunId = String(args?.runId); messages.push({ schemaVersion: 'game-creator-conversation.v1', role: 'user', content: String(args?.task ?? ''), agentId: null, updatedAt: 5003, }); return { ...runtimeResult(), taskQueue: { ...runningRuntimeState.taskQueue, total: 2, pending: 1, latestRunId: queuedRunId, }, recentTasks: [ ...runtimeResult().recentTasks, { ...runtimeResult().recentTasks[0], runId: queuedRunId, task: String(args?.task ?? ''), status: 'pending', phase: 'queued', currentAction: '等待当前后台任务完成', }, ], }; } if (command === 'cancel_game_creator_agent_runtime_task') { return runtimeResult({ ...runningRuntimeState, status: 'cancelling', phase: 'cancelling', currentAction: '正在取消 Agent 后台任务', }); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText('running / planning')).not.toBeNull(); expect(screen.getByLabelText('后台任务提交方式')).toHaveProperty( 'value', 'steer', ); expect( screen.getByRole('button', { name: '分叉当前 Agent 会话' }), ).toHaveProperty('disabled', true); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '先补充角色背面规范' }, }); fireEvent.click(screen.getByRole('button', { name: '追加指令' })); expect( await screen.findByText(`追加指令已排队,等待当前 Run 应用:${runId}`), ).not.toBeNull(); const firstSteerCall = invoke.mock.calls.find( ([command]) => command === 'steer_game_creator_agent_runtime_task', ); expect(firstSteerCall?.[1]).toEqual({ projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId, runId, steerId: expect.stringMatching(/^launcher-agent-steer-/), instruction: '先补充角色背面规范', }); expect(invoke).not.toHaveBeenCalledWith( 'start_game_creator_agent_runtime_task', expect.anything(), ); expect(screen.getByLabelText('Agent 聊天内容')).toHaveProperty('value', ''); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '立刻改为三视图输出' }, }); fireEvent.click(screen.getByRole('button', { name: '追加指令' })); expect( await screen.findByText( `智能服务已根据追加指令调整,旧请求已安全中断:${runId}`, ), ).not.toBeNull(); fireEvent.change(screen.getByLabelText('后台任务提交方式'), { target: { value: 'queue' }, }); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '下一轮再整理世界观' }, }); fireEvent.click(screen.getByRole('button', { name: '排队任务' })); expect( (await screen.findAllByText(/已加入后台队列:launcher-agent-task-/)) .length, ).toBeGreaterThan(0); expect(invoke).toHaveBeenCalledWith( 'start_game_creator_agent_runtime_task', expect.objectContaining({ projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId, task: '下一轮再整理世界观', runId: expect.stringMatching(/^launcher-agent-task-/), }), ); fireEvent.click( within(screen.getByLabelText('Agent Runtime 操作')).getByRole('button', { name: '取消任务', }), ); expect( (await screen.findAllByText(`正在取消后台任务:${runId}`)).length, ).toBeGreaterThan(0); }); it('runs developer agent work by default and syncs the terminal reply', async () => { const persistedMessages: Array<{ role: 'user' | 'assistant'; content: string; agentId: string | null; }> = []; const runningRuntimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', status: 'running', phase: 'action', currentTask: '后台整理角色规范', currentGoal: '完成角色规范阶段', currentAction: '调用工具 file.read', waitingOn: '工具观察结果', loopIteration: 1, maxLoopIterations: 3, toolActionBudget: 3, plan: ['读取项目笔记', '结合观察修正建议', '回复开发者'], planSteps: [ { index: 0, title: '读取项目笔记', status: 'completed', detail: 'file.read:ok · 已读取 game/notes.txt', updatedAt: 4005, }, { index: 1, title: '结合观察修正建议', status: 'active', detail: '正在根据观察修正计划', updatedAt: 4006, }, { index: 2, title: '回复开发者', status: 'failed', detail: '最终回复仍缺少角色规范确认', updatedAt: 4007, }, ], activePlanStepIndex: 1, observations: [ '思考摘要:需要先看项目笔记', 'file.read:ok · 已读取 game/notes.txt', ], recentToolCalls: [ { tool: 'file.read', status: 'ok', actionFingerprint: 'a'.repeat(64), inputSummary: 'path=game/notes.txt', reason: '读取笔记', summary: '已读取 game/notes.txt', detail: 'game/notes.txt: 连续上下文笔记', updatedAt: 4004, }, ], taskQueue: { total: 1, pending: 0, running: 1, completed: 0, failed: 0, latestRunId: 'launcher-agent-task-test', updatedAt: 4000, }, allowedTools: ['conversation.read', 'conversation.write'], lastResponse: null, error: null, updatedAt: 4000, }; const runningRuntimeTask = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', task: '后台整理角色规范', status: 'running', phase: 'action', currentAction: '调用工具 file.read', error: null, updatedAt: 4000, }; const completedRuntimeState = { ...runningRuntimeState, status: 'idle', phase: 'completed', currentAction: '等待下一轮输入', waitingOn: '开发者下一轮输入', nextStep: '等待下一轮输入', taskQueue: { total: 1, pending: 0, running: 0, completed: 1, failed: 0, latestRunId: 'launcher-agent-task-test', updatedAt: 4010, }, lastResponse: '角色规范已整理。', updatedAt: 4010, }; const completedRuntimeTask = { ...runningRuntimeTask, status: 'completed', phase: 'completed', currentAction: '等待下一轮输入', updatedAt: 4010, }; const runningRuntimeEvents = [ { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', eventType: 'thinking_summary', status: 'running', phase: 'planning', summary: '需要先看项目笔记', detail: null, updatedAt: 4001, }, { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', eventType: 'plan', status: 'running', phase: 'planning', summary: '已生成行动计划', detail: '读取项目笔记 / 回复开发者', updatedAt: 4002, }, { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', eventType: 'action', status: 'running', phase: 'action', summary: '调用 file.read', detail: 'game/notes.txt', updatedAt: 4003, }, { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', eventType: 'observation', status: 'running', phase: 'action', summary: 'file.read 返回项目笔记', detail: '已读取 game/notes.txt', updatedAt: 4004, }, { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', eventType: 'response', status: 'idle', phase: 'completed', summary: 'Agent 已生成最终回复。', detail: '角色规范已整理。', updatedAt: 4005, }, { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-test', source: 'agent-background-task', eventType: 'error', status: 'failed', phase: 'failed', summary: 'Agent Runtime 本轮处理失败。', detail: '最终回复调用失败', updatedAt: 4006, }, ]; let startedRunId = runningRuntimeState.runId; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: 3000 + index, })), }; } if (command === 'read_game_creator_agent_runtime') { return { state: runningRuntimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: runningRuntimeState.taskQueue, recentEvents: runningRuntimeEvents, recentTasks: [runningRuntimeTask], }; } if (command === 'start_game_creator_agent_runtime_task') { const runId = String(args?.runId ?? runningRuntimeState.runId); startedRunId = runId; persistedMessages.push({ role: 'user', content: String(args?.task ?? ''), agentId: null, }); return { state: { ...runningRuntimeState, runId, }, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: { ...runningRuntimeState.taskQueue, latestRunId: runId, }, recentEvents: runningRuntimeEvents, recentTasks: [{ ...runningRuntimeTask, runId }], }; } throw new Error(`unexpected invoke ${command}`); }, ); let runtimeUpdateHandler: | ((event: { payload: Record }) => void) | null = null; const listen = vi.fn( async ( eventName: string, handler: (event: { payload: Record }) => void, ) => { if (eventName === 'game-creator-agent-runtime-update') { runtimeUpdateHandler = handler; return () => { if (runtimeUpdateHandler === handler) { runtimeUpdateHandler = null; } }; } throw new Error(`unexpected listen ${eventName}`); }, ); window.__TAURI__ = { core: { invoke }, event: { listen } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); expect( screen.getByRole('button', { name: '执行' }).getAttribute('aria-pressed'), ).toBe('true'); expect( screen.getByRole('button', { name: '聊天' }).getAttribute('aria-pressed'), ).toBe('false'); expect(screen.queryByRole('button', { name: '后台运行' })).toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '后台整理角色规范' }, }); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect(await screen.findByText('后台整理角色规范')).not.toBeNull(); expect(await screen.findByText('running / action')).not.toBeNull(); expect(screen.getByText(/agent-background-task/)).not.toBeNull(); expect(screen.getByText('当前目标:完成角色规范阶段')).not.toBeNull(); expect(screen.getByText('调用工具 file.read')).not.toBeNull(); expect(screen.getByText('等待:工具观察结果')).not.toBeNull(); expect(screen.getByText('Loop:1/3 · 工具预算 3')).not.toBeNull(); expect( screen.getByText( /任务队列:pending 0 · running 1 · waiting 0 · needsInput 0 · cancelled 0 · completed 0 · failed 0 · total 1 · latest launcher-agent-task-/, ), ).not.toBeNull(); const runtimeActions = screen.getByLabelText('Agent Runtime 操作'); expect( ( within(runtimeActions).getByRole('button', { name: '取消任务', }) as HTMLButtonElement ).disabled, ).toBe(false); expect( ( within(runtimeActions).getByRole('button', { name: '重试', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( within(screen.getByLabelText('Agent 聊天记录')).getByRole('status') .textContent, ).toContain('等待工具观察结果'); const runtimePanel = screen.getByLabelText('Agent Runtime 状态'); const collapseRuntimeButton = within(runtimePanel).getByRole('button', { name: '折叠 Runtime 详情', }); expect(collapseRuntimeButton.getAttribute('aria-expanded')).toBe('true'); fireEvent.click(collapseRuntimeButton); expect( within(runtimePanel) .getByRole('button', { name: '展开 Runtime 详情' }) .getAttribute('aria-expanded'), ).toBe('false'); expect( within(runtimePanel).queryByText('当前目标:完成角色规范阶段'), ).toBeNull(); expect(within(runtimePanel).queryByText('计划进度')).toBeNull(); expect(within(runtimePanel).getByText('running / action')).not.toBeNull(); expect( ( within(runtimeActions).getByRole('button', { name: '取消任务', }) as HTMLButtonElement ).disabled, ).toBe(false); fireEvent.click( within(runtimePanel).getByRole('button', { name: '展开 Runtime 详情', }), ); expect(screen.getByText('计划进度')).not.toBeNull(); expect( screen.getByText( '#1 completed · 读取项目笔记 · file.read:ok · 已读取 game/notes.txt', ), ).not.toBeNull(); expect( screen.getByText('#2 active · 结合观察修正建议 · 正在根据观察修正计划'), ).not.toBeNull(); expect( screen.getByText('#3 failed · 回复开发者 · 最终回复仍缺少角色规范确认'), ).not.toBeNull(); expect(screen.getByText('思考摘要:需要先看项目笔记')).not.toBeNull(); expect( screen.getByText('file.read:ok · 已读取 game/notes.txt'), ).not.toBeNull(); expect(screen.getByText('最近动作')).not.toBeNull(); expect( screen.getByText( 'file.read · ok · 已读取 game/notes.txt · 目标:path=game/notes.txt · 读取笔记', ), ).not.toBeNull(); expect(screen.getByText('最近事件')).not.toBeNull(); expect( screen.getByText( 'error · failed / failed · Agent Runtime 本轮处理失败。', ), ).not.toBeNull(); expect(screen.queryByText(/最终回复调用失败/u)).toBeNull(); expect( screen.getByText( 'observation · running / action · file.read 返回项目笔记 · 已读取 game/notes.txt', ), ).not.toBeNull(); expect( screen.getByText( 'action · running / action · 调用 file.read · game/notes.txt', ), ).not.toBeNull(); expect( screen.getByText( 'response · idle / completed · Agent 已生成最终回复。 · 角色规范已整理。', ), ).not.toBeNull(); expect( screen.queryByText( 'thinking_summary · running / planning · 需要先看项目笔记', ), ).toBeNull(); expect( screen.queryByText( 'plan · running / planning · 已生成行动计划 · 读取项目笔记 / 回复开发者', ), ).toBeNull(); fireEvent.click( screen.getByRole('button', { name: '展开事件(另有 2 条)' }), ); expect( screen.getByText( 'thinking_summary · running / planning · 需要先看项目笔记', ), ).not.toBeNull(); expect( screen.getByText( 'plan · running / planning · 已生成行动计划 · 读取项目笔记 / 回复开发者', ), ).not.toBeNull(); expect(screen.getByRole('button', { name: '收起事件' })).not.toBeNull(); expect(screen.getByText('最近任务')).not.toBeNull(); expect( screen.getByText(/running \/ action · 后台整理角色规范/), ).not.toBeNull(); expect( screen.getByText(/已启动后台任务:launcher-agent-task-/), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith( 'start_game_creator_agent_runtime_task', expect.objectContaining({ projectPath: '/tmp/authorized-game', agentId: 'design-director', task: '后台整理角色规范', }), ); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_role_agent_stream', expect.anything(), ); expect(invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_role_agent', expect.anything(), ); expect(persistedMessages).toEqual([ { role: 'user', content: '后台整理角色规范', agentId: null, }, ]); const cancellingRuntimeState = { ...runningRuntimeState, status: 'cancelling', phase: 'cancelling', currentAction: '正在取消 Agent 后台任务', waitingOn: '当前 LLM 或工具调用返回', nextStep: '取消完成后可重试该任务或提交新任务', }; await act(async () => { runtimeUpdateHandler?.({ payload: { projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: startedRunId, status: 'cancelling', phase: 'cancelling', runtime: { state: { ...cancellingRuntimeState, runId: startedRunId }, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: cancellingRuntimeState.taskQueue, recentEvents: runningRuntimeEvents, recentTasks: [runningRuntimeTask], }, }, }); }); expect(await screen.findByText('cancelling / cancelling')).not.toBeNull(); expect(screen.getByText('等待:当前 LLM 或工具调用返回')).not.toBeNull(); const cancellingActions = screen.getByLabelText('Agent Runtime 操作'); expect( ( within(cancellingActions).getByRole('button', { name: '取消任务', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( within(cancellingActions).getByRole('button', { name: '重试', }) as HTMLButtonElement ).disabled, ).toBe(true); persistedMessages.push({ role: 'assistant', content: '角色规范已整理。', agentId: null, }); await act(async () => { runtimeUpdateHandler?.({ payload: { projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: startedRunId, status: 'idle', phase: 'completed', runtime: { state: { ...completedRuntimeState, runId: startedRunId }, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: { ...completedRuntimeState.taskQueue, latestRunId: startedRunId, }, recentEvents: [ ...runningRuntimeEvents, { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: startedRunId, source: 'agent-background-task', eventType: 'turn.completed', status: 'idle', phase: 'completed', summary: 'Agent Runtime 完成本轮处理。', detail: '角色规范已整理。', updatedAt: 4010, }, ], recentTasks: [{ ...completedRuntimeTask, runId: startedRunId }], }, }, }); }); expect(await screen.findByText('idle / completed')).not.toBeNull(); expect(await screen.findByText('角色规范已整理。')).not.toBeNull(); expect(screen.getByText('等待:开发者下一轮输入')).not.toBeNull(); expect( screen.getByText( 'turn.completed · idle / completed · Agent Runtime 完成本轮处理。 · 角色规范已整理。', ), ).not.toBeNull(); expect( within(screen.getByLabelText('Agent 聊天记录')).queryByRole('status'), ).toBeNull(); expect(persistedMessages).toEqual([ { role: 'user', content: '后台整理角色规范', agentId: null, }, { role: 'assistant', content: '角色规范已整理。', agentId: null, }, ]); }); it('normalizes and restores the V1.17 persistent plan snapshot after refresh', async () => { const sessionId = 'agent-session-plan-v117'; const planSteps = [ { step: '读取项目上下文', status: 'completed' }, { title: '确认目标与约束', status: 'completed' }, { step: '补齐核心玩法', status: 'in_progress' }, { step: '实现交互反馈', status: 'pending' }, { step: '运行定向验证', status: 'pending' }, { step: '检查移动端布局', status: 'pending' }, { step: '整理变更摘要', status: 'pending' }, { step: '准备最终回复', status: 'pending' }, { step: '不应展示的第九步', status: 'pending' }, ]; const runtimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId, runId: 'runtime-plan-v117', source: 'agent-background-task', status: 'running', phase: 'action', currentTask: '持续更新本轮执行计划', currentGoal: '完成首版玩法实现', currentAction: '补齐核心玩法', waitingOn: '核心玩法实现结果', nextStep: '运行定向验证', loopIteration: 3, maxLoopIterations: 6, toolActionBudget: 3, planRevision: 7, planExplanation: '根据最新项目观察调整实现与验证顺序', plan: planSteps.map((step) => step.step ?? step.title ?? ''), planSteps, activePlanStepIndex: 2, observations: [], allowedTools: ['file.read', 'file.patch', 'project.verify'], lastResponse: null, error: null, updatedAt: 7000, }; const runtimeResult = () => ({ state: { ...runtimeState, planSteps: runtimeState.planSteps.map((step) => ({ ...step })), }, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: { total: 1, pending: 0, running: 1, completed: 0, failed: 0, latestRunId: runtimeState.runId, updatedAt: runtimeState.updatedAt, }, recentEvents: [], recentTasks: [], }); let runtimeReadCount = 0; 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-5.5', apiKind: 'openai_responses', reasoningEffort: 'high', stream: true, webSearchEnabled: false, error: null, agents: [], }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'list_game_creator_agent_sessions') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director/sessions.json', agentId: 'design-director', activeSessionId: sessionId, sessions: [ { sessionId, title: '持久计划验证', createdAt: 6000, updatedAt: 7000, archivedAt: null, messageCount: 0, legacy: false, }, ], }; } if (command === 'read_local_conversation') { return { path: `/tmp/authorized-game/.agent/conversations/agents/design-director/sessions/${sessionId}.jsonl`, agentId: 'design-director', sessionId, messages: [], }; } if (command === 'read_game_creator_agent_runtime') { runtimeReadCount += 1; return runtimeResult(); } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); const planPanel = await screen.findByLabelText('Agent 计划进度'); expect(within(planPanel).getByText('计划修订号:7')).not.toBeNull(); expect( within(planPanel).getByText( '计划说明:根据最新项目观察调整实现与验证顺序', ), ).not.toBeNull(); expect(within(planPanel).getAllByText(/^#\d+ /)).toHaveLength(8); expect( within(planPanel).getByText('#2 completed · 确认目标与约束'), ).not.toBeNull(); expect( within(planPanel).getByText('#3 in_progress · 补齐核心玩法'), ).not.toBeNull(); expect(within(planPanel).queryByText(/不应展示的第九步/)).toBeNull(); const firstSnapshot = planPanel.textContent; const runtimeReadsBeforeRefresh = runtimeReadCount; fireEvent.click( within(screen.getByLabelText('Agent Runtime 操作')).getByRole('button', { name: '刷新状态', }), ); await waitFor(() => { expect(runtimeReadCount).toBeGreaterThan(runtimeReadsBeforeRefresh); }); const refreshedPlanPanel = screen.getByLabelText('Agent 计划进度'); expect(refreshedPlanPanel.textContent).toBe(firstSnapshot); expect(within(refreshedPlanPanel).getAllByText(/^#\d+ /)).toHaveLength(8); expect( within(refreshedPlanPanel).queryByText(/不应展示的第九步/), ).toBeNull(); }); it('manages a persistent Goal through the developer Agent modal and exact Tauri commands', async () => { const projectPath = '/tmp/authorized-game'; const agentId = 'design-director'; const sessionId = 'agent-session-goal-v118'; const messages: Array<{ role: 'user' | 'assistant'; content: string; agentId: string | null; }> = []; let currentGoal: Record | null = null; let runtimeStatus = 'idle'; let runtimePhase = 'idle'; const goalRecord = (overrides: Record = {}) => ({ schemaVersion: 'game-creator-agent-goal.v1', projectId: 'project-goal-v118', goalId: 'goal-design-v118', agentId, sessionId, runId: 'goal-run-v118', revision: 1, status: 'active', outcome: '完成首版战斗循环', constraints: ['保持移动端可操作'], verification: ['定向测试通过'], completionEvidence: [], responseFingerprint: null, createdAt: 1000, pauseRequestedAt: null, pausedAt: null, completedAt: null, clearedAt: null, error: null, updatedAt: 1000, ...overrides, }); const runtimeState = () => { const goal = currentGoal; const paused = goal?.status === 'paused' ? 1 : 0; return { schemaVersion: 'game-creator-agent-runtime.v1', agentId, taskId: agentId, sessionId, runId: String(goal?.runId ?? 'runtime-idle-v118'), source: 'agent-background-task', status: runtimeStatus, phase: runtimePhase, currentTask: String(goal?.outcome ?? ''), currentGoal: String(goal?.outcome ?? ''), goalId: goal?.goalId ?? null, goalRevision: Number(goal?.revision ?? 0), goalStatus: goal?.status ?? null, goalOutcome: goal?.outcome ?? null, goalConstraints: goal?.constraints ?? [], goalVerification: goal?.verification ?? [], currentAction: paused > 0 ? '等待恢复持久目标' : '推进持久目标', waitingOn: paused > 0 ? '开发者恢复持久目标' : 'Agent 推进目标', nextStep: paused > 0 ? '恢复后继续同一 Run' : '继续执行计划', plan: [], observations: [], allowedTools: [], lastResponse: null, error: null, updatedAt: Number(goal?.updatedAt ?? 1000), taskQueue: { total: goal ? 1 : 0, pending: 0, running: runtimeStatus === 'running' ? 1 : 0, waitingForConfirmation: 0, paused, cancelled: 0, completed: runtimeStatus === 'completed' ? 1 : 0, failed: 0, latestRunId: goal ? String(goal.runId) : null, updatedAt: Number(goal?.updatedAt ?? 1000), }, }; }; const runtimeResult = () => ({ state: runtimeState(), sessionPath: `${projectPath}/.agent/runtime/agents/${agentId}.json`, eventPath: `${projectPath}/.agent/runtime/events/${agentId}.jsonl`, taskPath: `${projectPath}/.agent/runtime/tasks/${agentId}.jsonl`, taskQueue: runtimeState().taskQueue, recentEvents: [], recentTasks: [], }); const conversationResult = () => ({ path: `${projectPath}/.agent/conversations/agents/${agentId}/sessions/${sessionId}.jsonl`, agentId, sessionId, messages: messages.map((message, index) => ({ schemaVersion: 'game-creator-conversation.v1', ...message, updatedAt: 2000 + index, })), }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_responses', reasoningEffort: 'high', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId, label: '策划总监', configured: true, apiKeyPresent: true, model: 'gpt-5.5', apiKind: 'openai_responses', reasoningEffort: 'high', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'list_game_creator_agent_sessions') { return { path: `${projectPath}/.agent/conversations/agents/${agentId}/sessions.json`, agentId, activeSessionId: sessionId, sessions: [ { sessionId, title: '持久目标验证', createdAt: 1000, updatedAt: 1000, archivedAt: null, messageCount: messages.length, legacy: false, }, ], }; } if (command === 'read_game_creator_agent_goal') { return currentGoal ? { ...currentGoal } : null; } if (command === 'read_local_conversation') { return conversationResult(); } if (command === 'read_game_creator_agent_runtime') { return runtimeResult(); } if (command === 'start_game_creator_agent_goal') { currentGoal = goalRecord({ runId: String(args?.runId), outcome: String(args?.outcome), constraints: args?.constraints, verification: args?.verification, updatedAt: 3000, }); runtimeStatus = 'running'; runtimePhase = 'planning'; messages.push({ role: 'user', content: String(args?.outcome), agentId: null, }); return { goal: { ...currentGoal }, runtime: runtimeResult(), providerInterrupted: false, }; } if (command === 'pause_game_creator_agent_goal') { currentGoal = { ...currentGoal!, status: 'pause-requested', pauseRequestedAt: 4000, updatedAt: 4000, }; runtimeStatus = 'running'; runtimePhase = 'pausing'; return { goal: { ...currentGoal }, runtime: runtimeResult(), providerInterrupted: true, }; } if (command === 'edit_game_creator_agent_goal') { currentGoal = { ...currentGoal!, revision: Number(currentGoal?.revision ?? 1) + 1, outcome: String(args?.outcome), constraints: args?.constraints, verification: args?.verification, updatedAt: 5000, }; return { goal: { ...currentGoal }, runtime: runtimeResult(), providerInterrupted: false, }; } if (command === 'resume_game_creator_agent_goal') { currentGoal = { ...currentGoal!, status: 'active', pauseRequestedAt: null, pausedAt: null, updatedAt: 6000, }; runtimeStatus = 'running'; runtimePhase = 'planning'; return { goal: { ...currentGoal }, runtime: runtimeResult(), providerInterrupted: false, }; } if (command === 'clear_game_creator_agent_goal') { currentGoal = { ...currentGoal!, status: 'cleared', clearedAt: 7000, updatedAt: 7000, }; runtimeStatus = 'idle'; runtimePhase = 'completed'; return { goal: { ...currentGoal }, runtime: runtimeResult(), providerInterrupted: false, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); const emptyGoalPanel = await screen.findByLabelText('Agent Goal 状态'); expect(within(emptyGoalPanel).getByText('尚未开始')).not.toBeNull(); selectDeveloperAgentChatMode('goal'); expect( screen.getByRole('button', { name: '目标' }).getAttribute('aria-pressed'), ).toBe('true'); fireEvent.click(screen.getByRole('button', { name: '开始目标' })); let goalDialog = screen.getByRole('dialog', { name: '创建持久目标' }); fireEvent.change(within(goalDialog).getByLabelText('Goal outcome'), { target: { value: '完成首版战斗循环' }, }); fireEvent.change(within(goalDialog).getByLabelText('Goal constraints'), { target: { value: '保持移动端可操作\n不改变正式用户首页' }, }); fireEvent.change(within(goalDialog).getByLabelText('Goal verification'), { target: { value: 'AppSurface 通过\nTypecheck 通过' }, }); fireEvent.click(within(goalDialog).getByRole('button', { name: '开始' })); expect(await screen.findByText('Outcome:完成首版战斗循环')).not.toBeNull(); expect(screen.getByText('Status:active · Revision:1')).not.toBeNull(); expect( screen.getByText('Verification:AppSurface 通过;Typecheck 通过'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('start_game_creator_agent_goal', { projectPath, agentId, sessionId, outcome: '完成首版战斗循环', constraints: ['保持移动端可操作', '不改变正式用户首页'], verification: ['AppSurface 通过', 'Typecheck 通过'], runId: expect.stringMatching(/^launcher-agent-goal-/), }); fireEvent.click( within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', { name: '暂停', }), ); expect( await screen.findByText('Status:pause-requested · Revision:1'), ).not.toBeNull(); expect( within(screen.getByLabelText('Agent Goal 状态')).queryByRole('button', { name: '恢复', }), ).toBeNull(); currentGoal = { ...currentGoal!, status: 'paused', pausedAt: 4500, updatedAt: 4500, }; // A terminal-looking Runtime proves paused Goal metadata disables ordinary retry. runtimeStatus = 'completed'; runtimePhase = 'completed'; fireEvent.click( within(screen.getByLabelText('Agent Runtime 操作')).getByRole('button', { name: '刷新状态', }), ); expect( await screen.findByText('Status:paused · Revision:1'), ).not.toBeNull(); expect(screen.getByText(/paused 1/)).not.toBeNull(); expect( ( within(screen.getByLabelText('Agent Runtime 操作')).getByRole( 'button', { name: '重试' }, ) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '新建 Agent 会话', }) as HTMLButtonElement ).disabled, ).toBe(true); expect(invoke).toHaveBeenCalledWith('pause_game_creator_agent_goal', { projectPath, agentId, sessionId, goalId: 'goal-design-v118', expectedRevision: 1, }); fireEvent.click( within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', { name: '编辑', }), ); goalDialog = screen.getByRole('dialog', { name: '编辑持久目标' }); fireEvent.change(within(goalDialog).getByLabelText('Goal outcome'), { target: { value: '完成并验证首版战斗循环' }, }); fireEvent.change(within(goalDialog).getByLabelText('Goal verification'), { target: { value: 'AppSurface 通过\nTypecheck 通过\n移动端可操作' }, }); fireEvent.click(within(goalDialog).getByRole('button', { name: '保存' })); expect( await screen.findByText('Outcome:完成并验证首版战斗循环'), ).not.toBeNull(); expect(screen.getByText('Status:paused · Revision:2')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('edit_game_creator_agent_goal', { projectPath, agentId, sessionId, goalId: 'goal-design-v118', expectedRevision: 1, outcome: '完成并验证首版战斗循环', constraints: ['保持移动端可操作', '不改变正式用户首页'], verification: ['AppSurface 通过', 'Typecheck 通过', '移动端可操作'], }); fireEvent.click( within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', { name: '恢复', }), ); expect( await screen.findByText('Status:active · Revision:2'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('resume_game_creator_agent_goal', { projectPath, agentId, sessionId, goalId: 'goal-design-v118', expectedRevision: 2, }); fireEvent.click( within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', { name: '清理', }), ); expect( await screen.findByText('Status:cleared · Revision:2'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('clear_game_creator_agent_goal', { projectPath, agentId, sessionId, goalId: 'goal-design-v118', expectedRevision: 2, }); expect( within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', { name: '开始', }), ).not.toBeNull(); }); it('clears stale Goal UI before reading the newly selected Agent and Session Goal', async () => { const projectPath = '/tmp/authorized-game'; const designAgentId = 'design-director'; const foundationAgentId = 'design-foundation'; const designSessionA = 'design-session-a'; const designSessionB = 'design-session-b'; const foundationSession = 'foundation-session-a'; const activeSessionByAgent: Record = { [designAgentId]: designSessionA, [foundationAgentId]: foundationSession, }; const goal = ( agentId: string, sessionId: string, goalId: string, outcome: string, ) => ({ schemaVersion: 'game-creator-agent-goal.v1', projectId: 'project-goal-isolation', goalId, agentId, sessionId, runId: `run-${goalId}`, revision: 3, status: 'completed', outcome, constraints: [], verification: [`验证 ${outcome}`], completionEvidence: [], responseFingerprint: null, createdAt: 1000, completedAt: 2000, updatedAt: 2000, }); const designGoalA = goal( designAgentId, designSessionA, 'goal-design-a', '设计目标 A', ); const designGoalB = goal( designAgentId, designSessionB, 'goal-design-b', '设计目标 B', ); const foundationGoal = goal( foundationAgentId, foundationSession, 'goal-foundation-a', '玩法目标 C', ); let resolveDesignGoalB: ((value: typeof designGoalB) => void) | null = null; const designGoalBRead = new Promise((resolve) => { resolveDesignGoalB = resolve; }); let resolveFoundationGoal: ((value: typeof foundationGoal) => void) | null = null; const foundationGoalRead = new Promise((resolve) => { resolveFoundationGoal = resolve; }); const sessionsForAgent = (agentId: string) => { const sessions = agentId === designAgentId ? [ { sessionId: designSessionA, title: '设计主线', createdAt: 1000, updatedAt: 2000, archivedAt: null, messageCount: 0, legacy: false, }, { sessionId: designSessionB, title: '设计分支', createdAt: 1000, updatedAt: 2000, archivedAt: null, messageCount: 0, legacy: false, }, ] : [ { sessionId: foundationSession, title: '玩法主线', createdAt: 1000, updatedAt: 2000, archivedAt: null, messageCount: 0, legacy: false, }, ]; return { path: `${projectPath}/.agent/conversations/agents/${agentId}/sessions.json`, agentId, activeSessionId: activeSessionByAgent[agentId], sessions, }; }; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_responses', reasoningEffort: 'high', stream: true, webSearchEnabled: false, error: null, agents: [], }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'list_game_creator_agent_sessions') { return sessionsForAgent(String(args?.agentId)); } if (command === 'set_active_game_creator_agent_session') { activeSessionByAgent[String(args?.agentId)] = String(args?.sessionId); return sessionsForAgent(String(args?.agentId)); } if (command === 'read_game_creator_agent_goal') { const agentId = String(args?.agentId); const sessionId = String(args?.sessionId); if (agentId === designAgentId && sessionId === designSessionA) { return designGoalA; } if (agentId === designAgentId && sessionId === designSessionB) { return designGoalBRead; } if (agentId === foundationAgentId) { return foundationGoalRead; } return null; } if (command === 'read_local_conversation') { return { path: `${projectPath}/.agent/conversations/agents/${String( args?.agentId, )}/sessions/${String(args?.sessionId)}.jsonl`, agentId: args?.agentId, sessionId: args?.sessionId, messages: [], }; } if (command === 'read_game_creator_agent_runtime') { const agentId = String(args?.agentId); const sessionId = String(args?.sessionId); return { state: { schemaVersion: 'game-creator-agent-runtime.v1', agentId, taskId: agentId, sessionId, runId: `idle-${sessionId}`, source: 'agent-background-task', status: 'idle', phase: 'idle', currentTask: '', currentGoal: '', currentAction: '', waitingOn: '', nextStep: '', plan: [], observations: [], allowedTools: [], lastResponse: null, error: null, updatedAt: 2000, }, sessionPath: `${projectPath}/.agent/runtime/agents/${agentId}.json`, eventPath: `${projectPath}/.agent/runtime/events/${agentId}.jsonl`, taskQueue: { total: 0, pending: 0, running: 0, waitingForConfirmation: 0, paused: 0, cancelled: 0, completed: 0, failed: 0, latestRunId: null, updatedAt: 2000, }, recentEvents: [], recentTasks: [], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText('Outcome:设计目标 A')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: /设计分支/ })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('read_game_creator_agent_goal', { projectPath, agentId: designAgentId, sessionId: designSessionB, }); }); expect(screen.queryByText('Outcome:设计目标 A')).toBeNull(); await act(async () => { resolveDesignGoalB?.(designGoalB); }); expect(await screen.findByText('Outcome:设计目标 B')).not.toBeNull(); fireEvent.click( screen.getByRole('button', { name: /确定玩法规格与界面原型/ }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('read_game_creator_agent_goal', { projectPath, agentId: foundationAgentId, sessionId: foundationSession, }); }); expect(screen.queryByText('Outcome:设计目标 B')).toBeNull(); await act(async () => { resolveFoundationGoal?.(foundationGoal); }); expect(await screen.findByText('Outcome:玩法目标 C')).not.toBeNull(); }); it('keeps developer Goal controls out of the formal user surface', () => { renderAppAt('/'); expect(screen.queryByLabelText('Agent Goal 状态')).toBeNull(); expect(screen.queryByRole('button', { name: '目标' })).toBeNull(); }); it('does not sync an old runtime reply into another Agent session', async () => { const activeSessionId = 'agent-session-design-active'; const archivedSessionId = 'agent-session-design-archived'; const messagesBySession: Record< string, Array<{ role: 'user' | 'assistant'; content: string; agentId: string | null; }> > = { [activeSessionId]: [], [archivedSessionId]: [ { role: 'assistant', content: '这是归档会话内容。', agentId: null, }, ], }; const runtimeState = ( sessionId: string, runId: string, status: string, ) => ({ schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId, runId, source: 'agent-background-task', status, phase: status === 'running' ? 'planning' : 'completed', currentTask: '整理当前会话', currentGoal: '整理当前会话', currentAction: status === 'running' ? '生成 Agent 工具计划' : '等待下一轮输入', waitingOn: status === 'running' ? 'Agent 输出计划或回复' : '开发者下一轮输入', plan: [], observations: [], allowedTools: [], lastResponse: status === 'running' ? null : '已完成', error: null, updatedAt: 6000, }); let startedRunId = ''; let runtimeUpdateHandler: | ((event: { payload: Record }) => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'list_game_creator_agent_sessions') { return { path: '/tmp/authorized-game/.agent/runtime/sessions/design-director.json', agentId: 'design-director', activeSessionId, sessions: [ { sessionId: activeSessionId, title: '活动会话', createdAt: 1, updatedAt: 2, archivedAt: null, messageCount: messagesBySession[activeSessionId].length, legacy: false, }, { sessionId: archivedSessionId, title: '归档会话', createdAt: 1, updatedAt: 2, archivedAt: 3, messageCount: messagesBySession[archivedSessionId].length, legacy: false, }, ], }; } if (command === 'read_local_conversation') { const sessionId = String(args?.sessionId ?? activeSessionId); return { path: `/tmp/authorized-game/.agent/conversations/agents/design-director/sessions/${sessionId}.jsonl`, agentId: 'design-director', messages: messagesBySession[sessionId].map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: 6100 + index, })), }; } if (command === 'read_game_creator_agent_runtime') { const sessionId = String(args?.sessionId ?? activeSessionId); return { state: runtimeState(sessionId, `idle-${sessionId}`, 'idle'), sessionPath: `/tmp/authorized-game/.agent/runtime/agents/design-director.json`, eventPath: `/tmp/authorized-game/.agent/runtime/events/design-director.jsonl`, recentEvents: [], recentTasks: [], }; } if (command === 'start_game_creator_agent_runtime_task') { startedRunId = String(args?.runId ?? 'runtime-active'); messagesBySession[activeSessionId].push({ role: 'user', content: String(args?.task ?? ''), agentId: null, }); return { state: runtimeState(activeSessionId, startedRunId, 'running'), sessionPath: `/tmp/authorized-game/.agent/runtime/agents/design-director.json`, eventPath: `/tmp/authorized-game/.agent/runtime/events/design-director.jsonl`, recentEvents: [], recentTasks: [ { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: activeSessionId, runId: startedRunId, source: 'agent-background-task', task: String(args?.task ?? ''), status: 'running', phase: 'planning', currentAction: '生成 Agent 工具计划', error: null, updatedAt: 6200, }, ], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: vi.fn( async ( eventName: string, handler: (event: { payload: Record }) => void, ) => { if (eventName === 'game-creator-agent-runtime-update') { runtimeUpdateHandler = handler; } return () => {}; }, ), }, }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect( await screen.findByRole('button', { name: /活动会话/ }), ).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '只属于活动会话的任务' }, }); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect(await screen.findByText('只属于活动会话的任务')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: /归档会话/ })); expect(await screen.findByText('这是归档会话内容。')).not.toBeNull(); expect( within(screen.getByLabelText('Agent 聊天记录')).queryByRole('status'), ).toBeNull(); const activeReadsBeforeTerminal = invoke.mock.calls.filter( ([command, args]) => command === 'read_local_conversation' && (args as Record | undefined)?.sessionId === activeSessionId, ).length; messagesBySession[activeSessionId].push({ role: 'assistant', content: '这条回复不能进入归档会话。', agentId: null, }); await act(async () => { runtimeUpdateHandler?.({ payload: { projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: startedRunId, status: 'idle', phase: 'completed', runtime: { state: runtimeState(activeSessionId, startedRunId, 'idle'), sessionPath: `/tmp/authorized-game/.agent/runtime/agents/design-director.json`, eventPath: `/tmp/authorized-game/.agent/runtime/events/design-director.jsonl`, recentEvents: [], recentTasks: [], }, }, }); }); expect(screen.getByText('这是归档会话内容。')).not.toBeNull(); expect(screen.queryByText('这条回复不能进入归档会话。')).toBeNull(); expect( invoke.mock.calls.filter( ([command, args]) => command === 'read_local_conversation' && (args as Record | undefined)?.sessionId === activeSessionId, ).length, ).toBe(activeReadsBeforeTerminal); }); it('confirms or rejects the exact pending tool action from the developer agent window', async () => { const waitingRuntimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-waiting', source: 'agent-background-task', status: 'waiting-for-confirmation', phase: 'waiting-for-confirmation', currentTask: '读取角色规范笔记', currentGoal: '确认角色规范依据', currentAction: '等待确认工具 file.read', waitingOn: '开发者确认 Agent 工具动作', nextStep: '确认或调整策略后继续 Agent 工具动作:file.read', loopIteration: 1, maxLoopIterations: 3, toolActionBudget: 3, plan: ['读取项目笔记'], observations: ['file.read:waiting-for-confirmation'], recentToolCalls: [ { tool: 'file.read', status: 'waiting-for-confirmation', actionFingerprint: 'b'.repeat(64), inputSummary: 'path=game/notes.txt', reason: '读取角色规范依据', summary: '项目权限策略要求用户确认:file.read', detail: null, updatedAt: 5000, }, ], pendingToolAction: { actionId: `action-${'b'.repeat(24)}`, actionFingerprint: 'b'.repeat(64), tool: 'file.read', inputSummary: 'path=game/notes.txt', reason: '读取角色规范依据', requestedAt: 5000, }, taskQueue: { total: 1, pending: 0, running: 0, waitingForConfirmation: 1, cancelled: 0, completed: 0, failed: 0, latestRunId: 'launcher-agent-waiting', updatedAt: 5000, }, allowedTools: ['file.read'], lastResponse: null, error: null, updatedAt: 5000, }; const waitingTask = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-waiting', source: 'agent-background-task', task: '读取角色规范笔记', status: 'waiting-for-confirmation', phase: 'waiting-for-confirmation', currentAction: '等待确认工具 file.read', error: null, updatedAt: 5000, }; const runtimeResult = { state: waitingRuntimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: waitingRuntimeState.taskQueue, recentEvents: [], recentTasks: [waitingTask], }; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } if (command === 'read_game_creator_agent_runtime') { return runtimeResult; } if (command === 'confirm_game_creator_agent_runtime_task') { const runId = waitingRuntimeState.runId; const taskQueue = { ...waitingRuntimeState.taskQueue, running: 1, waitingForConfirmation: 0, latestRunId: runId, }; return { ...runtimeResult, state: { ...waitingRuntimeState, runId, status: 'running', phase: 'action', currentAction: '执行已确认工具 file.read', waitingOn: '已确认工具执行结果', pendingToolAction: null, taskQueue, }, taskQueue, recentTasks: [ { ...waitingTask, runId, status: 'running', phase: 'action', }, ], }; } if (command === 'reject_game_creator_agent_runtime_task') { const runId = waitingRuntimeState.runId; const taskQueue = { ...waitingRuntimeState.taskQueue, running: 1, waitingForConfirmation: 0, latestRunId: runId, }; return { ...runtimeResult, state: { ...waitingRuntimeState, runId, status: 'running', phase: 'observation', currentAction: '开发者拒绝工具 file.read', waitingOn: 'Agent 根据拒绝结果修正计划', pendingToolAction: null, taskQueue, }, taskQueue, recentTasks: [ { ...waitingTask, runId, status: 'running', phase: 'observation', }, ], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect( await screen.findByText( 'waiting-for-confirmation / waiting-for-confirmation', ), ).not.toBeNull(); expect( screen.getByText( 'file.read · waiting-for-confirmation · 项目权限策略要求用户确认:file.read · 目标:path=game/notes.txt · 读取角色规范依据', ), ).not.toBeNull(); expect( screen.getByText('待确认动作:file.read · path=game/notes.txt'), ).not.toBeNull(); const runtimeActions = screen.getByLabelText('Agent Runtime 操作'); const confirmButton = within(runtimeActions).getByRole('button', { name: '确认继续', }) as HTMLButtonElement; expect(confirmButton.disabled).toBe(false); expect( ( within(runtimeActions).getByRole('button', { name: '拒绝并继续', }) as HTMLButtonElement ).disabled, ).toBe(false); const runtimeReadsBeforeRefresh = invoke.mock.calls.filter( ([command]) => command === 'read_game_creator_agent_runtime', ).length; fireEvent.click( within(runtimeActions).getByRole('button', { name: '刷新状态' }), ); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'read_game_creator_agent_runtime', ).length, ).toBeGreaterThan(runtimeReadsBeforeRefresh); }); const refreshedRuntimeActions = screen.getByLabelText('Agent Runtime 操作'); const refreshedConfirmButton = within(refreshedRuntimeActions).getByRole( 'button', { name: '确认继续' }, ); fireEvent.click(refreshedConfirmButton); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'confirm_game_creator_agent_runtime_task', expect.objectContaining({ projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: 'launcher-agent-waiting', actionId: `action-${'b'.repeat(24)}`, note: '开发者已确认待执行工具动作', }), ); }); expect(await screen.findByText('running / action')).not.toBeNull(); cleanup(); invoke.mockClear(); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect( await screen.findByText( 'waiting-for-confirmation / waiting-for-confirmation', ), ).not.toBeNull(); const rejectRuntimeActions = screen.getByLabelText('Agent Runtime 操作'); fireEvent.click( within(rejectRuntimeActions).getByRole('button', { name: '拒绝并继续', }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'reject_game_creator_agent_runtime_task', expect.objectContaining({ projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: 'launcher-agent-waiting', actionId: `action-${'b'.repeat(24)}`, note: '开发者拒绝待执行工具动作', }), ); }); expect(await screen.findByText('running / observation')).not.toBeNull(); }); it('answers launcher Agent Needs input without allowing a new composer turn', async () => { const projectPath = '/tmp/authorized-game'; const agentId = 'design-director'; const sessionId = 'agent-session-design-director'; const runId = 'launcher-agent-needs-input'; const request = agentRuntimeUserInputRequest({ agentId, sessionId, runId, requestId: 'launcher-request-input', actionId: 'launcher-action-input', }); let messages: Array> = []; let currentState: Record = { schemaVersion: 'game-creator-agent-runtime.v1', agentId, taskId: agentId, sessionId, runId, source: 'agent-background-task', status: 'waiting-for-user-input', phase: 'waiting-for-user-input', currentTask: '准备首版角色规范图', currentGoal: '确定美术方向', currentAction: '等待用户补充关键信息', waitingOn: '你的澄清回答', nextStep: '提交全部回答后继续同一 Run', plan: ['确定美术方向'], observations: [], allowedTools: ['user.input_request'], pendingToolAction: null, lastResponse: null, error: null, updatedAt: 6000, }; const runtimeResult = () => ({ state: currentState, sessionPath: `${projectPath}/.agent/runtime/agents/${agentId}.json`, eventPath: `${projectPath}/.agent/runtime/events/${agentId}.jsonl`, taskPath: `${projectPath}/.agent/runtime/tasks/${agentId}.jsonl`, taskQueue: { total: 1, pending: 0, running: currentState.status === 'running' ? 1 : 0, waitingForConfirmation: 0, waitingForUserInput: currentState.status === 'waiting-for-user-input' ? 1 : 0, cancelled: 0, completed: 0, failed: 0, latestRunId: runId, updatedAt: Number(currentState.updatedAt), }, recentEvents: [], recentTasks: [], userInputRequest: currentState.status === 'waiting-for-user-input' ? request : null, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [], }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return [runtimeResult()]; } if (command === 'list_game_creator_agent_sessions') { return { path: `${projectPath}/.agent/conversations/agents/${agentId}/sessions.json`, agentId, activeSessionId: sessionId, sessions: [ { sessionId, title: '默认会话', createdAt: 1000, updatedAt: 6000, archivedAt: null, messageCount: messages.length, legacy: false, }, ], }; } if (command === 'read_game_creator_agent_goal') { return null; } if (command === 'read_local_conversation') { return { path: `${projectPath}/.agent/conversations/agents/${agentId}/sessions/${sessionId}.jsonl`, agentId, sessionId, messages: [...messages], }; } if (command === 'read_game_creator_agent_runtime') { return runtimeResult(); } if (command === 'answer_game_creator_agent_runtime_user_input') { messages = [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: String( (args?.answers as Record).visual_direction, ), agentId, messageId: 'launcher-answer-message', updatedAt: 6100, }, ]; currentState = { ...currentState, status: 'running', phase: 'planning', currentAction: '根据用户回答继续规划', updatedAt: 6200, }; return runtimeResult(); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); const card = await screen.findByLabelText('Needs input'); expect( (screen.getByLabelText('Agent 聊天内容') as HTMLInputElement).disabled, ).toBe(true); fireEvent.click(screen.getByRole('button', { name: '折叠 Runtime 详情' })); expect(screen.getByLabelText('Needs input')).toBe(card); fireEvent.change(within(card).getByLabelText('美术方向 其他回答'), { target: { value: '低多边形卡通风' }, }); fireEvent.click(within(card).getByRole('button', { name: '提交回答' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'answer_game_creator_agent_runtime_user_input', { projectPath, agentId, runId, actionId: request.actionId, requestId: request.requestId, responseId: expect.stringMatching(/^app-user-input-/), answers: { visual_direction: '低多边形卡通风' }, }, ); }); expect(await screen.findByText('低多边形卡通风')).not.toBeNull(); expect(screen.queryByLabelText('Needs input')).toBeNull(); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_agent_runtime_task', ), ).toHaveLength(0); }); it('shows context usage and manually compacts an idle Agent session', async () => { const sessionId = 'agent-session-design-director'; let compacted = false; const runtimeResult = () => ({ state: { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId, runId: 'context-completed-run', source: 'agent-background-task', status: 'idle', phase: 'completed', currentTask: '已完成角色规范梳理', currentAction: '等待输入', waitingOn: '开发者输入', nextStep: '等待输入', loopIteration: 2, maxLoopIterations: 6, toolActionBudget: 3, plan: [], observations: [], recentToolCalls: [], pendingToolAction: null, taskQueue: { total: 1, pending: 0, running: 0, waitingForConfirmation: 0, cancelled: 0, completed: 1, failed: 0, latestRunId: 'context-completed-run', updatedAt: 5000, }, contextUsage: compacted ? { estimatedInputTokens: 12000, autoCompactTokenLimit: 48000, lastPromptTokens: 321, lastCompletionTokens: 45, lastTotalTokens: 366, compactionRevision: 3, compactionCount: 3, lastCompactionTrigger: 'manual', lastCompactedAt: 6000, } : { estimatedInputTokens: 42000, autoCompactTokenLimit: 48000, lastPromptTokens: 41000, lastCompletionTokens: 900, lastTotalTokens: 41900, compactionRevision: 2, compactionCount: 2, lastCompactionTrigger: 'auto', lastCompactedAt: 5000, }, allowedTools: ['file.read'], lastResponse: '角色规范已整理完成。', error: null, updatedAt: compacted ? 6000 : 5000, }, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: { total: 1, pending: 0, running: 0, waitingForConfirmation: 0, cancelled: 0, completed: 1, failed: 0, latestRunId: 'context-completed-run', updatedAt: 5000, }, recentEvents: [], recentTasks: [], }); 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-5.5', apiKind: 'openai_chat', reasoningEffort: 'high', stream: true, webSearchEnabled: false, contextWindowTokens: 96000, autoCompactTokenLimit: 48000, toolOutputTokenLimit: 8000, error: null, agents: [], }; } if (command === 'list_game_creator_agent_sessions') { return { path: '/tmp/authorized-game/.agent/runtime/sessions/design-director.json', agentId: 'design-director', activeSessionId: sessionId, sessions: [ { sessionId, title: '角色规范', createdAt: 1, updatedAt: 2, archivedAt: null, messageCount: 8, legacy: false, }, ], }; } if (command === 'read_local_conversation') { return { path: `/tmp/authorized-game/.agent/conversations/agents/design-director/sessions/${sessionId}.jsonl`, agentId: 'design-director', sessionId, messages: [], }; } if (command === 'read_game_creator_agent_runtime') { return runtimeResult(); } if (command === 'compact_game_creator_agent_runtime_context') { compacted = true; return { agentId: 'design-director', sessionId, runId: 'context-completed-run', trigger: 'manual', revision: 3, estimatedTokensBefore: 42000, estimatedTokensAfter: 12000, promptTokens: 321, completionTokens: 45, totalTokens: 366, coveredAgentMessages: 4, coveredProjectMessages: 0, coveredObservations: 0, reused: false, compactedAt: 6000, }; } throw new Error(`unexpected invoke ${command}`); }); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect( await screen.findByText( '上下文:预计 42000/48000 tokens · 最近实际 41000/900 · 压缩 revision 2', ), ).not.toBeNull(); const compactButton = within( screen.getByLabelText('Agent Runtime 操作'), ).getByRole('button', { name: '压缩上下文' }) as HTMLButtonElement; expect(compactButton.disabled).toBe(false); fireEvent.click(compactButton); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'compact_game_creator_agent_runtime_context', { projectPath: '/tmp/authorized-game', agentId: 'design-director', sessionId, }, ); }); expect( await screen.findByText( '上下文压缩完成:revision 3,预计 42000 -> 12000 tokens', ), ).not.toBeNull(); expect( await screen.findByText( '上下文:预计 12000/48000 tokens · 最近实际 321/45 · 压缩 revision 3', ), ).not.toBeNull(); }); it('shows queued developer agent background tasks when the agent is already running', async () => { const runningRuntimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-running', source: 'agent-delegate', parentAgentId: 'game-director', parentRunId: 'game-director-run-1', delegationId: 'delegation-design-1', status: 'running', phase: 'planning', currentTask: '正在处理上一条任务', currentAction: '生成 Agent 工具计划', loopIteration: 1, maxLoopIterations: 3, toolActionBudget: 3, plan: ['读取上下文', '回复开发者'], observations: ['上一条任务正在运行。'], taskQueue: { total: 1, pending: 0, running: 1, completed: 0, failed: 0, latestRunId: 'launcher-agent-task-running', updatedAt: 5000, }, allowedTools: ['conversation.read', 'conversation.write'], lastResponse: null, error: null, updatedAt: 5000, }; const runningTask = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'launcher-agent-task-running', source: 'agent-delegate', parentAgentId: 'game-director', parentRunId: 'game-director-run-1', delegationId: 'delegation-design-1', task: '正在处理上一条任务', status: 'running', phase: 'planning', currentAction: '生成 Agent 工具计划', error: null, updatedAt: 5000, }; const delegateReceiptTask = { ...runningTask, runId: 'delegate-receipt-gameplay-1', source: 'agent-delegate-receipt', parentAgentId: null, parentRunId: null, delegationId: 'delegation-gameplay-1', task: '接收 Gameplay Agent 委派结果', status: 'completed', phase: 'completed', currentAction: '根据委派结果继续任务', updatedAt: 4999, }; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } if (command === 'read_game_creator_agent_runtime') { return { state: runningRuntimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: runningRuntimeState.taskQueue, recentEvents: [], recentTasks: [delegateReceiptTask, runningTask], }; } if (command === 'start_game_creator_agent_runtime_task') { return { state: runningRuntimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: { total: 2, pending: 1, running: 1, completed: 0, failed: 0, latestRunId: String(args?.runId ?? 'launcher-agent-task-pending'), updatedAt: 5001, }, recentEvents: [], recentTasks: [ delegateReceiptTask, runningTask, { ...runningTask, runId: String(args?.runId ?? 'launcher-agent-task-pending'), source: 'agent-background-task', parentAgentId: null, parentRunId: null, delegationId: null, task: String(args?.task ?? ''), status: 'pending', phase: 'queued', currentAction: '等待当前后台任务完成', updatedAt: 5001, }, ], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); expect( screen.getByText( 'task: design-director · 委派自:game-director · 父 run:game-director-run-1 · 委派:delegation-design-1', ), ).not.toBeNull(); expect( screen.getByText( 'completed / completed · 接收 Gameplay Agent 委派结果 · 来源:委派回执 · 委派:delegation-gameplay-1', ), ).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '排队整理第二个需求' }, }); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect(await screen.findByText('正在处理上一条任务')).not.toBeNull(); expect(screen.getByText('Loop:1/3 · 工具预算 3')).not.toBeNull(); expect( screen.getAllByText(/已加入后台队列:launcher-agent-task-/).length, ).toBeGreaterThan(0); expect( screen.getByText(/pending \/ queued · 排队整理第二个需求/), ).not.toBeNull(); expect( screen.getByText( /任务队列:pending 1 · running 1 · waiting 0 · needsInput 0 · cancelled 0 · completed 0 · failed 0 · total 2 · latest launcher-agent-task-/, ), ).not.toBeNull(); }); it('shows developer agent runtime read failures', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: true, webSearchEnabled: false, error: null, }, ], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } if (command === 'read_game_creator_agent_runtime') { throw new Error('runtime json broken'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); expect(await screen.findByText('Runtime 状态读取失败')).not.toBeNull(); expect(screen.getByText('runtime json broken')).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '折叠 Runtime 详情' })); expect(screen.queryByText('runtime json broken')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '展开 Runtime 详情' })); expect(screen.getByText('runtime json broken')).not.toBeNull(); }); it('persists developer agent chat reply failures after saving the user message', async () => { const persistedMessages: Array<{ role: 'user' | 'assistant'; content: string; agentId: string | null; }> = []; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'check_game_creator_llm_config') { return { configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: false, webSearchEnabled: false, error: null, agents: [ { agentId: 'design-director', label: '拆解创作方向', configured: true, apiKeyPresent: true, baseUrl: 'https://llm.example.test/v1', model: 'gpt-5.5', apiKind: 'openai_chat', stream: false, webSearchEnabled: false, error: null, }, ], }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((message, index) => ({ schemaVersion: '1', ...message, updatedAt: 1000 + index, })), }; } if (command === 'chat_with_game_creator_role_agent') { throw new Error('上游 LLM 连接失败'); } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }; persistedMessages.push(message); return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: persistedMessages.map((record, index) => ({ schemaVersion: '1', ...record, updatedAt: 2000 + index, })), }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: '读取历史' })); expect(await screen.findByText(/已读取 0 条/)).not.toBeNull(); fireEvent.change(screen.getByLabelText('Agent 聊天内容'), { target: { value: '请真实回复,不要只记录' }, }); selectDeveloperAgentChatMode('chat'); fireEvent.click(screen.getByRole('button', { name: '发送' })); expect(await screen.findByText('请真实回复,不要只记录')).not.toBeNull(); expect( ( await screen.findAllByText( /已保存用户消息;Agent 回复失败:上游 LLM 连接失败/, ) ).length, ).toBeGreaterThan(0); expect(persistedMessages).toEqual([ { role: 'user', content: '请真实回复,不要只记录', agentId: null, }, { role: 'assistant', content: '已保存用户消息;Agent 回复失败:上游 LLM 连接失败', agentId: null, }, ]); }); it('loads selected developer agent history immediately after switching agents', async () => { const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_conversation') { if (args?.agentId === 'art-director') { return { path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl', agentId: 'art-director', messages: [ { schemaVersion: '1', role: 'assistant', content: '美术 Agent 历史已加载', agentId: null, updatedAt: 1000, }, ], }; } return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: args?.agentId, messages: [], }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherAgentChatAt('/?agent-chat'); fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), { target: { value: '/tmp/authorized-game' }, }); fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ })); expect(await screen.findByText('美术 Agent 历史已加载')).not.toBeNull(); expect(screen.queryByText('暂无对话')).toBeNull(); expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath: '/tmp/authorized-game', agentId: 'art-director', }); }); } export function registerDeveloperToolsTests() { 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', async () => { renderAppAt('/'); await 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('/'); await 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('/'); await 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', async () => { renderAppAt('/'); await submitChat('/help'); expect( screen.getByText(/\/audit:审计当前项目的 Agent 能力证据/), ).not.toBeNull(); expect( screen.getByText(/\/llm-routes:查看 Agent 智能服务状态/), ).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(/\/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('/'); await submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); await submitChat('/audit'); expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); expect(screen.getByText(/能力\/命令契约:通过/)).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('/'); await submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); invoke.mockClear(); await 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('/'); await submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); invoke.mockClear(); await 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('/'); await submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); invoke.mockClear(); await 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('/'); await submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); await submitChat('/audit'); expect(await screen.findByText(/权限 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('/'); await submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); await submitChat('/audit'); expect(await screen.findByText(/权限 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('/'); await submitChat('/project /tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: '确认' })); expect(await screen.findByText('已打开:authorized-game')).not.toBeNull(); await submitChat('/audit'); expect(await screen.findByText(/6 组任务配置:通过/)).not.toBeNull(); expect(screen.getByText(/6 组协作证据:待生成/)).not.toBeNull(); expect( screen.getByText( /Loop trace:待生成 · 还没有 \.agent\/run\.latest\.json/, ), ).not.toBeNull(); }); }