53afc85319
- 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 下任何产品代码,未放宽或删除断言
1929 lines
63 KiB
TypeScript
1929 lines
63 KiB
TypeScript
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(/Run:run-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(/Run:run-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-next:game/index.html',
|
||
},
|
||
resume: {
|
||
status: 'passed',
|
||
lifecycleStatus: 'done',
|
||
nextStep: 'preview-playtest',
|
||
message: `run run-control-chat 已恢复:${detail},已重新运行为 run-control-chat-next:game/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(/run:run-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',
|
||
});
|
||
});
|
||
}
|