6bea5bb2b1
合入 origin/master 最新提交 a465e481e
保留后台错误报告、AGC 受控搜索与 Router 计费安全修复
同步 SpacetimeDB migration、技能文档和配置契约
6014 lines
206 KiB
TypeScript
6014 lines
206 KiB
TypeScript
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: '',
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '刷新状态' }));
|
||
expect(await screen.findByText('只属于新会话的问题')).not.toBeNull();
|
||
await waitFor(() => {
|
||
expect(invoke).toHaveBeenLastCalledWith(
|
||
'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.read:ok · 已读取 game/notes.txt',
|
||
updatedAt: 4005,
|
||
},
|
||
{
|
||
index: 1,
|
||
title: '结合观察修正建议',
|
||
status: 'active',
|
||
detail: '正在根据观察修正计划',
|
||
updatedAt: 4006,
|
||
},
|
||
{
|
||
index: 2,
|
||
title: '回复开发者',
|
||
status: 'failed',
|
||
detail: '最终回复仍缺少角色规范确认',
|
||
updatedAt: 4007,
|
||
},
|
||
],
|
||
activePlanStepIndex: 1,
|
||
observations: [
|
||
'思考摘要:需要先看项目笔记',
|
||
'file.read:ok · 已读取 game/notes.txt',
|
||
],
|
||
recentToolCalls: [
|
||
{
|
||
tool: 'file.read',
|
||
status: 'ok',
|
||
actionFingerprint: 'a'.repeat(64),
|
||
inputSummary: 'path=game/notes.txt',
|
||
reason: '读取笔记',
|
||
summary: '已读取 game/notes.txt',
|
||
detail: 'game/notes.txt: 连续上下文笔记',
|
||
updatedAt: 4004,
|
||
},
|
||
],
|
||
taskQueue: {
|
||
total: 1,
|
||
pending: 0,
|
||
running: 1,
|
||
completed: 0,
|
||
failed: 0,
|
||
latestRunId: 'launcher-agent-task-test',
|
||
updatedAt: 4000,
|
||
},
|
||
allowedTools: ['conversation.read', 'conversation.write'],
|
||
lastResponse: null,
|
||
error: null,
|
||
updatedAt: 4000,
|
||
};
|
||
const runningRuntimeTask = {
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-task-test',
|
||
source: 'agent-background-task',
|
||
task: '后台整理角色规范',
|
||
status: 'running',
|
||
phase: 'action',
|
||
currentAction: '调用工具 file.read',
|
||
error: null,
|
||
updatedAt: 4000,
|
||
};
|
||
const completedRuntimeState = {
|
||
...runningRuntimeState,
|
||
status: 'idle',
|
||
phase: 'completed',
|
||
currentAction: '等待下一轮输入',
|
||
waitingOn: '开发者下一轮输入',
|
||
nextStep: '等待下一轮输入',
|
||
taskQueue: {
|
||
total: 1,
|
||
pending: 0,
|
||
running: 0,
|
||
completed: 1,
|
||
failed: 0,
|
||
latestRunId: 'launcher-agent-task-test',
|
||
updatedAt: 4010,
|
||
},
|
||
lastResponse: '角色规范已整理。',
|
||
updatedAt: 4010,
|
||
};
|
||
const completedRuntimeTask = {
|
||
...runningRuntimeTask,
|
||
status: 'completed',
|
||
phase: 'completed',
|
||
currentAction: '等待下一轮输入',
|
||
updatedAt: 4010,
|
||
};
|
||
const runningRuntimeEvents = [
|
||
{
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-task-test',
|
||
source: 'agent-background-task',
|
||
eventType: 'thinking_summary',
|
||
status: 'running',
|
||
phase: 'planning',
|
||
summary: '需要先看项目笔记',
|
||
detail: null,
|
||
updatedAt: 4001,
|
||
},
|
||
{
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-task-test',
|
||
source: 'agent-background-task',
|
||
eventType: 'plan',
|
||
status: 'running',
|
||
phase: 'planning',
|
||
summary: '已生成行动计划',
|
||
detail: '读取项目笔记 / 回复开发者',
|
||
updatedAt: 4002,
|
||
},
|
||
{
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-task-test',
|
||
source: 'agent-background-task',
|
||
eventType: 'action',
|
||
status: 'running',
|
||
phase: 'action',
|
||
summary: '调用 file.read',
|
||
detail: 'game/notes.txt',
|
||
updatedAt: 4003,
|
||
},
|
||
{
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-task-test',
|
||
source: 'agent-background-task',
|
||
eventType: 'observation',
|
||
status: 'running',
|
||
phase: 'action',
|
||
summary: 'file.read 返回项目笔记',
|
||
detail: '已读取 game/notes.txt',
|
||
updatedAt: 4004,
|
||
},
|
||
{
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-task-test',
|
||
source: 'agent-background-task',
|
||
eventType: 'response',
|
||
status: 'idle',
|
||
phase: 'completed',
|
||
summary: 'Agent 已生成最终回复。',
|
||
detail: '角色规范已整理。',
|
||
updatedAt: 4005,
|
||
},
|
||
{
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-task-test',
|
||
source: 'agent-background-task',
|
||
eventType: 'error',
|
||
status: 'failed',
|
||
phase: 'failed',
|
||
summary: 'Agent Runtime 本轮处理失败。',
|
||
detail: '最终回复调用失败',
|
||
updatedAt: 4006,
|
||
},
|
||
];
|
||
let startedRunId = runningRuntimeState.runId;
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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('Loop:1/3 · 工具预算 3')).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
/任务队列:pending 0 · running 1 · waiting 0 · needsInput 0 · cancelled 0 · completed 0 · failed 0 · total 1 · latest launcher-agent-task-/,
|
||
),
|
||
).not.toBeNull();
|
||
const runtimeActions = screen.getByLabelText('Agent Runtime 操作');
|
||
expect(
|
||
(
|
||
within(runtimeActions).getByRole('button', {
|
||
name: '取消任务',
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(false);
|
||
expect(
|
||
(
|
||
within(runtimeActions).getByRole('button', {
|
||
name: '重试',
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(true);
|
||
expect(
|
||
within(screen.getByLabelText('Agent 聊天记录')).getByRole('status')
|
||
.textContent,
|
||
).toContain('等待工具观察结果');
|
||
const runtimePanel = screen.getByLabelText('Agent Runtime 状态');
|
||
const collapseRuntimeButton = within(runtimePanel).getByRole('button', {
|
||
name: '折叠 Runtime 详情',
|
||
});
|
||
expect(collapseRuntimeButton.getAttribute('aria-expanded')).toBe('true');
|
||
fireEvent.click(collapseRuntimeButton);
|
||
expect(
|
||
within(runtimePanel)
|
||
.getByRole('button', { name: '展开 Runtime 详情' })
|
||
.getAttribute('aria-expanded'),
|
||
).toBe('false');
|
||
expect(
|
||
within(runtimePanel).queryByText('当前目标:完成角色规范阶段'),
|
||
).toBeNull();
|
||
expect(within(runtimePanel).queryByText('计划进度')).toBeNull();
|
||
expect(within(runtimePanel).getByText('running / action')).not.toBeNull();
|
||
expect(
|
||
(
|
||
within(runtimeActions).getByRole('button', {
|
||
name: '取消任务',
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(false);
|
||
fireEvent.click(
|
||
within(runtimePanel).getByRole('button', {
|
||
name: '展开 Runtime 详情',
|
||
}),
|
||
);
|
||
expect(screen.getByText('计划进度')).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'#1 completed · 读取项目笔记 · file.read:ok · 已读取 game/notes.txt',
|
||
),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText('#2 active · 结合观察修正建议 · 正在根据观察修正计划'),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText('#3 failed · 回复开发者 · 最终回复仍缺少角色规范确认'),
|
||
).not.toBeNull();
|
||
expect(screen.getByText('思考摘要:需要先看项目笔记')).not.toBeNull();
|
||
expect(
|
||
screen.getByText('file.read:ok · 已读取 game/notes.txt'),
|
||
).not.toBeNull();
|
||
expect(screen.getByText('最近动作')).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'file.read · ok · 已读取 game/notes.txt · 目标:path=game/notes.txt · 读取笔记',
|
||
),
|
||
).not.toBeNull();
|
||
expect(screen.getByText('最近事件')).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'error · failed / failed · Agent Runtime 本轮处理失败。',
|
||
),
|
||
).not.toBeNull();
|
||
expect(screen.queryByText(/最终回复调用失败/u)).toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'observation · running / action · file.read 返回项目笔记 · 已读取 game/notes.txt',
|
||
),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'action · running / action · 调用 file.read · game/notes.txt',
|
||
),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'response · idle / completed · Agent 已生成最终回复。 · 角色规范已整理。',
|
||
),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.queryByText(
|
||
'thinking_summary · running / planning · 需要先看项目笔记',
|
||
),
|
||
).toBeNull();
|
||
expect(
|
||
screen.queryByText(
|
||
'plan · running / planning · 已生成行动计划 · 读取项目笔记 / 回复开发者',
|
||
),
|
||
).toBeNull();
|
||
fireEvent.click(
|
||
screen.getByRole('button', { name: '展开事件(另有 2 条)' }),
|
||
);
|
||
expect(
|
||
screen.getByText(
|
||
'thinking_summary · running / planning · 需要先看项目笔记',
|
||
),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'plan · running / planning · 已生成行动计划 · 读取项目笔记 / 回复开发者',
|
||
),
|
||
).not.toBeNull();
|
||
expect(screen.getByRole('button', { name: '收起事件' })).not.toBeNull();
|
||
expect(screen.getByText('最近任务')).not.toBeNull();
|
||
expect(
|
||
screen.getByText(/running \/ action · 后台整理角色规范/),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(/已启动后台任务:launcher-agent-task-/),
|
||
).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith(
|
||
'start_game_creator_agent_runtime_task',
|
||
expect.objectContaining({
|
||
projectPath: '/tmp/authorized-game',
|
||
agentId: 'design-director',
|
||
task: '后台整理角色规范',
|
||
}),
|
||
);
|
||
expect(invoke).not.toHaveBeenCalledWith(
|
||
'chat_with_game_creator_role_agent_stream',
|
||
expect.anything(),
|
||
);
|
||
expect(invoke).not.toHaveBeenCalledWith(
|
||
'chat_with_game_creator_role_agent',
|
||
expect.anything(),
|
||
);
|
||
expect(persistedMessages).toEqual([
|
||
{
|
||
role: 'user',
|
||
content: '后台整理角色规范',
|
||
agentId: null,
|
||
},
|
||
]);
|
||
|
||
const cancellingRuntimeState = {
|
||
...runningRuntimeState,
|
||
status: 'cancelling',
|
||
phase: 'cancelling',
|
||
currentAction: '正在取消 Agent 后台任务',
|
||
waitingOn: '当前 LLM 或工具调用返回',
|
||
nextStep: '取消完成后可重试该任务或提交新任务',
|
||
};
|
||
await act(async () => {
|
||
runtimeUpdateHandler?.({
|
||
payload: {
|
||
projectPath: '/tmp/authorized-game',
|
||
agentId: 'design-director',
|
||
runId: startedRunId,
|
||
status: 'cancelling',
|
||
phase: 'cancelling',
|
||
runtime: {
|
||
state: { ...cancellingRuntimeState, runId: startedRunId },
|
||
sessionPath:
|
||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||
eventPath:
|
||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||
taskPath:
|
||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||
taskQueue: cancellingRuntimeState.taskQueue,
|
||
recentEvents: runningRuntimeEvents,
|
||
recentTasks: [runningRuntimeTask],
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
expect(await screen.findByText('cancelling / cancelling')).not.toBeNull();
|
||
expect(screen.getByText('等待:当前 LLM 或工具调用返回')).not.toBeNull();
|
||
const cancellingActions = screen.getByLabelText('Agent Runtime 操作');
|
||
expect(
|
||
(
|
||
within(cancellingActions).getByRole('button', {
|
||
name: '取消任务',
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(true);
|
||
expect(
|
||
(
|
||
within(cancellingActions).getByRole('button', {
|
||
name: '重试',
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(true);
|
||
|
||
persistedMessages.push({
|
||
role: 'assistant',
|
||
content: '角色规范已整理。',
|
||
agentId: null,
|
||
});
|
||
await act(async () => {
|
||
runtimeUpdateHandler?.({
|
||
payload: {
|
||
projectPath: '/tmp/authorized-game',
|
||
agentId: 'design-director',
|
||
runId: startedRunId,
|
||
status: 'idle',
|
||
phase: 'completed',
|
||
runtime: {
|
||
state: { ...completedRuntimeState, runId: startedRunId },
|
||
sessionPath:
|
||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||
eventPath:
|
||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||
taskPath:
|
||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||
taskQueue: {
|
||
...completedRuntimeState.taskQueue,
|
||
latestRunId: startedRunId,
|
||
},
|
||
recentEvents: [
|
||
...runningRuntimeEvents,
|
||
{
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: startedRunId,
|
||
source: 'agent-background-task',
|
||
eventType: 'turn.completed',
|
||
status: 'idle',
|
||
phase: 'completed',
|
||
summary: 'Agent Runtime 完成本轮处理。',
|
||
detail: '角色规范已整理。',
|
||
updatedAt: 4010,
|
||
},
|
||
],
|
||
recentTasks: [{ ...completedRuntimeTask, runId: startedRunId }],
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
expect(await screen.findByText('idle / completed')).not.toBeNull();
|
||
expect(await screen.findByText('角色规范已整理。')).not.toBeNull();
|
||
expect(screen.getByText('等待:开发者下一轮输入')).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'turn.completed · idle / completed · Agent Runtime 完成本轮处理。 · 角色规范已整理。',
|
||
),
|
||
).not.toBeNull();
|
||
expect(
|
||
within(screen.getByLabelText('Agent 聊天记录')).queryByRole('status'),
|
||
).toBeNull();
|
||
expect(persistedMessages).toEqual([
|
||
{
|
||
role: 'user',
|
||
content: '后台整理角色规范',
|
||
agentId: null,
|
||
},
|
||
{
|
||
role: 'assistant',
|
||
content: '角色规范已整理。',
|
||
agentId: null,
|
||
},
|
||
]);
|
||
});
|
||
|
||
it('normalizes and restores the V1.17 persistent plan snapshot after refresh', async () => {
|
||
const sessionId = 'agent-session-plan-v117';
|
||
const planSteps = [
|
||
{ step: '读取项目上下文', status: 'completed' },
|
||
{ title: '确认目标与约束', status: 'completed' },
|
||
{ step: '补齐核心玩法', status: 'in_progress' },
|
||
{ step: '实现交互反馈', status: 'pending' },
|
||
{ step: '运行定向验证', status: 'pending' },
|
||
{ step: '检查移动端布局', status: 'pending' },
|
||
{ step: '整理变更摘要', status: 'pending' },
|
||
{ step: '准备最终回复', status: 'pending' },
|
||
{ step: '不应展示的第九步', status: 'pending' },
|
||
];
|
||
const runtimeState = {
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId,
|
||
runId: 'runtime-plan-v117',
|
||
source: 'agent-background-task',
|
||
status: 'running',
|
||
phase: 'action',
|
||
currentTask: '持续更新本轮执行计划',
|
||
currentGoal: '完成首版玩法实现',
|
||
currentAction: '补齐核心玩法',
|
||
waitingOn: '核心玩法实现结果',
|
||
nextStep: '运行定向验证',
|
||
loopIteration: 3,
|
||
maxLoopIterations: 6,
|
||
toolActionBudget: 3,
|
||
planRevision: 7,
|
||
planExplanation: '根据最新项目观察调整实现与验证顺序',
|
||
plan: planSteps.map((step) => step.step ?? step.title ?? ''),
|
||
planSteps,
|
||
activePlanStepIndex: 2,
|
||
observations: [],
|
||
allowedTools: ['file.read', 'file.patch', 'project.verify'],
|
||
lastResponse: null,
|
||
error: null,
|
||
updatedAt: 7000,
|
||
};
|
||
const runtimeResult = () => ({
|
||
state: {
|
||
...runtimeState,
|
||
planSteps: runtimeState.planSteps.map((step) => ({ ...step })),
|
||
},
|
||
sessionPath:
|
||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||
eventPath:
|
||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||
taskPath:
|
||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||
taskQueue: {
|
||
total: 1,
|
||
pending: 0,
|
||
running: 1,
|
||
completed: 0,
|
||
failed: 0,
|
||
latestRunId: runtimeState.runId,
|
||
updatedAt: runtimeState.updatedAt,
|
||
},
|
||
recentEvents: [],
|
||
recentTasks: [],
|
||
});
|
||
let runtimeReadCount = 0;
|
||
const invoke = vi.fn(async (command: string) => {
|
||
if (command === 'check_game_creator_llm_config') {
|
||
return {
|
||
configured: true,
|
||
apiKeyPresent: true,
|
||
baseUrl: 'https://llm.example.test/v1',
|
||
model: 'gpt-5.5',
|
||
apiKind: 'openai_responses',
|
||
reasoningEffort: 'high',
|
||
stream: true,
|
||
webSearchEnabled: false,
|
||
error: null,
|
||
agents: [],
|
||
};
|
||
}
|
||
if (command === 'resume_game_creator_agent_runtime_tasks') {
|
||
return [];
|
||
}
|
||
if (command === 'list_game_creator_agent_sessions') {
|
||
return {
|
||
path: '/tmp/authorized-game/.agent/conversations/agents/design-director/sessions.json',
|
||
agentId: 'design-director',
|
||
activeSessionId: sessionId,
|
||
sessions: [
|
||
{
|
||
sessionId,
|
||
title: '持久计划验证',
|
||
createdAt: 6000,
|
||
updatedAt: 7000,
|
||
archivedAt: null,
|
||
messageCount: 0,
|
||
legacy: false,
|
||
},
|
||
],
|
||
};
|
||
}
|
||
if (command === 'read_local_conversation') {
|
||
return {
|
||
path: `/tmp/authorized-game/.agent/conversations/agents/design-director/sessions/${sessionId}.jsonl`,
|
||
agentId: 'design-director',
|
||
sessionId,
|
||
messages: [],
|
||
};
|
||
}
|
||
if (command === 'read_game_creator_agent_runtime') {
|
||
runtimeReadCount += 1;
|
||
return runtimeResult();
|
||
}
|
||
throw new Error(`unexpected invoke ${command}`);
|
||
});
|
||
window.__TAURI__ = { core: { invoke } };
|
||
renderLauncherAgentChatAt('/?agent-chat');
|
||
|
||
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
|
||
target: { value: '/tmp/authorized-game' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '读取历史' }));
|
||
|
||
const planPanel = await screen.findByLabelText('Agent 计划进度');
|
||
expect(within(planPanel).getByText('计划修订号:7')).not.toBeNull();
|
||
expect(
|
||
within(planPanel).getByText(
|
||
'计划说明:根据最新项目观察调整实现与验证顺序',
|
||
),
|
||
).not.toBeNull();
|
||
expect(within(planPanel).getAllByText(/^#\d+ /)).toHaveLength(8);
|
||
expect(
|
||
within(planPanel).getByText('#2 completed · 确认目标与约束'),
|
||
).not.toBeNull();
|
||
expect(
|
||
within(planPanel).getByText('#3 in_progress · 补齐核心玩法'),
|
||
).not.toBeNull();
|
||
expect(within(planPanel).queryByText(/不应展示的第九步/)).toBeNull();
|
||
const firstSnapshot = planPanel.textContent;
|
||
const runtimeReadsBeforeRefresh = runtimeReadCount;
|
||
|
||
fireEvent.click(
|
||
within(screen.getByLabelText('Agent Runtime 操作')).getByRole('button', {
|
||
name: '刷新状态',
|
||
}),
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(runtimeReadCount).toBeGreaterThan(runtimeReadsBeforeRefresh);
|
||
});
|
||
const refreshedPlanPanel = screen.getByLabelText('Agent 计划进度');
|
||
expect(refreshedPlanPanel.textContent).toBe(firstSnapshot);
|
||
expect(within(refreshedPlanPanel).getAllByText(/^#\d+ /)).toHaveLength(8);
|
||
expect(
|
||
within(refreshedPlanPanel).queryByText(/不应展示的第九步/),
|
||
).toBeNull();
|
||
});
|
||
|
||
it('manages a persistent Goal through the developer Agent modal and exact Tauri commands', async () => {
|
||
const projectPath = '/tmp/authorized-game';
|
||
const agentId = 'design-director';
|
||
const sessionId = 'agent-session-goal-v118';
|
||
const messages: Array<{
|
||
role: 'user' | 'assistant';
|
||
content: string;
|
||
agentId: string | null;
|
||
}> = [];
|
||
let currentGoal: Record<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();
|
||
selectDeveloperAgentChatMode('goal');
|
||
expect(
|
||
screen.getByRole('button', { name: '目标' }).getAttribute('aria-pressed'),
|
||
).toBe('true');
|
||
fireEvent.click(screen.getByRole('button', { name: '开始目标' }));
|
||
|
||
let goalDialog = screen.getByRole('dialog', { name: '创建持久目标' });
|
||
fireEvent.change(within(goalDialog).getByLabelText('Goal outcome'), {
|
||
target: { value: '完成首版战斗循环' },
|
||
});
|
||
fireEvent.change(within(goalDialog).getByLabelText('Goal constraints'), {
|
||
target: { value: '保持移动端可操作\n不改变正式用户首页' },
|
||
});
|
||
fireEvent.change(within(goalDialog).getByLabelText('Goal verification'), {
|
||
target: { value: 'AppSurface 通过\nTypecheck 通过' },
|
||
});
|
||
fireEvent.click(within(goalDialog).getByRole('button', { name: '开始' }));
|
||
|
||
expect(await screen.findByText('Outcome:完成首版战斗循环')).not.toBeNull();
|
||
expect(screen.getByText('Status:active · Revision:1')).not.toBeNull();
|
||
expect(
|
||
screen.getByText('Verification:AppSurface 通过;Typecheck 通过'),
|
||
).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('start_game_creator_agent_goal', {
|
||
projectPath,
|
||
agentId,
|
||
sessionId,
|
||
outcome: '完成首版战斗循环',
|
||
constraints: ['保持移动端可操作', '不改变正式用户首页'],
|
||
verification: ['AppSurface 通过', 'Typecheck 通过'],
|
||
runId: expect.stringMatching(/^launcher-agent-goal-/),
|
||
});
|
||
|
||
fireEvent.click(
|
||
within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', {
|
||
name: '暂停',
|
||
}),
|
||
);
|
||
expect(
|
||
await screen.findByText('Status:pause-requested · Revision:1'),
|
||
).not.toBeNull();
|
||
expect(
|
||
within(screen.getByLabelText('Agent Goal 状态')).queryByRole('button', {
|
||
name: '恢复',
|
||
}),
|
||
).toBeNull();
|
||
currentGoal = {
|
||
...currentGoal!,
|
||
status: 'paused',
|
||
pausedAt: 4500,
|
||
updatedAt: 4500,
|
||
};
|
||
// A terminal-looking Runtime proves paused Goal metadata disables ordinary retry.
|
||
runtimeStatus = 'completed';
|
||
runtimePhase = 'completed';
|
||
fireEvent.click(
|
||
within(screen.getByLabelText('Agent Runtime 操作')).getByRole('button', {
|
||
name: '刷新状态',
|
||
}),
|
||
);
|
||
expect(
|
||
await screen.findByText('Status:paused · Revision:1'),
|
||
).not.toBeNull();
|
||
expect(screen.getByText(/paused 1/)).not.toBeNull();
|
||
expect(
|
||
(
|
||
within(screen.getByLabelText('Agent Runtime 操作')).getByRole(
|
||
'button',
|
||
{ name: '重试' },
|
||
) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(true);
|
||
expect(
|
||
(
|
||
screen.getByRole('button', {
|
||
name: '新建 Agent 会话',
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(true);
|
||
expect(invoke).toHaveBeenCalledWith('pause_game_creator_agent_goal', {
|
||
projectPath,
|
||
agentId,
|
||
sessionId,
|
||
goalId: 'goal-design-v118',
|
||
expectedRevision: 1,
|
||
});
|
||
|
||
fireEvent.click(
|
||
within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', {
|
||
name: '编辑',
|
||
}),
|
||
);
|
||
goalDialog = screen.getByRole('dialog', { name: '编辑持久目标' });
|
||
fireEvent.change(within(goalDialog).getByLabelText('Goal outcome'), {
|
||
target: { value: '完成并验证首版战斗循环' },
|
||
});
|
||
fireEvent.change(within(goalDialog).getByLabelText('Goal verification'), {
|
||
target: { value: 'AppSurface 通过\nTypecheck 通过\n移动端可操作' },
|
||
});
|
||
fireEvent.click(within(goalDialog).getByRole('button', { name: '保存' }));
|
||
|
||
expect(
|
||
await screen.findByText('Outcome:完成并验证首版战斗循环'),
|
||
).not.toBeNull();
|
||
expect(screen.getByText('Status:paused · Revision:2')).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('edit_game_creator_agent_goal', {
|
||
projectPath,
|
||
agentId,
|
||
sessionId,
|
||
goalId: 'goal-design-v118',
|
||
expectedRevision: 1,
|
||
outcome: '完成并验证首版战斗循环',
|
||
constraints: ['保持移动端可操作', '不改变正式用户首页'],
|
||
verification: ['AppSurface 通过', 'Typecheck 通过', '移动端可操作'],
|
||
});
|
||
|
||
fireEvent.click(
|
||
within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', {
|
||
name: '恢复',
|
||
}),
|
||
);
|
||
expect(
|
||
await screen.findByText('Status:active · Revision:2'),
|
||
).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('resume_game_creator_agent_goal', {
|
||
projectPath,
|
||
agentId,
|
||
sessionId,
|
||
goalId: 'goal-design-v118',
|
||
expectedRevision: 2,
|
||
});
|
||
|
||
fireEvent.click(
|
||
within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', {
|
||
name: '清理',
|
||
}),
|
||
);
|
||
expect(
|
||
await screen.findByText('Status:cleared · Revision:2'),
|
||
).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('clear_game_creator_agent_goal', {
|
||
projectPath,
|
||
agentId,
|
||
sessionId,
|
||
goalId: 'goal-design-v118',
|
||
expectedRevision: 2,
|
||
});
|
||
expect(
|
||
within(screen.getByLabelText('Agent Goal 状态')).getByRole('button', {
|
||
name: '开始',
|
||
}),
|
||
).not.toBeNull();
|
||
});
|
||
|
||
it('clears stale Goal UI before reading the newly selected Agent and Session Goal', async () => {
|
||
const projectPath = '/tmp/authorized-game';
|
||
const designAgentId = 'design-director';
|
||
const foundationAgentId = 'design-foundation';
|
||
const designSessionA = 'design-session-a';
|
||
const designSessionB = 'design-session-b';
|
||
const foundationSession = 'foundation-session-a';
|
||
const activeSessionByAgent: Record<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.read:waiting-for-confirmation'],
|
||
recentToolCalls: [
|
||
{
|
||
tool: 'file.read',
|
||
status: 'waiting-for-confirmation',
|
||
actionFingerprint: 'b'.repeat(64),
|
||
inputSummary: 'path=game/notes.txt',
|
||
reason: '读取角色规范依据',
|
||
summary: '项目权限策略要求用户确认:file.read',
|
||
detail: null,
|
||
updatedAt: 5000,
|
||
},
|
||
],
|
||
pendingToolAction: {
|
||
actionId: `action-${'b'.repeat(24)}`,
|
||
actionFingerprint: 'b'.repeat(64),
|
||
tool: 'file.read',
|
||
inputSummary: 'path=game/notes.txt',
|
||
reason: '读取角色规范依据',
|
||
requestedAt: 5000,
|
||
},
|
||
taskQueue: {
|
||
total: 1,
|
||
pending: 0,
|
||
running: 0,
|
||
waitingForConfirmation: 1,
|
||
cancelled: 0,
|
||
completed: 0,
|
||
failed: 0,
|
||
latestRunId: 'launcher-agent-waiting',
|
||
updatedAt: 5000,
|
||
},
|
||
allowedTools: ['file.read'],
|
||
lastResponse: null,
|
||
error: null,
|
||
updatedAt: 5000,
|
||
};
|
||
const waitingTask = {
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'design-director',
|
||
taskId: 'design-director',
|
||
sessionId: 'agent-session-design-director',
|
||
runId: 'launcher-agent-waiting',
|
||
source: 'agent-background-task',
|
||
task: '读取角色规范笔记',
|
||
status: 'waiting-for-confirmation',
|
||
phase: 'waiting-for-confirmation',
|
||
currentAction: '等待确认工具 file.read',
|
||
error: null,
|
||
updatedAt: 5000,
|
||
};
|
||
const runtimeResult = {
|
||
state: waitingRuntimeState,
|
||
sessionPath:
|
||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||
eventPath:
|
||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||
taskPath:
|
||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||
taskQueue: waitingRuntimeState.taskQueue,
|
||
recentEvents: [],
|
||
recentTasks: [waitingTask],
|
||
};
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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 · 父 run:game-director-run-1 · 委派:delegation-design-1',
|
||
),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
'completed / completed · 接收 Gameplay Agent 委派结果 · 来源:委派回执 · 委派:delegation-gameplay-1',
|
||
),
|
||
).not.toBeNull();
|
||
|
||
fireEvent.change(screen.getByLabelText('Agent 聊天内容'), {
|
||
target: { value: '排队整理第二个需求' },
|
||
});
|
||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||
|
||
expect(await screen.findByText('正在处理上一条任务')).not.toBeNull();
|
||
expect(screen.getByText('Loop:1/3 · 工具预算 3')).not.toBeNull();
|
||
expect(
|
||
screen.getAllByText(/已加入后台队列:launcher-agent-task-/).length,
|
||
).toBeGreaterThan(0);
|
||
expect(
|
||
screen.getByText(/pending \/ queued · 排队整理第二个需求/),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
/任务队列:pending 1 · running 1 · waiting 0 · needsInput 0 · cancelled 0 · completed 0 · failed 0 · total 2 · latest launcher-agent-task-/,
|
||
),
|
||
).not.toBeNull();
|
||
});
|
||
|
||
it('shows developer agent runtime read failures', async () => {
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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', () => {
|
||
renderAppAt('/');
|
||
|
||
submitChat('/capabilities');
|
||
|
||
const capabilities = screen.getByText(/Agent 能力清单:/);
|
||
expect(capabilities.textContent).toMatch(/任务拆分/);
|
||
expect(capabilities.textContent).toMatch(/任务编排/);
|
||
expect(capabilities.textContent).toMatch(
|
||
/Planner \/ Generator \/ Evaluator 循环/,
|
||
);
|
||
expect(capabilities.textContent).toMatch(/多智能体协作/);
|
||
expect(capabilities.textContent).toMatch(/短期记忆/);
|
||
expect(capabilities.textContent).toMatch(/长期记忆/);
|
||
expect(capabilities.textContent).toMatch(/本地 HTTP 预览/);
|
||
expect(screen.queryByLabelText('开发环境')).toBeNull();
|
||
expect(screen.queryByText('编排 Trace')).toBeNull();
|
||
});
|
||
|
||
it('loads agent capabilities from the native runtime when available', async () => {
|
||
const invoke = vi.fn(async (command: string) => {
|
||
if (command === 'get_game_creation_agent_capabilities') {
|
||
return [
|
||
{
|
||
id: 'native-only-capability',
|
||
area: 'agent-runtime',
|
||
title: 'Native Runtime 能力',
|
||
},
|
||
];
|
||
}
|
||
throw new Error(`unexpected invoke ${command}`);
|
||
});
|
||
window.__TAURI__ = { core: { invoke } };
|
||
renderAppAt('/');
|
||
|
||
submitChat('/capabilities');
|
||
|
||
expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull();
|
||
expect(screen.queryByText(/任务拆分/)).toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('get_game_creation_agent_capabilities');
|
||
});
|
||
|
||
it('falls back to standard agent capabilities when the native runtime returns none', async () => {
|
||
const invoke = vi.fn(async (command: string) => {
|
||
if (command === 'get_game_creation_agent_capabilities') {
|
||
return [];
|
||
}
|
||
throw new Error(`unexpected invoke ${command}`);
|
||
});
|
||
window.__TAURI__ = { core: { invoke } };
|
||
renderAppAt('/');
|
||
|
||
submitChat('/capabilities');
|
||
|
||
expect(await screen.findByText(/任务拆分/)).not.toBeNull();
|
||
expect(screen.getByText(/本地 HTTP 预览/)).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('get_game_creation_agent_capabilities');
|
||
});
|
||
|
||
it('exposes the audit command from chat help', () => {
|
||
renderAppAt('/');
|
||
|
||
submitChat('/help');
|
||
|
||
expect(
|
||
screen.getByText(/\/audit:审计当前项目的 Agent 能力证据/),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(/\/llm-routes:查看 Agent 智能服务状态/),
|
||
).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('/');
|
||
|
||
submitChat('/project /tmp/authorized-game');
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||
|
||
submitChat('/audit');
|
||
|
||
expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull();
|
||
expect(screen.getByText(/能力\/命令契约:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/6 组任务配置:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/6 组协作证据:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/角色任务 16 个/)).not.toBeNull();
|
||
expect(
|
||
screen.getByText(/Loop trace:通过 · run run-chat-audit/),
|
||
).not.toBeNull();
|
||
expect(
|
||
screen.getByText(/Planner\/Orchestrator\/Generator\/Evaluator:通过/),
|
||
).not.toBeNull();
|
||
expect(screen.getByText(/返工路由\/Carry-over:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/记忆:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/本地产物:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/本地 HTTP 预览:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/画板回流:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/权限 Gate\/命令日志:通过/)).not.toBeNull();
|
||
expect(screen.queryByLabelText('开发环境')).toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', {
|
||
projectPath: '/tmp/authorized-game',
|
||
commandId: 'agent.audit',
|
||
});
|
||
expect(invoke).toHaveBeenCalledWith('list_local_project_files', {
|
||
projectPath: '/tmp/authorized-game',
|
||
});
|
||
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
});
|
||
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/logs/command.log',
|
||
commandId: 'file.read',
|
||
});
|
||
});
|
||
|
||
it('requires trace read confirmation before audit reads run trace', async () => {
|
||
const manifest = createGameCreationAppManifest(
|
||
'local-project-draft',
|
||
'未命名游戏原型',
|
||
);
|
||
const trace: GameCreationAgentRunTrace = {
|
||
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
||
runId: 'run-audit-trace-confirm',
|
||
commandId: 'game.generate_draft',
|
||
status: 'passed',
|
||
lifecycleStatus: 'done',
|
||
passes: 1,
|
||
maxPasses: 3,
|
||
toolCallCount: 1,
|
||
maxToolCalls: 128,
|
||
stopReason: 'evaluator-passed',
|
||
goal: '做一个厨房弹幕游戏',
|
||
coordination: 'filesystem',
|
||
steps: [],
|
||
artifacts: [],
|
||
taskGraph: {
|
||
goal: '做一个厨房弹幕游戏',
|
||
readyTaskIds: [],
|
||
activeTaskIds: [],
|
||
carriedTaskIds: [],
|
||
repairFocus: [],
|
||
repairRoutes: [],
|
||
tasks: createGameCreationAppSeedTasks(),
|
||
},
|
||
passPlans: [],
|
||
nextStep: 'preview',
|
||
error: null,
|
||
updatedAt: 1,
|
||
};
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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('/');
|
||
|
||
submitChat('/project /tmp/authorized-game');
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||
invoke.mockClear();
|
||
|
||
submitChat('/audit');
|
||
|
||
expect(
|
||
await screen.findByText('准备确认审计读取:agent.trace_read'),
|
||
).not.toBeNull();
|
||
expect(screen.getByText('agent.trace_read')).not.toBeNull();
|
||
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
});
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
|
||
expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
});
|
||
});
|
||
|
||
it('requires file read confirmation before audit reads command logs', async () => {
|
||
const manifest = createGameCreationAppManifest(
|
||
'local-project-draft',
|
||
'未命名游戏原型',
|
||
);
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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('/');
|
||
|
||
submitChat('/project /tmp/authorized-game');
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||
invoke.mockClear();
|
||
|
||
submitChat('/audit');
|
||
|
||
expect(
|
||
await screen.findByText('准备确认审计读取:file.read'),
|
||
).not.toBeNull();
|
||
expect(screen.getByText('file.read')).not.toBeNull();
|
||
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/logs/command.log',
|
||
commandId: 'file.read',
|
||
});
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
|
||
expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/logs/command.log',
|
||
commandId: 'file.read',
|
||
});
|
||
});
|
||
|
||
it('confirms each audit read policy instead of one confirmation unlocking all reads', async () => {
|
||
const manifest = createGameCreationAppManifest(
|
||
'local-project-draft',
|
||
'未命名游戏原型',
|
||
);
|
||
const trace: GameCreationAgentRunTrace = {
|
||
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
||
runId: 'run-audit-multi-confirm',
|
||
commandId: 'game.generate_draft',
|
||
status: 'passed',
|
||
lifecycleStatus: 'done',
|
||
passes: 1,
|
||
maxPasses: 3,
|
||
toolCallCount: 1,
|
||
maxToolCalls: 128,
|
||
stopReason: 'evaluator-passed',
|
||
goal: '做一个厨房弹幕游戏',
|
||
coordination: 'filesystem',
|
||
steps: [],
|
||
artifacts: [],
|
||
taskGraph: {
|
||
goal: '做一个厨房弹幕游戏',
|
||
readyTaskIds: [],
|
||
activeTaskIds: [],
|
||
carriedTaskIds: [],
|
||
repairFocus: [],
|
||
repairRoutes: [],
|
||
tasks: createGameCreationAppSeedTasks(),
|
||
},
|
||
passPlans: [],
|
||
nextStep: 'preview',
|
||
error: null,
|
||
updatedAt: 1,
|
||
};
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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('/');
|
||
|
||
submitChat('/project /tmp/authorized-game');
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||
invoke.mockClear();
|
||
|
||
submitChat('/audit');
|
||
|
||
expect(
|
||
await screen.findByText('准备确认审计读取:file.read'),
|
||
).not.toBeNull();
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
|
||
expect(
|
||
await screen.findByText('准备确认审计读取:agent.trace_read'),
|
||
).not.toBeNull();
|
||
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/logs/command.log',
|
||
commandId: 'file.read',
|
||
});
|
||
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
});
|
||
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
|
||
expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull();
|
||
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
|
||
projectPath: '/tmp/authorized-game',
|
||
relativePath: '.agent/run.latest.json',
|
||
commandId: 'agent.trace_read',
|
||
});
|
||
});
|
||
|
||
it('does not mark permission gate as passed without durable permission events', async () => {
|
||
const manifest = createGameCreationAppManifest(
|
||
'local-project-draft',
|
||
'未命名游戏原型',
|
||
);
|
||
const auditedManifest = {
|
||
...manifest,
|
||
commandRuns: [
|
||
{
|
||
commandId: 'game.static_smoke',
|
||
status: 'completed' as const,
|
||
output: 'ok',
|
||
logPath: '.agent/logs/command.log',
|
||
updatedAt: 1,
|
||
},
|
||
],
|
||
};
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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('/');
|
||
|
||
submitChat('/project /tmp/authorized-game');
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||
|
||
submitChat('/audit');
|
||
|
||
expect(await screen.findByText(/权限 Gate\/命令日志:待补/)).not.toBeNull();
|
||
expect(
|
||
screen.getByText(/缺 permission\.pending 或确认\/取消记录/),
|
||
).not.toBeNull();
|
||
});
|
||
|
||
it('marks permission gate as passed with durable auto command logs', async () => {
|
||
const manifest = createGameCreationAppManifest(
|
||
'local-project-draft',
|
||
'未命名游戏原型',
|
||
);
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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('/');
|
||
|
||
submitChat('/project /tmp/authorized-game');
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||
|
||
submitChat('/audit');
|
||
|
||
expect(await screen.findByText(/权限 Gate\/命令日志:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/含 auto 权限记录/)).not.toBeNull();
|
||
});
|
||
|
||
it('does not mark multi-agent collaboration as passed before a run trace exists', async () => {
|
||
const manifest = createGameCreationAppManifest(
|
||
'local-project-draft',
|
||
'未命名游戏原型',
|
||
);
|
||
const invoke = vi.fn(
|
||
async (command: string, args?: Record<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('/');
|
||
|
||
submitChat('/project /tmp/authorized-game');
|
||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||
|
||
submitChat('/audit');
|
||
|
||
expect(await screen.findByText(/6 组任务配置:通过/)).not.toBeNull();
|
||
expect(screen.getByText(/6 组协作证据:待生成/)).not.toBeNull();
|
||
expect(
|
||
screen.getByText(
|
||
/Loop trace:待生成 · 还没有 \.agent\/run\.latest\.json/,
|
||
),
|
||
).not.toBeNull();
|
||
});
|
||
}
|