Files
Genarrative/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts
k88936 85d69a29a9 前端:认不出的拒单也在聊天里补一条同级提示
- 新增 `directTurnUnrecognizedRejectionNoticeText`:宿主 / 环境事实的拒单在聊天里的文案取宿主收口文案的脱敏摘要与建议(`projectRuntimeVisibleRejectionError`),不是收口形状时只给一句通用兜底,机器字段不进聊天
- 控制器在「认不出的拒单」分支补写一条与用户消息同级的提示(沿用 `directTurnRejectionNoticeMessageId` 身份):拒单不产生 `turn.completed`,这条乐观用户气泡后面不会再有事件来解释它;上报与横幅照旧保留
- 修正该处注释「聊天里的失败说明不由这里写」——那条只对回合失败成立,拒单没有终态出口;同时把非结构化错误继续只走横幅的理由写清楚
- 用例:`chat-composer.suite.ts` 补一条结构化拒单的界面用例(同级提示可见、`direct-codex-failure` / `stage=` 不进聊天、忙碌态放掉);`project-conversation.suite.ts` 补该文案函数的单元断言
2026-09-24 16:21:35 +08:00

886 lines
33 KiB
TypeScript

import { directCodexUserItemFromContent } from '../../src/features/project-workspace/resourceReferences';
import {
directCodexPolicyRetryInput,
directTurnRejectionNotice,
directTurnUnrecognizedRejectionNoticeText,
readDirectTurnRejection,
} from '../../src/view/project-development/chat/conversation/directCodexConversation';
import {
createGameCreationAppManifest,
emptyProjectPolicy,
expect,
fireEvent,
it,
mockRoleAgentReply,
renderAppAt,
roleAgentMockReply,
screen,
vi,
waitFor,
within,
} from './harness';
export function registerProjectConversationTests() {
it('splits structured rejections by variant and never by copy', () => {
// 拒单是**结构化**载荷:分流只看 `error.type`,文案不参与任何判断。
const concurrent = {
error: {
type: 'turnAlreadyRunning' as const,
existingInvocationId: 'turn-1',
incomingInvocationId: 'turn-1',
},
message: '同一轮消息仍在处理中',
};
expect(readDirectTurnRejection(concurrent)?.error.type).toBe(
'turnAlreadyRunning',
);
expect(directTurnRejectionNotice(concurrent)).toBe('同一轮消息仍在处理中');
// 认得的参数 / 前置条件类都给同级提示。
expect(
directTurnRejectionNotice({
error: { type: 'contentEmpty' },
message: '聊天内容不能为空',
}),
).toBe('聊天内容不能为空');
// 宿主 / 环境事实不在这里认领:它们必须走上报通道(原样抛出)。
expect(
directTurnRejectionNotice({
error: { type: 'environmentNotReady', detail: '连不上 app-server' },
message: '环境未就绪',
}),
).toBeNull();
expect(
directTurnRejectionNotice({
error: { type: 'hostStateUnavailable', detail: '账本损坏' },
message: '宿主状态取不到',
}),
).toBeNull();
// 认不出的拒单在聊天里也要有一条同级提示:它们的 `message` 是宿主的收口文案(带 stage= /
// code= 这类机器字段),进聊天前先取脱敏摘要与建议,机器字段不进聊天。
expect(
directTurnUnrecognizedRejectionNoticeText({
error: { type: 'environmentNotReady', detail: '连不上 app-server' },
message:
'direct-codex-failure:v2 stage=code-generation code=runtime-failure retryable=false summary=Codex app-server 启动失败:找不到可执行文件;建议:请重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
}),
).toBe(
'陶泥儿智能创作:Codex app-server 启动失败:找不到可执行文件。请重试;如持续失败请检查项目诊断',
);
// 不是收口形状时也只给一句可读的话,绝不回落带机器字段的原文。
expect(
directTurnUnrecognizedRejectionNoticeText({
error: { type: 'hostStateUnavailable', detail: '账本损坏' },
message: '宿主状态取不到:exitStatus=signal: 9 (SIGKILL)',
}),
).toBe('陶泥儿智能创作 执行失败,请稍后重试');
// 不是这份结构(旧字符串、Error、裸对象)一律不认。
expect(
readDirectTurnRejection('codex-app-server-error:unauthorized'),
).toBeNull();
expect(readDirectTurnRejection(new Error('boom'))).toBeNull();
expect(readDirectTurnRejection({ error: {}, message: 'x' })).toBeNull();
expect(
readDirectTurnRejection({ error: { type: 'contentEmpty' } }),
).toBeNull();
});
it('carries the whole direct turn input, including @ references, into the policy-confirmation retry', () => {
// 确认 `conversation.write` 之后重跑的是同一轮输入:漏掉任何一项都会让用户
// 在确认之后拿到另一轮内容。历史缺陷正是漏了 canonical user item 里的 @ 引用
// (引用被静默丢掉),所以这里把「首轮入参整体带过去」钉成硬约束。
const firstTurn = {
clientTurnId: 'direct-turn-1',
creationType: 'game' as const,
userItem: directCodexUserItemFromContent(
[
{ type: 'input_text' as const, text: '用这张图改一下' },
{
type: 'agc_resource_reference' as const,
resourceId: 'reference-hero',
},
{
type: 'agc_attachment_reference' as const,
name: '角色草图.png',
mediaType: 'image/png',
size: 128,
localPath: '',
status: 'imported' as const,
},
],
'direct-turn-1:user',
),
};
expect(directCodexPolicyRetryInput(firstTurn)).toEqual({
...firstTurn,
directPolicyChecked: true,
});
// canonical content 是这一次输入的判别项,单独再断言一遍,避免上面整体相等
// 被未来字段扩展掩盖。
expect(directCodexPolicyRetryInput(firstTurn).userItem.content).toEqual(
firstTurn.userItem.content,
);
});
it.skip('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 === 'set_agc_plugin_project_path') return null;
if (command === 'list_agc_plugins') return [];
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');
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.skip('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 === 'set_agc_plugin_project_path') return null;
if (command === 'list_agc_plugins') return [];
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');
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.skip('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');
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.skip('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');
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.skip('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');
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.skip('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');
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
const agentDialog = await screen.findByLabelText('Agent 对话');
const warning = await within(agentDialog).findByRole('status');
expect(warning.textContent).toContain(
'当前 Agent 智能服务未就绪:官方智能服务暂不可用,请稍后重试',
);
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.skip('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');
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.skip('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');
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.skip('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');
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();
});
}