Files
lhk229 ea141d9f17
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m11s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m4s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 5m50s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m59s
Project CI / AI game creator shell Rust crates (push) Successful in 2m28s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 4m37s
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
修复持久目标模式切换测试的异步竞态
- developer-agent-window.suite.ts 在切换目标模式前等待按钮脱离 disabled,避免读取历史链路在途时点击落空导致 aria-pressed 断言偶发失败
2026-09-19 01:21:05 +08:00

6023 lines
207 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown>) => {
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<string, unknown>) => {
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<void>((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<string, unknown>) => {
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<unknown[]>((resolve) => {
resolveConfirmedResume = resolve;
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
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<string, Array<{ role: string; content: string }>>([
[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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown> }) => 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<string, unknown> }) => 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<string, unknown>) => {
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<void>((resolve) => {
releaseFirstDelta = resolve;
});
streamHandler?.({
payload: {
...payloadBase,
status: 'delta',
deltaText: '专业',
accumulatedText: '专业',
},
});
streamHandler?.({
payload: {
...payloadBase,
status: 'delta',
deltaText: ' Agent',
accumulatedText: '专业 Agent',
},
});
await new Promise<void>((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<string, unknown> }) => void)
| null = null;
let streamRunId = '';
let releaseStream: (() => void) | null = null;
const listen = vi.fn(
async (
eventName: string,
handler: (event: { payload: Record<string, unknown> }) => 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<string, unknown>) => {
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<void>((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<string, unknown>) => {
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.readok · 已读取 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.readok · 已读取 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<string, unknown>) => {
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<string, unknown> }) => void)
| null = null;
const listen = vi.fn(
async (
eventName: string,
handler: (event: { payload: Record<string, unknown> }) => 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('Loop1/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.readok · 已读取 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.readok · 已读取 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<string, unknown> | null = null;
let runtimeStatus = 'idle';
let runtimePhase = 'idle';
const goalRecord = (overrides: Record<string, unknown> = {}) => ({
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<string, unknown>) => {
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();
// 读取历史的异步链路未结束时按钮处于 disabled,等它可用再切换模式,避免点击落空。
const goalModeButton = screen.getByRole('button', { name: '目标' });
await waitFor(() => {
expect(goalModeButton.hasAttribute('disabled')).toBe(false);
});
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('Statusactive · Revision1')).not.toBeNull();
expect(
screen.getByText('VerificationAppSurface 通过;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('Statuspause-requested · Revision1'),
).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('Statuspaused · Revision1'),
).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('Statuspaused · Revision2')).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('Statusactive · Revision2'),
).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('Statuscleared · Revision2'),
).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<string, string> = {
[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<typeof designGoalB>((resolve) => {
resolveDesignGoalB = resolve;
});
let resolveFoundationGoal: ((value: typeof foundationGoal) => void) | null =
null;
const foundationGoalRead = new Promise<typeof foundationGoal>((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<string, unknown>) => {
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<string, unknown> }) => void)
| null = null;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
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<string, unknown> }) => 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<string, unknown> | 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<string, unknown> | 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.readwaiting-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<string, unknown>) => {
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<Record<string, unknown>> = [];
let currentState: Record<string, unknown> = {
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<string, unknown>) => {
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<string, string>).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<string, unknown>) => {
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 · 父 rungame-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('Loop1/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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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: '<canvas id="game"></canvas>',
};
}
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',
'<canvas id="game"></canvas>',
);
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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<string, unknown>) => {
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();
});
}