Files
suzmii 53afc85319 修正 AGC 测试按新的 Lexical 聊天输入区驱动与断言
- appSurface/harness 补齐三处 jsdom 缺口:ClipboardEvent、DragEvent、Range.prototype.getBoundingClientRect;否则 Lexical 的粘贴通路直接抛 ReferenceError / TypeError
- appSurface/harness 的 submitChat 改为 async:全选并让编辑器吸收选区、清空、走产品真实 paste 通路写入,再让出一帧让 React 追平 draft,最后点发送
- 新增 composerText / composerValue / composerDisabled / setComposerText 助手,按原生控件与 Lexical contenteditable 两种 DOM 口径读写输入区
- 15 个测试文件中 116 处 toHaveProperty('value', …) 断言等义改写为 await composerText() / await composerValue();1 处 placeholder 断言改查输入区占位文案;2 处 disabled 断言改用 composerDisabled(同时覆盖 data-disabled 与 contenteditable=false);9 处 fireEvent.change 写入改用 setComposerText
- 328 处 submitChat 调用补 await,9 个非 async 用例补 async
- Godot 输入区键盘语义用例按 Lexical 实际行为改写:Shift+Enter 与组合态 Enter 由编辑器消化并插入换行、不发送、草稿保留
- 未改动 src 下任何产品代码,未放宽或删除断言
2026-09-10 11:46:02 +08:00

1929 lines
63 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
composerText,
composerValue,
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
emptyProjectPolicy,
expect,
fireEvent,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
it,
renderAppAt,
screen,
setComposerText,
submitChat,
vi,
waitFor,
within,
} from './harness';
export function registerAgentRuntimeCommandTests() {
it('reads agent loop trace from chat through the authorized project path', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-chat-trace',
commandId: 'game.generate_draft',
status: 'needs-revision',
passes: 2,
maxPasses: 3,
toolCallCount: 18,
maxToolCalls: 128,
stopReason: 'evaluator-feedback',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'Planner -> Orchestrator -> Generator -> Evaluator',
steps: [
{
pass: 2,
agent: 'Orchestrator',
phase: 'plan',
taskId: 'code-director',
group: 'code',
role: 'Code',
status: 'completed',
inputPaths: ['.agent/findings.md'],
outputPaths: ['.agent/passes/pass-2/task-graph.json'],
summary: '按返工路由重跑程序和预览',
toolCalls: [
{
toolId: 'agent.task_graph.plan_pass',
status: 'completed',
inputPaths: ['.agent/findings.md'],
outputPaths: ['.agent/passes/pass-2/task-graph.json'],
summary: 'repair pass',
},
],
},
{
pass: 2,
agent: 'Generator',
phase: 'generate',
taskId: 'code-director',
group: 'code',
role: 'Code',
status: 'completed',
inputPaths: ['.agent/spec.md', '.agent/findings.md'],
outputPaths: ['.agent/passes/pass-2/draft.json'],
summary: '生成修复草案',
toolCalls: [],
},
],
artifacts: [
{
path: '.agent/passes/pass-2/task-graph.json',
sizeBytes: 256,
checksum: 'fnv1a64:chat-trace',
},
],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: ['code-director'],
activeTaskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
carriedTaskIds: ['design-director'],
repairFocus: ['缺少输入监听'],
repairRoutes: [
{
issue: '缺少输入监听',
taskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
reason: 'code-repair',
},
],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [
{
pass: 2,
mode: 'repair',
summary: '第 2 轮按 Evaluator 反馈返工',
activeTaskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
carriedTaskIds: ['design-director'],
dependencyWaves: [
['code-director'],
['code-prototype'],
['quality-review'],
['preview-readiness'],
],
repairFocus: ['缺少输入监听'],
repairRoutes: [
{
issue: '缺少输入监听',
taskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
reason: 'code-repair',
},
],
},
],
nextStep: 'repair-next-pass',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
await submitChat('/trace');
expect(await screen.findByText(/Runrun-chat-trace/)).not.toBeNull();
expect(
screen.getByText(/状态:needs-revision · 2\/3 轮 · evaluator-feedback/),
).not.toBeNull();
expect(
screen.getByText(
/active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/,
),
).not.toBeNull();
expect(screen.getByText(/返工路线:code-repair/)).not.toBeNull();
expect(screen.getByText(/编排轮次:/)).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
});
});
it('shows an empty agent trace message before the first run', 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_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/trace');
expect(
await screen.findByText(
'暂无最近 Agent trace。先生成一次游戏草案后再查看。',
),
).not.toBeNull();
expect(screen.getByLabelText('聊天').textContent).not.toContain(
'No such file or directory',
);
});
it('requires confirmation for agent trace reads when project policy asks for it', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-chat-trace-confirm',
commandId: 'game.generate_draft',
status: 'passed',
passes: 1,
maxPasses: 3,
toolCallCount: 3,
maxToolCalls: 128,
stopReason: 'evaluator-passed',
goal: '做一个厨房弹幕游戏',
coordination: 'Planner -> Generator',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个厨房弹幕游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: [],
},
passPlans: [],
nextStep: 'preview-playtest',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['agent.trace_read'],
},
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
await submitChat('/trace');
expect(
await screen.findByText('准备读取 Agent run trace。'),
).not.toBeNull();
expect(screen.getByText('agent.trace_read')).not.toBeNull();
expect(
screen.getByText('读取 /tmp/authorized-game 的最近 Agent run trace'),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'read_local_project_file',
expect.objectContaining({ commandId: 'agent.trace_read' }),
);
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(/Runrun-chat-trace-confirm/),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
});
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
projectPath: '/tmp/authorized-game',
event: 'permission.pending',
commandId: 'agent.trace_read',
});
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
projectPath: '/tmp/authorized-game',
event: 'permission.confirm',
commandId: 'agent.trace_read',
});
});
it('controls agent run lifecycle from chat through the authorized local project path', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-control-chat',
commandId: 'game.generate_draft',
status: 'pending',
lifecycleStatus: 'pending',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'retry-requested',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'rerun-now',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'control_agent_run') {
const action = String(args?.action ?? '');
const detail = String(args?.detail ?? '');
const resultByAction = {
status: {
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'rerun-now',
message: 'run run-control-chat 当前状态:pending / pending',
},
kill: {
status: 'killed',
lifecycleStatus: 'killed',
nextStep: 'resume-or-retry',
message: 'run run-control-chat 已标记为 killed',
},
retry: {
status: 'passed',
lifecycleStatus: 'done',
nextStep: 'preview-playtest',
message:
'run run-control-chat 已重试,已重新运行为 run-control-chat-nextgame/index.html',
},
resume: {
status: 'passed',
lifecycleStatus: 'done',
nextStep: 'preview-playtest',
message: `run run-control-chat 已恢复:${detail},已重新运行为 run-control-chat-nextgame/index.html`,
},
}[action];
if (!resultByAction) {
throw new Error(`unexpected agent run action ${action}`);
}
return {
runId: 'run-control-chat',
...resultByAction,
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath:
'/tmp/authorized-game/.agent/context.bundle.json',
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '输出' }));
expect(await composerText()).toBe('/read .agent/output.jsonl');
fireEvent.click(screen.getByRole('button', { name: '活动' }));
expect(await composerText()).toBe('/read .agent/activity.jsonl');
fireEvent.click(screen.getByRole('button', { name: '上下文包' }));
expect(await composerText()).toBe('/read .agent/context.bundle.json');
expect(invoke).not.toHaveBeenCalledWith(
'read_local_project_file',
expect.objectContaining({ relativePath: '.agent/context.bundle.json' }),
);
await submitChat('/agent-status');
expect(
await screen.findByText(
/run run-control-chat 当前状态:pending \/ pending/,
),
).not.toBeNull();
expect(screen.getByText(/runrun-control-chat/)).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '读取 Run 输出' }));
expect(await composerText()).toBe('/read .agent/output.jsonl');
expect(invoke).not.toHaveBeenCalledWith(
'read_local_project_file',
expect.objectContaining({ relativePath: '.agent/output.jsonl' }),
);
await setComposerText(screen.getByLabelText('创作想法'), '');
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'status',
detail: undefined,
});
expect(invoke).toHaveBeenCalledWith('append_local_permission_log', {
projectPath: '/tmp/authorized-game',
event: 'command.auto',
commandId: 'agent.run_status',
});
await submitChat('/agent-kill');
expect(screen.getByText('agent.kill')).not.toBeNull();
expect(
screen.getByText(
'标记 /tmp/authorized-game/.agent/run.latest.json 为 killed,并写入 activity/output',
),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(/run run-control-chat 已标记为 killed/),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'kill',
detail: undefined,
});
await submitChat('/agent-retry');
expect(screen.getByText('agent.retry')).not.toBeNull();
expect(
screen.getByText(
'使用 /tmp/authorized-game/.agent/run.latest.json 的目标重新运行一次',
),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
/run run-control-chat 已重试,已重新运行为 run-control-chat-next/,
),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'retry',
detail: undefined,
});
await submitChat('/agent-resume 继续修复输入监听');
expect(screen.getByText('agent.resume')).not.toBeNull();
expect(
screen.getByText(
'附加说明「继续修复输入监听」,继续运行 /tmp/authorized-game/.agent/run.latest.json 的目标',
),
).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
/run run-control-chat 已恢复:继续修复输入监听,已重新运行为 run-control-chat-next/,
),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'resume',
detail: '继续修复输入监听',
});
});
it('shows an empty agent run status message before the first run', 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 === 'control_agent_run') {
throw new Error(
'读取 Agent run trace 失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/agent-status');
expect(
await screen.findByText(
'暂无最近 Agent run。先生成一次游戏草案后再查看状态。',
),
).not.toBeNull();
expect(screen.getByLabelText('聊天').textContent).not.toContain(
'No such file or directory',
);
});
it('shows an empty agent run control message before the first run', 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 === 'control_agent_run') {
throw new Error(
'读取 Agent run trace 失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/agent-kill');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
'暂无可控制的 Agent run。先生成一次游戏草案后再操作。',
),
).not.toBeNull();
expect(screen.getByLabelText('聊天').textContent).not.toContain(
'No such file or directory',
);
});
it('blocks pending agent run control when project policy denies 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: ['agent.kill'],
confirmCommands: [],
},
};
}
if (command === 'control_agent_run') {
throw new Error('should not control agent run after deny');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/agent-kill');
expect(screen.getByText('agent.kill')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('项目权限策略拒绝执行:agent.kill'),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'control_agent_run',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'append_local_permission_log',
expect.objectContaining({
event: 'permission.confirm',
commandId: 'agent.kill',
}),
);
});
it('requires project policy confirmation before controlling agent run lifecycle', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-kill-confirm',
commandId: 'game.generate_draft',
status: 'killed',
lifecycleStatus: 'killed',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'killed',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'rerun-now',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['agent.kill'],
},
};
}
if (command === 'control_agent_run') {
return {
runId: 'run-kill-confirm',
status: 'killed',
lifecycleStatus: 'killed',
nextStep: 'rerun-now',
message: 'agent run killed',
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath:
'/tmp/authorized-game/.agent/context.bundle.json',
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
await submitChat('/agent-kill');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('准备执行 Agent run 操作。')).not.toBeNull();
expect(screen.getByText('agent.kill')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'control_agent_run',
expect.anything(),
);
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'kill',
detail: undefined,
});
});
});
it('requires project policy confirmation before reading agent run status from chat', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-status-confirm',
commandId: 'game.generate_draft',
status: 'pending',
lifecycleStatus: 'pending',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'running',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'rerun-now',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['agent.run_status'],
},
};
}
if (command === 'control_agent_run') {
return {
runId: 'run-status-confirm',
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'rerun-now',
message: 'run run-status-confirm 当前状态:pending / pending',
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath:
'/tmp/authorized-game/.agent/context.bundle.json',
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
await submitChat('/agent-status');
expect(await screen.findByText('准备查看 Agent run 状态。')).not.toBeNull();
expect(screen.getByText('agent.run_status')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'control_agent_run',
expect.anything(),
);
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
/run run-status-confirm 当前状态:pending \/ pending/,
),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'status',
detail: undefined,
});
});
it('cancels agent run status policy confirmation without reading status', 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: ['agent.run_status'],
},
};
}
if (command === 'control_agent_run') {
throw new Error('should wait for agent run status confirmation');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/agent-status');
const agentStatusCommand = await screen.findByText('agent.run_status');
fireEvent.click(
within(
agentStatusCommand.closest('.pending-command') as HTMLElement,
).getByRole('button', { name: '取消' }),
);
expect(
await screen.findByText('run: 已取消读取 Agent run 状态'),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'control_agent_run',
expect.anything(),
);
});
it('cancels pending agent run control without invoking native control', 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 === 'control_agent_run') {
throw new Error('should not control agent run after cancel');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/agent-kill');
const agentCommand = screen.getByText('agent.kill');
fireEvent.click(
within(agentCommand.closest('.pending-command') as HTMLElement).getByRole(
'button',
{ name: '取消' },
),
);
expect(
await screen.findByText('run: 已取消 Agent run 操作'),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'control_agent_run',
expect.anything(),
);
});
it('confirms before reading trace after agent run status from chat', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-status-trace-confirm',
commandId: 'game.generate_draft',
status: 'pending',
lifecycleStatus: 'pending',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'running',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'rerun-now',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['agent.trace_read'],
},
};
}
if (command === 'control_agent_run') {
return {
runId: 'run-status-trace-confirm',
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'rerun-now',
message: 'run run-status-trace-confirm 当前状态:pending / pending',
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath:
'/tmp/authorized-game/.agent/context.bundle.json',
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
await submitChat('/agent-status');
expect(
await screen.findByText(
/run run-status-trace-confirm 当前状态:pending \/ pending/,
),
).not.toBeNull();
expect(await screen.findByText('agent.trace_read')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'status',
detail: undefined,
});
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
});
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
});
});
});
it('controls agent run lifecycle from the agent status panel', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-panel-control',
commandId: 'game.generate_draft',
status: 'pending',
lifecycleStatus: 'pending',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'retry-requested',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'rerun-now',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'control_agent_run') {
const action = String(args?.action ?? '');
return {
runId: 'run-panel-control',
status: action === 'kill' ? 'killed' : 'pending',
lifecycleStatus: action === 'kill' ? 'killed' : 'pending',
nextStep: action === 'retry' ? 'preview-playtest' : 'rerun-now',
message: `panel ${action}`,
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath:
'/tmp/authorized-game/.agent/context.bundle.json',
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: '状态' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'status',
detail: undefined,
});
});
fireEvent.click(screen.getByRole('button', { name: '终止' }));
expect(screen.getByText('agent.kill')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'kill',
detail: undefined,
});
});
fireEvent.click(screen.getByRole('button', { name: '重试' }));
expect(screen.getByText('agent.retry')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'retry',
detail: undefined,
});
});
fireEvent.click(screen.getByRole('button', { name: '继续' }));
expect(screen.getByText('agent.resume')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'resume',
detail: undefined,
});
});
const composerInput = screen.getByLabelText('创作想法');
fireEvent.click(screen.getByRole('button', { name: '继续说明' }));
expect(await composerValue(composerInput)).toBe('/agent-resume ');
});
it('requires project policy confirmation before reading agent run status from the panel', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-panel-status-confirm',
commandId: 'game.generate_draft',
status: 'pending',
lifecycleStatus: 'pending',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'running',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'rerun-now',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['agent.run_status'],
},
};
}
if (command === 'control_agent_run') {
return {
runId: 'run-panel-status-confirm',
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'rerun-now',
message: 'panel status',
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath:
'/tmp/authorized-game/.agent/context.bundle.json',
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: '状态' }));
expect(await screen.findByText('agent.run_status')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'control_agent_run',
expect.anything(),
);
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'status',
detail: undefined,
});
});
});
it('confirms before reading trace after agent run status from the panel', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-panel-status-trace-confirm',
commandId: 'game.generate_draft',
status: 'pending',
lifecycleStatus: 'pending',
passes: 1,
maxPasses: 3,
toolCallCount: 1,
maxToolCalls: 128,
stopReason: 'running',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [],
artifacts: [],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: [],
activeTaskIds: [],
carriedTaskIds: [],
repairFocus: [],
repairRoutes: [],
tasks: createGameCreationAppSeedTasks(),
},
passPlans: [],
nextStep: 'rerun-now',
error: null,
updatedAt: 1,
};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_project_permission_policy') {
return {
path: '.agent/policy.json',
policy: {
deniedCommands: [],
confirmCommands: ['agent.trace_read'],
},
};
}
if (command === 'control_agent_run') {
return {
runId: 'run-panel-status-trace-confirm',
status: 'pending',
lifecycleStatus: 'pending',
nextStep: 'rerun-now',
message: 'panel status',
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
contextBundlePath:
'/tmp/authorized-game/.agent/context.bundle.json',
};
}
if (command === 'read_local_project_file') {
return {
path: '.agent/run.latest.json',
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
content: JSON.stringify(trace),
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
fireEvent.click(screen.getByRole('button', { name: '状态' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
projectPath: '/tmp/authorized-game',
action: 'status',
detail: undefined,
});
});
expect(await screen.findByText('agent.trace_read')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
});
fireEvent.click(screen.getByRole('button', { name: '确认' }));
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
});
});
});
it('manages long memory from chat through the authorized local project path', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let projectMemory = '# 项目长期记忆\n';
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_game_memory') {
return {
scope: 'long',
path: 'memory/project.md',
content: projectMemory,
exists: true,
};
}
if (command === 'write_local_game_memory') {
projectMemory = String(args?.content ?? '');
return {
scope: 'long',
path: 'memory/project.md',
content: projectMemory,
exists: true,
};
}
if (command === 'delete_local_game_memory') {
projectMemory = '';
return {
scope: 'long',
path: 'memory/project.md',
content: '',
exists: false,
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/memory');
expect(await screen.findByText(/长期记忆:/)).not.toBeNull();
await submitChat('/remember long 保留厨房主题');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已追加长期记忆。')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'long',
});
expect(invoke).toHaveBeenCalledWith('write_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'long',
content: '# 项目长期记忆\n- 保留厨房主题\n',
});
await submitChat('/memory-set long 覆盖后的长期记忆');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已保存长期记忆。')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('write_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'long',
content: '覆盖后的长期记忆',
});
await submitChat('/forget-memory');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已删除长期记忆。')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('delete_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'long',
});
});
it('requires project policy confirmation before reading memory from chat', 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_game_memory') {
return {
scope: args?.scope,
path: 'memory/project.md',
content: '# 项目长期记忆\n',
exists: true,
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
await submitChat('/memory');
expect(await screen.findByText('准备读取项目记忆。')).not.toBeNull();
expect(screen.getByText('memory.read')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'read_local_game_memory',
expect.anything(),
);
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText(/长期记忆:/)).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'long',
});
});
it('cancels project memory read confirmation from chat', 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_game_memory') {
throw new Error('should wait for confirmation');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
invoke.mockClear();
await submitChat('/memory');
expect(await screen.findByText('准备读取项目记忆。')).not.toBeNull();
const memoryReadCommand = screen.getByText('memory.read');
fireEvent.click(
within(
memoryReadCommand.closest('.pending-command') as HTMLElement,
).getByRole('button', { name: '取消' }),
);
await waitFor(() => {
expect(screen.queryByText('memory.read')).toBeNull();
});
expect(screen.getByText('已取消读取项目记忆。')).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'read_local_game_memory',
expect.anything(),
);
});
it('rejects unknown memory scopes before reading or deleting memory', 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 === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
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 === 'append_local_conversation_message') {
return {
path: '/tmp/authorized-game/.agent/conversations/project.jsonl',
agentId: null,
messages: [
{
schemaVersion: '1',
...(args?.message as Record<string, unknown>),
updatedAt: 1,
},
],
};
}
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('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
await submitChat('/memory typo');
expect(
await screen.findByText('格式:/memory [short|long|blackboard]'),
).not.toBeNull();
await submitChat('/forget-memory typo');
expect(
await screen.findByText('格式:/forget-memory [short|long|blackboard]'),
).not.toBeNull();
expect(invoke).not.toHaveBeenCalledWith(
'read_local_game_memory',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'delete_local_game_memory',
expect.anything(),
);
expect(screen.queryByText('memory.delete')).toBeNull();
expect(screen.queryByRole('button', { name: '确认' })).toBeNull();
});
it('manages blackboard memory from chat through the authorized local project path', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let blackboardMemory = '# 项目黑板\n';
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'init_local_game_project') {
const projectPath = String(args?.projectPath ?? '');
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'read_local_game_memory') {
return {
scope: args?.scope,
path: 'memory/blackboard.md',
content: blackboardMemory,
exists: true,
};
}
if (command === 'write_local_game_memory') {
blackboardMemory = String(args?.content ?? '');
return {
scope: args?.scope,
path: 'memory/blackboard.md',
content: blackboardMemory,
exists: true,
};
}
if (command === 'delete_local_game_memory') {
blackboardMemory = '';
return {
scope: args?.scope,
path: 'memory/blackboard.md',
content: '',
exists: false,
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await submitChat('/memory blackboard');
expect(await screen.findByText(/黑板记忆:/)).not.toBeNull();
await submitChat('/remember blackboard 共享美术约束');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已追加黑板记忆。')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('write_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'blackboard',
content: '# 项目黑板\n- 共享美术约束\n',
});
await submitChat('/memory-set 黑板 统一使用俯视角');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已保存黑板记忆。')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('write_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'blackboard',
content: '统一使用俯视角',
});
await submitChat('/forget-memory blackboard');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('已删除黑板记忆。')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('delete_local_game_memory', {
projectPath: '/tmp/authorized-game',
scope: 'blackboard',
});
});
}