794f3d4cf8
解析 Codex app-server 稳定失败分类并生成安全可行动摘要 统一 Runtime 事件、任务、阶段记录和各 Agent 状态面的失败展示 限制自主构建最终回复 fallback,避免鉴权与网络错误被提交为成功 补充失败分类、防泄漏、待核对和 fallback 回归测试 同步技术方案与共享决策记录
3179 lines
111 KiB
TypeScript
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();
|
|
});
|
|
}
|