Files
Genarrative/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts
T
kdletters 794f3d4cf8
Project CI / Repository checks (push) Failing after 45s
Project CI / Frontend tests (push) Successful in 3m39s
Project CI / Backend tests (push) Successful in 4m44s
Project CI / Native shell tests (push) Failing after 13m11s
修复Agent失败原因丢失与成功误判
解析 Codex app-server 稳定失败分类并生成安全可行动摘要
统一 Runtime 事件、任务、阶段记录和各 Agent 状态面的失败展示
限制自主构建最终回复 fallback,避免鉴权与网络错误被提交为成功
补充失败分类、防泄漏、待核对和 fallback 回归测试
同步技术方案与共享决策记录
2026-08-12 21:35:11 +08:00

3179 lines
111 KiB
TypeScript

import {
act,
agentRuntimeUserInputRequest,
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
createProjectSupervisorRuntimeHarness,
emptyProjectPolicy,
expect,
fireEvent,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
it,
mockRoleAgentReply,
openMainProject,
renderAppAt,
roleAgentMockReply,
screen,
submitChat,
vi,
waitFor,
within,
} from './harness';
export function registerProjectConversationTests() {
it('loads the first run history page by file modified time before reading traces', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const makeTrace = (runId: string, updatedAt: number) =>
({
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId,
commandId: 'game.generate_draft',
status: 'passed',
lifecycleStatus: 'done',
passes: 1,
maxPasses: 3,
toolCallCount: 0,
maxToolCalls: 128,
stopReason: 'evaluator-passed',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner -> Generator',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'preview',
error: null,
updatedAt,
}) satisfies GameCreationAgentRunTrace;
const runFiles = [
{
path: '.agent/runs/000-latest.json',
kind: 'file',
size: 200,
modifiedAt: 10_000,
},
...Array.from({ length: 20 }, (_, index) => {
const number = String(index + 1).padStart(2, '0');
return {
path: `.agent/runs/z-${number}.json`,
kind: 'file',
size: 100 + index,
modifiedAt: index + 1,
};
}),
];
const traces = new Map<string, GameCreationAgentRunTrace>([
['.agent/run.latest.json', makeTrace('run-current', 20_000)],
['.agent/runs/000-latest.json', makeTrace('run-latest', 10_000)],
...runFiles
.slice(1)
.map(
(file, index) =>
[
file.path,
makeTrace(`run-${String(index + 1).padStart(2, '0')}`, index + 1),
] as const,
),
]);
const invoke = vi.fn(
async (command: string, args?: Record<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_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: runFiles,
};
}
if (command === 'read_local_project_file') {
const trace = traces.get(String(args?.relativePath ?? ''));
if (!trace) {
throw new Error(
`missing trace ${String(args?.relativePath ?? '')}`,
);
}
return {
path: String(args?.relativePath ?? ''),
absolutePath: `${String(args?.projectPath ?? '')}/${String(
args?.relativePath ?? '',
)}`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByLabelText('Agent run history');
expect(screen.getByText(/run-latest/)).not.toBeNull();
expect(screen.queryByText(/run-01/)).toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/runs/z-01.json',
});
});
it('loads project conversation history in the main project window', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '历史需求:做一个厨房弹幕游戏',
agentId: null,
updatedAt: 1,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '历史回复:已生成第一版',
agentId: null,
updatedAt: 2,
},
],
};
}
if (command === 'read_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
expect(
await screen.findByText('历史需求:做一个厨房弹幕游戏'),
).not.toBeNull();
expect(screen.getByText('历史回复:已生成第一版')).not.toBeNull();
expect(screen.queryByLabelText('工作区管理')).toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: null,
});
expect(window.localStorage.length).toBe(1);
expect(
window.localStorage.getItem(window.localStorage.key(0) ?? ''),
).toContain('/tmp/authorized-game');
});
it('loads project conversation history after opening from chat command', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '历史需求:保留弹幕厨房',
agentId: null,
updatedAt: 1,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '历史回复:继续做第二版',
agentId: null,
updatedAt: 2,
},
],
};
}
if (command === 'read_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('历史需求:保留弹幕厨房')).not.toBeNull();
expect(screen.getByText('历史回复:继续做第二版')).not.toBeNull();
expect(
screen.queryByText('已设置本地项目:/tmp/authorized-game'),
).toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: null,
});
expect(invoke).not.toHaveBeenCalledWith(
'append_local_conversation_message',
expect.anything(),
);
});
it('reloads project conversation history from chat on demand', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'chat_with_game_creator_agent') {
return {
replyText: `主聊天回复:${String(args?.prompt ?? '')}`,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '重载历史需求',
agentId: null,
updatedAt: 1,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '重载历史回复',
agentId: null,
updatedAt: 2,
},
],
};
}
if (command === 'read_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('重载历史需求')).not.toBeNull();
submitChat('临时未保存的输入');
expect(await screen.findByText('临时未保存的输入')).not.toBeNull();
submitChat('/history');
expect(await screen.findByText('重载历史回复')).not.toBeNull();
expect(screen.queryByText('临时未保存的输入')).toBeNull();
expect(screen.getByText('已读取项目对话历史:2 条')).not.toBeNull();
submitChat('另一条临时未保存的输入');
expect(await screen.findByText('另一条临时未保存的输入')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '历史' }));
expect(await screen.findByText('重载历史回复')).not.toBeNull();
expect(screen.queryByText('另一条临时未保存的输入')).toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: null,
});
});
it('does not persist the transient project open status while history is loading', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let finishConversationRead:
| ((value: { path: string; agentId: null; messages: [] }) => void)
| null = null;
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return await new Promise((resolve) => {
finishConversationRead = resolve as typeof finishConversationRead;
});
}
if (command === 'read_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
projectPath: '/tmp/authorized-game',
});
});
expect(invoke).not.toHaveBeenCalledWith(
'append_local_conversation_message',
expect.objectContaining({
message: expect.objectContaining({
content: '已设置本地项目:/tmp/authorized-game',
}),
}),
);
await act(async () => {
finishConversationRead?.({
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
});
});
expect(invoke).not.toHaveBeenCalledWith(
'append_local_conversation_message',
expect.objectContaining({
message: expect.objectContaining({
content: '已设置本地项目:/tmp/authorized-game',
}),
}),
);
});
it('confirms before loading project conversation history when policy requires it', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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: ['conversation.read'],
},
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '受保护历史需求',
agentId: null,
updatedAt: 1,
},
],
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
expect(await screen.findByText('想做什么游戏?')).not.toBeNull();
expect(screen.queryByText('受保护历史需求')).toBeNull();
expect(await screen.findByText('conversation.read')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: null,
});
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('受保护历史需求')).not.toBeNull();
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: null,
});
});
it('keeps the project chat usable after cancelling conversation history read', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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: ['conversation.read'],
},
};
}
if (command === 'read_local_conversation') {
throw new Error('should wait for conversation confirmation');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
expect(await screen.findByText('想做什么游戏?')).not.toBeNull();
const conversationReadCommand =
await screen.findByText('conversation.read');
fireEvent.click(
within(
conversationReadCommand.closest('.pending-command') as HTMLElement,
).getByRole('button', { name: '取消' }),
);
expect(await screen.findByText('已取消读取项目对话')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: null,
});
});
it('reports conversation history read failure after confirmation', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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: ['conversation.read'],
},
};
}
if (command === 'read_local_conversation') {
throw new Error('conversation read failed');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
expect(await screen.findByText('想做什么游戏?')).not.toBeNull();
expect(await screen.findByText('conversation.read')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('项目对话读取失败:conversation read failed'),
).not.toBeNull();
expect(screen.getByText('想做什么游戏?')).not.toBeNull();
});
it('shows project conversation history in recent batches', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const messages = Array.from({ length: 25 }, (_, index) => ({
schemaVersion: 'game-creator-conversation.v1',
role: index % 2 === 0 ? ('user' as const) : ('assistant' as const),
content: `历史对话 ${String(index + 1).padStart(2, '0')}`,
agentId: null,
updatedAt: index + 1,
}));
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages,
};
}
if (command === 'read_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
expect(await screen.findByText('历史对话 06')).not.toBeNull();
expect(screen.getByText('历史对话 25')).not.toBeNull();
expect(screen.queryByText('历史对话 05')).toBeNull();
fireEvent.click(
screen.getByRole('button', {
name: '显示更早 · 还有 5 条对话',
}),
);
expect(screen.getByText('历史对话 01')).not.toBeNull();
expect(screen.getByText('历史对话 05')).not.toBeNull();
expect(screen.queryByRole('button', { name: /显示更早/ })).toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'append_local_conversation_message',
expect.anything(),
);
});
it('retries unsaved slash chat messages after conversation persistence fails', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let appendAttempts = 0;
let releaseRetryAppend: (() => void) | null = null;
const savedContents: string[] = [];
const makeConversationResult = () => ({
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
});
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'chat_with_game_creator_agent') {
return {
replyText: `主聊天回复:${String(args?.prompt ?? '')}`,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
if (command === 'chat_with_game_creator_role_agent') {
return {
replyText: mockRoleAgentReply(),
};
}
if (command === 'append_local_conversation_message') {
appendAttempts += 1;
const message = args?.message as { content: string };
if (appendAttempts === 1) {
throw new Error('conversation append failed once');
}
if (appendAttempts === 2) {
return await new Promise((resolve) => {
releaseRetryAppend = () => {
savedContents.push(message.content);
resolve(makeConversationResult());
};
});
}
savedContents.push(message.content);
return makeConversationResult();
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
submitChat('/第一条本地命令');
await waitFor(() => {
expect(appendAttempts).toBe(1);
});
expect(
await screen.findByText(
'项目对话保存失败:conversation append failed once',
),
).not.toBeNull();
submitChat('/第二条本地命令');
await waitFor(() => {
expect(releaseRetryAppend).not.toBeNull();
});
await act(async () => {
releaseRetryAppend?.();
});
await waitFor(() => {
expect(savedContents).toEqual([
'/第一条本地命令',
'未知命令:/第一条本地命令。输入 /help 查看可用命令。',
'/第二条本地命令',
'未知命令:/第二条本地命令。输入 /help 查看可用命令。',
]);
});
expect(
screen.queryByText('项目对话保存失败:conversation append failed once'),
).toBeNull();
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
});
it('confirms before saving slash conversation when policy requires it', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const savedContents: string[] = [];
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: ['conversation.write'],
},
};
}
if (command === 'chat_with_game_creator_agent') {
return {
replyText: `主聊天回复:${String(args?.prompt ?? '')}`,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
if (command === 'append_local_conversation_message') {
savedContents.push(
String((args?.message as { content?: unknown })?.content ?? ''),
);
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
invoke.mockClear();
submitChat('/需要确认保存');
const conversationWriteCommand =
await screen.findByText('conversation.write');
expect(await screen.findByText('等待确认保存项目对话')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'append_local_conversation_message',
expect.anything(),
);
fireEvent.click(
within(
conversationWriteCommand.closest('.pending-command') as HTMLElement,
).getByRole('button', { name: '确认' }),
);
await waitFor(() => {
expect(savedContents).toEqual([
'/需要确认保存',
'未知命令:/需要确认保存。输入 /help 查看可用命令。',
]);
});
});
it('does not immediately re-prompt after cancelling slash conversation save', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const savedContents: string[] = [];
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: ['conversation.write'],
},
};
}
if (command === 'chat_with_game_creator_agent') {
return {
replyText: `主聊天回复:${String(args?.prompt ?? '')}`,
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_local_project_file') {
throw new Error('missing trace');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
if (command === 'append_local_conversation_message') {
savedContents.push(
String((args?.message as { content?: unknown })?.content ?? ''),
);
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
invoke.mockClear();
submitChat('/先不保存');
const firstPrompt = await screen.findByText('conversation.write');
fireEvent.click(
within(firstPrompt.closest('.pending-command') as HTMLElement).getByRole(
'button',
{ name: '取消' },
),
);
await waitFor(() => {
expect(screen.queryByText('conversation.write')).toBeNull();
});
expect(screen.getByText('已打开:authorized-game')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'append_local_conversation_message',
expect.anything(),
);
submitChat('/继续补充');
const secondPrompt = await screen.findByText('conversation.write');
fireEvent.click(
within(secondPrompt.closest('.pending-command') as HTMLElement).getByRole(
'button',
{ name: '确认' },
),
);
await waitFor(() => {
expect(savedContents).toEqual([
'/先不保存',
'未知命令:/先不保存。输入 /help 查看可用命令。',
'/继续补充',
'未知命令:/继续补充。输入 /help 查看可用命令。',
]);
});
});
it('persists slash chat messages submitted while a previous write is still running', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const savedContents: string[] = [];
let releaseFirstAppend: (() => void) | null = null;
const makeConversationResult = () => ({
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
});
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'chat_with_game_creator_agent') {
return {
replyText: `主聊天回复:${String(args?.prompt ?? '')}`,
};
}
if (command === 'read_local_conversation') {
return makeConversationResult();
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
if (command === 'append_local_conversation_message') {
const message = args?.message as { content: string };
if (savedContents.length === 0 && !releaseFirstAppend) {
return await new Promise((resolve) => {
releaseFirstAppend = () => {
savedContents.push(message.content);
resolve(makeConversationResult());
};
});
}
savedContents.push(message.content);
return makeConversationResult();
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
submitChat('/第一条并发命令');
await waitFor(() => {
expect(releaseFirstAppend).not.toBeNull();
});
submitChat('/第二条并发命令');
releaseFirstAppend?.();
await waitFor(() => {
expect(savedContents).toEqual([
'/第一条并发命令',
'未知命令:/第一条并发命令。输入 /help 查看可用命令。',
'/第二条并发命令',
'未知命令:/第二条并发命令。输入 /help 查看可用命令。',
]);
});
});
it('opens a specific agent conversation and persists messages to that agent', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const agentMessages: Array<{
schemaVersion: string;
role: 'user' | 'assistant';
content: string;
agentId: string | null;
updatedAt: number;
}> = [];
let agentConversationReadCount = 0;
let agentMemoryContent = '# 策划 Director 私有记忆\n- 保留轻量像素风\n';
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
if (args?.agentId) {
agentConversationReadCount += 1;
}
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: [...agentMessages],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: agentMemoryContent,
exists: true,
};
}
if (command === 'write_local_agent_memory') {
agentMemoryContent = String(args?.content ?? '');
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: agentMemoryContent,
exists: true,
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: '/tmp/authorized-game/.agent/run.latest.json',
content: JSON.stringify({
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-agent-dialog-evidence',
commandId: 'game.generate_draft',
status: 'running',
lifecycleStatus: 'pending',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'running',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner',
steps: [
{
pass: 1,
agent: 'Planner',
phase: 'plan',
taskId: 'design-director',
group: 'design',
role: 'Director',
status: 'running',
inputPaths: ['memory/session.md'],
outputPaths: ['.agent/spec.md'],
summary: '正在拆解创作方向',
toolCalls: [
{
toolId: 'llm.planner',
status: 'ok',
inputPaths: ['memory/session.md'],
outputPaths: ['.agent/spec.md'],
summary: 'Planner 已读取短期记忆',
},
{
toolId: 'agent.tool.suggest.canvas.project_sync',
status: 'suggested',
inputPaths: ['assets/manifest.art.json'],
outputPaths: [],
summary: '建议用户确认 /sync-canvas-project <画板项目ID>',
},
],
},
],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: ['design-director'],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'continue',
error: null,
updatedAt: 1,
} satisfies GameCreationAgentRunTrace),
};
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: [
{
path: '.agent/runs/run-agent-dialog-evidence.json',
kind: 'file',
size: 1,
},
],
};
}
if (command === 'chat_with_game_creator_role_agent') {
return {
replyText: mockRoleAgentReply(),
};
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
role: 'user' | 'assistant';
content: string;
agentId: string | null;
};
if (args?.agentId) {
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: message.role,
content: message.content,
agentId: String(args.agentId),
updatedAt: agentMessages.length + 1,
});
}
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: args?.agentId ? [...agentMessages] : [],
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
expect(await screen.findByText('正在拆解创作方向')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const agentDialog = await screen.findByLabelText('Agent 对话');
expect(agentDialog).not.toBeNull();
expect(agentDialog.textContent).toContain('正在拆解创作方向');
expect(agentDialog.textContent).toContain('pass 1 · plan');
expect(agentDialog.textContent).toContain('run: pending');
expect(agentDialog.textContent).toContain('编排:本轮 active');
expect(agentDialog.textContent).toContain(
'已读取 0 条:/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
);
const input = screen.getByLabelText('Agent 对话内容');
fireEvent.change(input, { target: { value: '优先保留轻量像素风' } });
fireEvent.submit(input.closest('form') as HTMLFormElement);
expect(
await screen.findByText(
'已保存 2 条:/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
),
).not.toBeNull();
expect(screen.getByText(roleAgentMockReply)).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
});
expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain(
'保留轻量像素风',
);
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'in: memory/session.md',
);
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'out: .agent/spec.md',
);
expect(screen.getByLabelText('Agent 最近证据').textContent).toContain(
'tool: llm.planner · ok · Planner 已读取短期记忆',
);
expect(
screen.getByRole('button', { name: '填入读取 memory/session.md' }),
).not.toBeNull();
expect(screen.getByRole('button', { name: '填入同步命令' })).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_agent_memory', {
projectPath: '/tmp/authorized-game',
taskId: 'design-director',
});
expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
message: {
role: 'user',
content: '优先保留轻量像素风',
agentId: null,
},
});
expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
message: {
role: 'assistant',
content: roleAgentMockReply,
agentId: null,
},
});
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
prompt: '优先保留轻量像素风',
});
fireEvent.change(screen.getByLabelText('Agent 对话内容'), {
target: { value: '稳定结论:锅铲音效要跟随连击节奏' },
});
fireEvent.click(
within(agentDialog).getByRole('button', { name: '记入记忆' }),
);
expect(
await screen.findByText('已写入 拆解创作方向 私有记忆。'),
).not.toBeNull();
expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain(
'锅铲音效要跟随连击节奏',
);
expect(invoke).toHaveBeenCalledWith('write_local_agent_memory', {
projectPath: '/tmp/authorized-game',
taskId: 'design-director',
content:
'# 策划 Director 私有记忆\n- 保留轻量像素风\n- 稳定结论:锅铲音效要跟随连击节奏\n',
});
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '刷新后外部记录',
agentId: 'design-director',
updatedAt: 2,
});
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content:
'后台任务失败:kind=codex-app-server-context-window-exceeded ' +
`fingerprint=${'c'.repeat(64)} chars=2048`,
agentId: 'design-director',
updatedAt: 3,
});
fireEvent.click(within(agentDialog).getByRole('button', { name: '刷新' }));
expect(await screen.findByText('刷新后外部记录')).not.toBeNull();
expect(
await screen.findByText(
'策划 Agent 模型上下文已超限,请缩小任务范围后重试',
),
).not.toBeNull();
expect(agentDialog.textContent).not.toContain('fingerprint');
expect(agentDialog.textContent).not.toContain('chars=');
expect(agentConversationReadCount).toBeGreaterThanOrEqual(2);
fireEvent.click(screen.getByRole('button', { name: '填入同步命令' }));
expect(screen.queryByLabelText('Agent 对话')).toBeNull();
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/sync-canvas-project ',
);
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const reopenedAgentDialog = await screen.findByLabelText('Agent 对话');
fireEvent.click(
within(reopenedAgentDialog).getByRole('button', {
name: '填入读取 .agent/spec.md',
}),
);
expect(screen.queryByLabelText('Agent 对话')).toBeNull();
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
'/read .agent/spec.md',
);
expect(invoke).not.toHaveBeenCalledWith(
'sync_canvas_project_assets',
expect.anything(),
);
});
it('answers Needs input from the selected project Agent dialog', async () => {
const harness = createProjectSupervisorRuntimeHarness();
const agentId = 'design-director';
const sessionId = 'selected-agent-session';
const runId = 'selected-agent-needs-input-run';
const request = agentRuntimeUserInputRequest({
agentId,
sessionId,
runId,
requestId: 'selected-agent-request',
actionId: 'selected-agent-action',
});
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: 7000,
};
const runtimeResult = () => ({
state: currentState,
sessionPath: `${harness.projectPath}/.agent/runtime/agents/${agentId}.json`,
eventPath: `${harness.projectPath}/.agent/runtime/events/${agentId}.jsonl`,
taskPath: `${harness.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 === 'read_local_conversation' &&
args?.agentId === agentId
) {
return {
path: `${harness.projectPath}/.agent/conversations/agents/${agentId}/sessions/${sessionId}.jsonl`,
agentId,
sessionId,
messages: [...messages],
};
}
if (
command === 'read_game_creator_agent_runtime' &&
args?.agentId === agentId
) {
return runtimeResult();
}
if (command === 'read_local_agent_memory') {
return {
taskId: agentId,
path: `${harness.projectPath}/memory/agents/design/director.md`,
content: '',
exists: false,
};
}
if (
command === 'answer_game_creator_agent_runtime_user_input' &&
args?.agentId === agentId
) {
messages = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: String(
(args.answers as Record<string, string>).visual_direction,
),
agentId,
messageId: 'selected-agent-answer',
updatedAt: 7100,
},
];
currentState = {
...currentState,
status: 'running',
phase: 'planning',
currentAction: '根据用户回答继续规划',
updatedAt: 7200,
};
return runtimeResult();
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const dialog = await screen.findByLabelText('Agent 对话');
const card = await within(dialog).findByLabelText('Needs input');
expect(
(within(dialog).getByLabelText('Agent 对话内容') as HTMLInputElement)
.disabled,
).toBe(true);
expect(
(
within(dialog).getByRole('button', {
name: '后台运行',
}) as HTMLButtonElement
).disabled,
).toBe(true);
fireEvent.click(within(card).getByRole('button', { name: /手绘风/ }));
fireEvent.click(within(card).getByRole('button', { name: '提交回答' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'answer_game_creator_agent_runtime_user_input',
{
projectPath: harness.projectPath,
agentId,
runId,
actionId: request.actionId,
requestId: request.requestId,
responseId: expect.stringMatching(/^app-user-input-/),
answers: { visual_direction: '手绘风' },
},
);
});
expect(await within(dialog).findByText('手绘风')).not.toBeNull();
expect(within(dialog).queryByLabelText('Needs input')).toBeNull();
});
it('steers the same project Agent run by default and preserves explicit queueing', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const sessionId = 'project-agent-session-design';
const runId = 'project-agent-run-design';
const agentMessages: 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: 'action',
currentTask: '完善玩法需求',
currentAction: '读取项目上下文',
plan: ['读取上下文', '更新方案'],
observations: [],
taskQueue: {
total: 1,
pending: 0,
running: 1,
completed: 0,
failed: 0,
latestRunId: runId,
updatedAt: 7000,
},
allowedTools: ['conversation.read', 'conversation.write'],
lastResponse: null,
error: null,
updatedAt: 7000,
};
const runtimeResult = {
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: [
{
schemaVersion: 'game-creator-agent-runtime-task.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId,
runId,
source: 'agent-background-task',
task: '完善玩法需求',
status: 'running',
phase: 'action',
currentAction: '读取项目上下文',
error: null,
updatedAt: 7000,
},
],
};
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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [runtimeResult];
}
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 === 'read_local_conversation') {
if (args?.agentId === 'design-director') {
return {
path: `/tmp/authorized-game/.agent/conversations/agents/design-director/sessions/${sessionId}.jsonl`,
agentId: 'design-director',
sessionId,
messages: [...agentMessages],
};
}
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (command === 'read_game_creator_agent_runtime') {
return runtimeResult;
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
if (command === 'steer_game_creator_agent_runtime_task') {
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: String(args?.instruction ?? ''),
agentId: null,
updatedAt: 7001,
});
return {
runtime: runtimeResult,
steerId: String(args?.steerId),
sequence: 1,
status: 'queued',
providerInterrupted: true,
};
}
if (command === 'start_game_creator_agent_runtime_task') {
const queuedRunId = String(args?.runId);
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: String(args?.task ?? ''),
agentId: null,
updatedAt: 7002,
});
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: '等待当前后台任务完成',
},
],
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('已打开:authorized-game');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const dialog = await screen.findByLabelText('Agent 对话');
expect(
await within(dialog).findByLabelText('项目 Agent 后台任务提交方式'),
).toHaveProperty('value', 'steer');
const input = within(dialog).getByLabelText('Agent 对话内容');
fireEvent.change(input, {
target: { value: '先停止扩展范围,只修主循环' },
});
fireEvent.click(within(dialog).getByRole('button', { name: '追加指令' }));
expect(
await within(dialog).findByText(
`LLM 已判定需要改向,旧 Provider 已安全中断:${runId}`,
),
).not.toBeNull();
const steerCall = invoke.mock.calls.find(
([command]) => command === 'steer_game_creator_agent_runtime_task',
);
expect(steerCall?.[1]).toEqual({
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
sessionId,
runId,
steerId: expect.stringMatching(/^agent-steer-/),
instruction: '先停止扩展范围,只修主循环',
});
expect(invoke).not.toHaveBeenCalledWith(
'start_game_creator_agent_runtime_task',
expect.anything(),
);
expect(input).toHaveProperty('value', '');
fireEvent.change(
within(dialog).getByLabelText('项目 Agent 后台任务提交方式'),
{ target: { value: 'queue' } },
);
fireEvent.change(input, { target: { value: '下一轮再补素材验收' } });
fireEvent.click(within(dialog).getByRole('button', { name: '排队任务' }));
expect(
await within(dialog).findByText(/已加入后台队列:agent-background-task-/),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_agent_runtime_task',
expect.objectContaining({
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
sessionId,
task: '下一轮再补素材验收',
runId: expect.stringMatching(/^agent-background-task-/),
}),
);
});
it('requires confirmation before reading a specific agent conversation when policy asks for it', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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: ['conversation.read'],
},
};
}
if (command === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
expect(await screen.findByText('准备读取 Agent 对话。')).not.toBeNull();
expect(screen.getByText('conversation.read')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
});
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
'已读取 0 条:/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
});
});
it('leaves the agent conversation panel usable after cancelling read confirmation', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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: ['conversation.read'],
},
};
}
if (command === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: [],
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const conversationReadCommand =
await screen.findByText('conversation.read');
fireEvent.click(
within(
conversationReadCommand.closest('.pending-command') as HTMLElement,
).getByRole('button', { name: '取消' }),
);
await waitFor(() => {
expect(screen.queryByText('conversation.read')).toBeNull();
});
expect(screen.getAllByText('已取消读取 Agent 对话').length).toBeGreaterThan(
0,
);
expect(screen.getByText('暂无对话')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_conversation', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
});
});
it('keeps loaded agent conversation after cancelling private memory read confirmation', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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: ['memory.read'],
},
};
}
if (command === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: args?.agentId
? [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '已读到 Agent 对话',
agentId: 'design-director',
updatedAt: 1,
},
]
: [],
};
}
if (command === 'read_local_agent_memory') {
throw new Error('should wait for memory confirmation');
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
expect(await screen.findByText('已读到 Agent 对话')).not.toBeNull();
const memoryReadCommand = await screen.findByText('memory.read');
fireEvent.click(
within(
memoryReadCommand.closest('.pending-command') as HTMLElement,
).getByRole('button', { name: '取消' }),
);
await waitFor(() => {
expect(screen.queryByText('memory.read')).toBeNull();
});
expect(screen.getByText('已读到 Agent 对话')).not.toBeNull();
expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain(
'已取消读取 Agent 私有记忆',
);
expect(invoke).not.toHaveBeenCalledWith('read_local_agent_memory', {
projectPath: '/tmp/authorized-game',
taskId: 'design-director',
});
});
it('requires confirmation before writing a specific agent conversation when policy asks for it', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const agentMessages: Array<{
schemaVersion: string;
role: 'user' | 'assistant';
content: string;
agentId: string | null;
updatedAt: number;
}> = [];
const invoke = vi.fn(
async (command: string, args?: Record<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: ['conversation.write'],
},
};
}
if (command === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: args?.agentId ? agentMessages : [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'chat_with_game_creator_role_agent') {
return {
replyText: mockRoleAgentReply(),
};
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
role: 'user' | 'assistant';
content: string;
agentId: string | null;
};
if (args?.agentId) {
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: message.role,
content: message.content,
agentId: message.agentId,
updatedAt: agentMessages.length + 1,
});
}
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: args?.agentId ? agentMessages : [],
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('已打开:authorized-game');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const input = await screen.findByLabelText('Agent 对话内容');
invoke.mockClear();
fireEvent.change(input, { target: { value: '先记住这个方向' } });
fireEvent.submit(input.closest('form') as HTMLFormElement);
expect(await screen.findByText('准备保存 Agent 对话。')).not.toBeNull();
expect(screen.getByText('conversation.write')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'append_local_conversation_message',
expect.objectContaining({ agentId: 'design-director' }),
);
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText(/已保存 2 条/)).not.toBeNull();
expect(screen.getByText('先记住这个方向')).not.toBeNull();
expect(screen.getByText(roleAgentMockReply)).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
message: {
role: 'user',
content: '先记住这个方向',
agentId: null,
},
});
expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
message: {
role: 'assistant',
content: roleAgentMockReply,
agentId: null,
},
});
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
prompt: '先记住这个方向',
});
});
it('shows agent conversation history in recent batches', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const agentMessages = Array.from({ length: 25 }, (_, index) => ({
schemaVersion: 'game-creator-conversation.v1',
role: index % 2 === 0 ? ('user' as const) : ('assistant' as const),
content: `Agent 历史 ${String(index + 1).padStart(2, '0')}`,
agentId: 'design-director',
updatedAt: index + 1,
}));
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: args?.agentId ? agentMessages : [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('已打开:authorized-game');
expect(screen.queryByText('/tmp/authorized-game')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const agentDialog = await screen.findByLabelText('Agent 对话');
expect(agentDialog.textContent).toContain('Agent 历史 06');
expect(agentDialog.textContent).toContain('Agent 历史 25');
expect(agentDialog.textContent).not.toContain('Agent 历史 05');
fireEvent.click(
screen.getByRole('button', {
name: '显示更早 · 还有 5 条对话',
}),
);
expect(agentDialog.textContent).toContain('Agent 历史 01');
expect(agentDialog.textContent).toContain('Agent 历史 05');
});
it('does not submit duplicate agent messages while a save is running', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const agentMessages: Array<{
schemaVersion: string;
role: 'user' | 'assistant';
content: string;
agentId: string | null;
updatedAt: number;
}> = [];
let releaseUserAppend: (() => void) | null = null;
let userAppendCount = 0;
const makeAgentConversationResult = () => ({
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: 'design-director',
messages: agentMessages,
});
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'chat_with_game_creator_role_agent') {
return {
replyText: mockRoleAgentReply(),
};
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
role: 'user' | 'assistant';
content: string;
agentId: string | null;
};
if (message.role === 'user') {
userAppendCount += 1;
return await new Promise((resolve) => {
releaseUserAppend = () => {
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: message.role,
content: message.content,
agentId: message.agentId,
updatedAt: agentMessages.length + 1,
});
resolve(makeAgentConversationResult());
};
});
}
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: message.role,
content: message.content,
agentId: message.agentId,
updatedAt: agentMessages.length + 1,
});
return makeAgentConversationResult();
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(
await screen.findByRole('button', { name: /拆解创作方向/ }),
);
const input = await screen.findByLabelText('Agent 对话内容');
const form = input.closest('form') as HTMLFormElement;
fireEvent.change(input, { target: { value: '只保存一次' } });
fireEvent.submit(form);
await screen.findByText('正在保存用户消息');
fireEvent.submit(form);
expect((form.querySelector('button') as HTMLButtonElement).disabled).toBe(
true,
);
expect(userAppendCount).toBe(1);
await act(async () => {
releaseUserAppend?.();
});
expect(await screen.findByText(/已保存 2 条/)).not.toBeNull();
expect(userAppendCount).toBe(1);
});
it('does not show unsaved agent messages when persistence fails', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'append_local_conversation_message') {
throw new Error('保存 Agent 对话失败');
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const input = await screen.findByLabelText('Agent 对话内容');
fireEvent.change(input, { target: { value: '这条不应该显示成已保存' } });
fireEvent.submit(input.closest('form') as HTMLFormElement);
expect(await screen.findByText('保存 Agent 对话失败')).not.toBeNull();
expect(screen.getByText('暂无对话')).not.toBeNull();
expect(screen.queryByText('这条不应该显示成已保存')).toBeNull();
expect(screen.getByLabelText('Agent 对话内容')).toHaveProperty(
'value',
'这条不应该显示成已保存',
);
});
it('keeps the saved user message visible when the local agent receipt fails', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const agentMessages: Array<{
schemaVersion: string;
role: 'user' | 'assistant';
content: string;
agentId: string | null;
updatedAt: number;
}> = [];
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: agentMessages,
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'chat_with_game_creator_role_agent') {
throw new Error('Agent LLM 未配置');
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
role: 'user' | 'assistant';
content: string;
agentId: string | null;
};
agentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: message.role,
content: message.content,
agentId: message.agentId,
updatedAt: 1,
});
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: agentMessages,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const input = await screen.findByLabelText('Agent 对话内容');
fireEvent.change(input, { target: { value: '先保留这个方向' } });
fireEvent.submit(input.closest('form') as HTMLFormElement);
expect(
(
await screen.findAllByText(
/已保存用户消息;Agent 回复失败:Agent LLM 未配置/,
)
).length,
).toBeGreaterThan(0);
expect(screen.getByText('先保留这个方向')).not.toBeNull();
expect(screen.queryByText(roleAgentMockReply)).toBeNull();
expect(screen.getByLabelText('Agent 对话内容')).toHaveProperty('value', '');
});
it('shows LLM configuration gaps in the project agent conversation before sending', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
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: args?.agentId
? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl'
: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: args?.agentId ?? null,
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const agentDialog = await screen.findByLabelText('Agent 对话');
const warning = await within(agentDialog).findByRole('status');
expect(warning.textContent).toContain(
'当前 Agent LLM 未就绪:LLM 未配置:请在 agentLlm.design-director.apiKey 中设置 API Key',
);
expect(within(agentDialog).getByLabelText('Agent 对话内容')).toHaveProperty(
'disabled',
true,
);
expect(
within(agentDialog).getByRole('button', { name: '发送' }),
).toHaveProperty('disabled', true);
expect(invoke).not.toHaveBeenCalledWith(
'chat_with_game_creator_role_agent',
expect.anything(),
);
});
it('reports Tauri availability when saving an agent conversation without invoke', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: [],
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const input = await screen.findByLabelText('Agent 对话内容');
fireEvent.change(input, { target: { value: '确认运行环境提示' } });
delete window.__TAURI__;
fireEvent.submit(input.closest('form') as HTMLFormElement);
expect(await screen.findByText('需要在 Tauri App 内运行')).not.toBeNull();
expect(screen.queryByText('请先初始化本地项目')).toBeNull();
});
it('falls back to a saved normal reply when the agent stream listener rejects', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const savedAgentMessages: Array<{
schemaVersion: string;
role: 'user' | 'assistant';
content: string;
agentId: string | null;
updatedAt: number;
}> = [];
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: savedAgentMessages,
};
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: '/tmp/authorized-game/memory/agents/design/director.md',
content: '',
exists: false,
};
}
if (command === 'chat_with_game_creator_role_agent') {
return {
replyText: mockRoleAgentReply(),
};
}
if (command === 'append_local_conversation_message') {
const message = args?.message as {
role: 'user' | 'assistant';
content: string;
agentId: string | null;
};
savedAgentMessages.push({
schemaVersion: 'game-creator-conversation.v1',
role: message.role,
content: message.content,
agentId: message.agentId,
updatedAt: 1,
});
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: savedAgentMessages,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
const listen = vi.fn(async (eventName: string) => {
if (
eventName === 'game-creator-agent-progress' ||
eventName === 'game-creator-agent-runtime-update'
) {
return vi.fn();
}
if (eventName === 'game-creator-role-agent-chat-stream') {
throw new Error('core:event:allow-listen denied');
}
throw new Error(`unexpected listen ${eventName}`);
});
window.__TAURI__ = { core: { invoke }, event: { listen } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const input = await screen.findByLabelText('Agent 对话内容');
fireEvent.change(input, { target: { value: '先记住这个方向' } });
fireEvent.submit(input.closest('form') as HTMLFormElement);
expect(await screen.findByText(/已保存 2 条/)).not.toBeNull();
expect(screen.getByText('先记住这个方向')).not.toBeNull();
expect(screen.getByText(roleAgentMockReply)).not.toBeNull();
expect(screen.getByLabelText('Agent 对话内容')).toHaveProperty('value', '');
expect(listen).toHaveBeenCalledWith(
'game-creator-role-agent-chat-stream',
expect.any(Function),
);
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', {
projectPath: '/tmp/authorized-game',
agentId: 'design-director',
prompt: '先记住这个方向',
});
});
it('clears stale agent messages when another agent conversation fails to load', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const invoke = vi.fn(
async (command: string, args?: Record<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 === 'resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_local_conversation') {
if (args?.agentId === null) {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [],
};
}
if (args?.agentId === 'design-director') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: 'design-director',
messages: [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '旧 Agent 历史消息',
agentId: 'design-director',
updatedAt: 1,
},
],
};
}
throw new Error('读取 Agent 对话失败');
}
if (command === 'read_local_agent_memory') {
return {
taskId: args?.taskId,
path: `/tmp/authorized-game/memory/agents/${String(
args?.taskId ?? '',
)}.md`,
content: '',
exists: false,
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
await screen.findByText('想做什么游戏?');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
expect(await screen.findByText('旧 Agent 历史消息')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '关闭' }));
fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ }));
expect(await screen.findByText('读取 Agent 对话失败')).not.toBeNull();
expect(screen.getByText('暂无对话')).not.toBeNull();
expect(screen.queryByText('旧 Agent 历史消息')).toBeNull();
});
it('asks for explicit confirmation before recovering runtime tasks under the default policy', 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 === 'resume_game_creator_agent_runtime_tasks') {
throw new Error('项目权限策略要求用户确认:agent.resume');
}
if (command === 'confirm_resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
const detail = await screen.findByText(
'恢复 /tmp/authorized-game 中未完成的 Agent Runtime 任务',
);
expect(screen.getByText(/run:/).textContent).toContain(
'等待确认恢复 Agent Runtime 任务',
);
const confirmation = detail.closest('.pending-command');
expect(confirmation).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.mock.calls.filter(
([command]) =>
command === 'confirm_resume_game_creator_agent_runtime_tasks',
),
).toHaveLength(1);
expect(
screen.queryByText(
'恢复 /tmp/authorized-game 中未完成的 Agent Runtime 任务',
),
).toBeNull();
expect(screen.getByText(/run:/).textContent).toContain(
'已确认恢复 Agent Runtime 任务',
);
});
});
it('cancels a stale runtime recovery confirmation when the project changes', 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 === 'resume_game_creator_agent_runtime_tasks') {
throw new Error('项目权限策略要求用户确认:agent.resume');
}
if (command === 'confirm_resume_game_creator_agent_runtime_tasks') {
return [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'read_local_conversation') {
return {
path: `${String(args?.projectPath ?? '')}/.agent/conversations/project.jsonl`,
agentId: null,
messages: [],
};
}
if (command === 'read_local_project_file') {
throw new Error('run trace 不存在');
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?dev');
fireEvent.change(screen.getByLabelText('本地项目目录'), {
target: { value: '/tmp/project-a' },
});
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
const createA = screen
.getByText('创建 /tmp/project-a')
.closest('.pending-command');
fireEvent.click(
within(createA as HTMLElement).getByRole('button', { name: '确认' }),
);
expect(
await screen.findByText(
'恢复 /tmp/project-a 中未完成的 Agent Runtime 任务',
),
).not.toBeNull();
fireEvent.change(screen.getByLabelText('本地项目目录'), {
target: { value: '/tmp/project-b' },
});
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
const createB = screen
.getByText('创建 /tmp/project-b')
.closest('.pending-command');
fireEvent.click(
within(createB as HTMLElement).getByRole('button', { name: '确认' }),
);
expect(
await screen.findByText(
'恢复 /tmp/project-b 中未完成的 Agent Runtime 任务',
),
).not.toBeNull();
expect(
screen.queryByText('恢复 /tmp/project-a 中未完成的 Agent Runtime 任务'),
).toBeNull();
expect(
invoke.mock.calls.filter(
([command]) =>
command === 'confirm_resume_game_creator_agent_runtime_tasks',
),
).toHaveLength(0);
expect(invoke).toHaveBeenCalledWith(
'append_local_permission_log',
expect.objectContaining({
projectPath: '/tmp/project-a',
event: 'permission.cancel',
commandId: 'agent.resume',
}),
);
});
it('ignores delayed runtime recovery results from a previously opened project', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let resolveProjectAResume: ((value: unknown[]) => void) | null = null;
const projectAResume = new Promise<unknown[]>((resolve) => {
resolveProjectAResume = resolve;
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
const targetProjectPath = String(args?.projectPath ?? '');
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
return {
projectPath: targetProjectPath,
manifestPath: `${targetProjectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'resume_game_creator_agent_runtime_tasks') {
return targetProjectPath === '/tmp/project-a' ? projectAResume : [];
}
if (command === 'read_game_creator_agent_runtimes') {
return [];
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'read_local_conversation') {
return {
path: `${targetProjectPath}/.agent/conversations/project.jsonl`,
agentId: null,
messages: [],
};
}
if (command === 'read_local_project_file') {
throw new Error('run trace 不存在');
}
if (command === 'list_local_project_files') {
return { projectPath: targetProjectPath, files: [] };
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/?dev');
fireEvent.change(screen.getByLabelText('本地项目目录'), {
target: { value: '/tmp/project-a' },
});
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
const createA = screen
.getByText('创建 /tmp/project-a')
.closest('.pending-command');
fireEvent.click(
within(createA as HTMLElement).getByRole('button', { name: '确认' }),
);
expect(await screen.findByText('已打开:project-a')).not.toBeNull();
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'resume_game_creator_agent_runtime_tasks',
{ projectPath: '/tmp/project-a' },
);
});
fireEvent.change(screen.getByLabelText('本地项目目录'), {
target: { value: '/tmp/project-b' },
});
fireEvent.click(screen.getByRole('button', { name: '初始化' }));
const createB = screen
.getByText('创建 /tmp/project-b')
.closest('.pending-command');
fireEvent.click(
within(createB as HTMLElement).getByRole('button', { name: '确认' }),
);
expect(await screen.findByText('已打开:project-b')).not.toBeNull();
await act(async () => {
resolveProjectAResume?.([]);
await projectAResume;
});
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_game_creator_agent_runtimes', {
projectPath: '/tmp/project-b',
});
});
expect(invoke).not.toHaveBeenCalledWith(
'read_game_creator_agent_runtimes',
{ projectPath: '/tmp/project-a' },
);
expect(screen.getByText('已打开:project-b')).not.toBeNull();
});
}