Files
Genarrative/apps/ai-game-creator-shell/tests/appSurface/supervisor-runtime.suite.ts
lhk229 8fcbd0be52
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Has been cancelled
收敛已退役界面契约测试
停用 PR375 后已移除的历史对话、附件入口和旧工具组流程断言。

保留当前界面仍使用的工具调用、时间戳和资源预览契约。
2026-09-16 06:55:47 +08:00

2053 lines
68 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 {
act,
agentRuntimeUserInputRequest,
cleanup,
composerDisabled,
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
createProjectSupervisorRuntimeHarness,
emptyProjectPolicy,
expect,
fireEvent,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
type GameCreationAgentRunTrace,
it,
openMainProject,
projectSupervisorResponseStream,
renderAppAt,
screen,
submitChat,
vi,
waitFor,
within,
} from './harness';
export function registerSupervisorRuntimeTests() {
it('routes ordinary chat to the active Project Supervisor Session without project double writes', async () => {
const harness = createProjectSupervisorRuntimeHarness();
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'list_game_creator_agent_sessions',
{
projectPath: harness.projectPath,
agentId: 'project-supervisor',
},
);
});
await submitChat('做一个反弹弹幕厨房游戏');
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
{
projectPath: harness.projectPath,
sessionId: harness.sessionId,
task: '做一个反弹弹幕厨房游戏',
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'autonomous-game-build',
source: 'project-supervisor-gui',
},
);
});
expect(
harness.invoke.mock.calls.some(
([command]) => command === 'chat_with_game_creator_agent',
),
).toBe(false);
expect(
harness.invoke.mock.calls.filter(
([command, args]) =>
command === 'append_local_conversation_message' &&
(args as Record<string, unknown>)?.agentId === null &&
((args as Record<string, unknown>)?.message as { content?: string })
?.content === '做一个反弹弹幕厨房游戏',
),
).toHaveLength(0);
await waitFor(() => {
expect(
(screen.getByRole('button', { name: '发送' }) as HTMLButtonElement)
.disabled,
).toBe(false);
});
expect(screen.getAllByText('做一个反弹弹幕厨房游戏')).toHaveLength(1);
});
it('keeps Project Supervisor plan progress compact on the ordinary chat surface', async () => {
const supervisorRunId = 'supervisor-plan-run';
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId: supervisorRunId,
status: 'running',
phase: 'waiting-for-delegate-receipts',
currentTask: '协调专业 Agent 完成首版方案',
currentAction: '内部控制台:轮询 durable delivery claim',
waitingOn: '专业 Agent 回执',
nextStep: '汇总专业 Agent 结果',
planRevision: 4,
planExplanation: '内部计划说明不应出现在普通聊天',
plan: ['确认目标', '并行委派专业 Agent', '汇总并回复'],
planSteps: [
{ step: '确认目标', status: 'completed' },
{ step: '并行委派专业 Agent', status: 'active' },
{ step: '汇总并回复', status: 'pending' },
],
activePlanStepIndex: 1,
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
expect(
await screen.findByText('项目总控 Agent · 等待专业 Agent'),
).not.toBeNull();
await waitFor(() => {
expect(harness.listen).toHaveBeenCalledWith(
'game-creator-agent-runtime-update',
expect.any(Function),
);
});
const delegatedRuntime = (agentId: string, delegationId: string) =>
harness.runtimeState({
agentId,
taskId: agentId,
sessionId: `session-${agentId}`,
runId: `run-${agentId}`,
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId,
status: 'running',
phase: 'action',
currentTask: `执行 ${agentId} 专业任务`,
currentAction: '执行专业工具动作',
waitingOn: '工具结果',
nextStep: '返回专业结论',
updatedAt: 5000,
});
await act(async () => {
const designRuntime = delegatedRuntime(
'design-director',
'delegation-design',
);
harness.emitAgentRuntime(designRuntime);
harness.emitAgentRuntime(designRuntime);
harness.emitAgentRuntime(
delegatedRuntime('art-asset-plan', 'delegation-art'),
);
harness.emitAgentRuntime({
...delegatedRuntime('dynamic-review-child', 'isolated-review'),
source: 'agent-isolated-child',
});
});
const compactProgress = await screen.findByLabelText('项目总控 Agent 进度');
expect(compactProgress.textContent).toBe(
'计划完成:1/3 · 当前步骤:并行委派专业 Agent · 等待:专业 Agent 回执 · 下一步:汇总专业 Agent 结果 · 专业 Agent 协作:2',
);
const supervisorPanel = screen.getByLabelText('项目总控 Agent 状态');
expect(supervisorPanel.textContent).not.toContain('计划修订号:4');
expect(supervisorPanel.textContent).not.toContain(
'内部计划说明不应出现在普通聊天',
);
expect(supervisorPanel.textContent).not.toContain(
'内部控制台:轮询 durable delivery claim',
);
expect(within(supervisorPanel).queryByText(/#2 active/)).toBeNull();
expect(
within(supervisorPanel).queryByLabelText('Agent 计划进度'),
).toBeNull();
});
it('keeps completed Supervisor steps when a lower planRevision arrives out of order', async () => {
const runId = 'supervisor-revision-run';
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId,
status: 'running',
phase: 'action',
planRevision: 5,
planExplanation: '初始计划',
plan: ['确认目标', '完成实现', '收束验证'],
planSteps: [
{ step: '确认目标', status: 'completed' },
{ step: '完成实现', status: 'in_progress' },
{ step: '收束验证', status: 'pending' },
],
activePlanStepIndex: 1,
waitingOn: '实现结果',
nextStep: '收束验证',
updatedAt: 5000,
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
await waitFor(() => {
expect(harness.listen).toHaveBeenCalledWith(
'game-creator-agent-runtime-update',
expect.any(Function),
);
});
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId,
status: 'running',
phase: 'action',
planRevision: 6,
planExplanation: '实现已完成,进入验证',
plan: ['确认目标', '完成实现', '收束验证'],
planSteps: [
{ step: '确认目标', status: 'completed' },
{ step: '完成实现', status: 'completed' },
{ step: '收束验证', status: 'in_progress' },
],
activePlanStepIndex: 2,
waitingOn: '验证结果',
nextStep: '整理最终回复',
updatedAt: 6000,
}),
);
});
expect(
(await screen.findByLabelText('项目总控 Agent 进度')).textContent,
).toContain('计划完成:2/3 · 当前步骤:收束验证');
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId,
status: 'running',
phase: 'waiting-for-delegate-receipts',
planRevision: 5,
planExplanation: '迟到的旧计划',
plan: ['确认目标', '完成实现', '收束验证'],
planSteps: [
{ step: '确认目标', status: 'completed' },
{ step: '完成实现', status: 'in_progress' },
{ step: '收束验证', status: 'pending' },
],
activePlanStepIndex: 1,
waitingOn: '迟到读取仍报告等待回执',
nextStep: '迟到读取的运行摘要',
updatedAt: 7000,
}),
);
});
expect(
await screen.findByText('项目总控 Agent · 等待专业 Agent'),
).not.toBeNull();
expect(screen.getByLabelText('项目总控 Agent 进度').textContent).toBe(
'计划完成:2/3 · 当前步骤:收束验证 · 等待:迟到读取仍报告等待回执 · 下一步:迟到读取的运行摘要 · 专业 Agent 协作:0',
);
});
it('clears the old plan for a new Supervisor run and ignores a late old-run event', async () => {
const oldRunId = 'supervisor-completed-old-run';
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId: oldRunId,
status: 'completed',
phase: 'completed',
currentTask: '旧 run 任务',
planRevision: 8,
planExplanation: '旧 run 已完成计划',
plan: ['旧 run 设计', '旧 run 验证'],
planSteps: [
{ step: '旧 run 设计', status: 'completed' },
{ step: '旧 run 验证', status: 'completed' },
],
activePlanStepIndex: null,
updatedAt: 5000,
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
expect(await screen.findByText('项目总控 Agent · 已完成')).not.toBeNull();
await submitChat('开始真正的新一轮实现');
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
const startCall = harness.invoke.mock.calls.find(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const newRunId = String(startCall?.[1]?.runId ?? '');
expect(newRunId).not.toBe('');
expect(newRunId).not.toBe(oldRunId);
expect(await screen.findByText('项目总控 Agent · 分析')).not.toBeNull();
expect(screen.getByLabelText('项目总控 Agent 进度').textContent).toContain(
'计划完成:0/0 · 当前步骤:暂无',
);
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
runId: oldRunId,
status: 'running',
phase: 'waiting-for-delegate-receipts',
currentTask: '迟到的旧 run',
planRevision: 9,
planExplanation: '迟到旧 run 不得覆盖新 run',
plan: ['迟到旧步骤'],
planSteps: [{ step: '迟到旧步骤', status: 'in_progress' }],
activePlanStepIndex: 0,
waitingOn: '旧 run 专业 Agent',
nextStep: '旧 run 摘要',
updatedAt: 9999,
}),
);
});
expect(screen.getByText('项目总控 Agent · 分析')).not.toBeNull();
const progress = screen.getByLabelText('项目总控 Agent 进度');
expect(progress.textContent).toContain('计划完成:0/0 · 当前步骤:暂无');
expect(progress.textContent).not.toContain('迟到旧步骤');
expect(progress.textContent).not.toContain('旧 run 专业 Agent');
});
it('preserves a newer runtime event when an older full runtime map refresh resolves', async () => {
const supervisorRunId = 'supervisor-runtime-map-run';
let resolveRuntimeMap!: (states: Array<Record<string, unknown>>) => void;
const runtimeMap = new Promise<Array<Record<string, unknown>>>(
(resolve) => {
resolveRuntimeMap = resolve;
},
);
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId: supervisorRunId,
status: 'running',
phase: 'action',
updatedAt: 5000,
},
runtimeMapLoader: () => runtimeMap,
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'read_game_creator_agent_runtimes',
{ projectPath: harness.projectPath },
);
expect(harness.listen).toHaveBeenCalledWith(
'game-creator-agent-runtime-update',
expect.any(Function),
);
});
const currentDelegatedRuntime = harness.runtimeState({
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'design-current-session',
runId: 'design-current-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'delegation-current',
status: 'running',
phase: 'action',
updatedAt: 9000,
});
await act(async () => {
harness.emitAgentRuntime(currentDelegatedRuntime);
});
expect(
(await screen.findByLabelText('项目总控 Agent 进度')).textContent,
).toContain('专业 Agent 协作:1');
const staleDelegatedRuntime = harness.runtimeState({
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'design-old-session',
runId: 'design-old-run',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: 'supervisor-old-run',
delegationId: 'delegation-old',
status: 'completed',
phase: 'completed',
updatedAt: 8000,
});
await act(async () => {
resolveRuntimeMap([staleDelegatedRuntime]);
await runtimeMap;
});
expect(screen.getByLabelText('项目总控 Agent 进度').textContent).toContain(
'专业 Agent 协作:1',
);
});
it('starts a new autonomous run instead of steering an active standard Project Supervisor run', async () => {
const activeRunId = 'supervisor-standard-active-run';
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId: activeRunId,
status: 'running',
phase: 'planning',
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
await waitFor(() =>
expect(harness.invoke).toHaveBeenCalledWith(
'read_game_creator_agent_runtime',
expect.objectContaining({ sessionId: harness.sessionId }),
),
);
await submitChat('切换为自主构建并完成可玩原型');
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
const startCall = harness.invoke.mock.calls.find(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
expect(startCall?.[1]).toMatchObject({
projectPath: harness.projectPath,
sessionId: harness.sessionId,
task: '切换为自主构建并完成可玩原型',
runProfile: 'autonomous-game-build',
});
expect(String(startCall?.[1]?.runId ?? '')).not.toBe(activeRunId);
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'steer_game_creator_agent_runtime_task',
),
).toHaveLength(0);
});
it('steers an active autonomous Project Supervisor run for another autonomous request', async () => {
const activeRunId = 'supervisor-autonomous-active-run';
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId: activeRunId,
runProfile: 'autonomous-game-build',
source: 'project-supervisor-gui',
status: 'running',
phase: 'planning',
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
await waitFor(() =>
expect(harness.invoke).toHaveBeenCalledWith(
'read_game_creator_agent_runtime',
expect.objectContaining({ sessionId: harness.sessionId }),
),
);
await submitChat('补充:先检查已有素材');
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'steer_game_creator_agent_runtime_task',
{
projectPath: harness.projectPath,
agentId: 'project-supervisor',
sessionId: harness.sessionId,
runId: activeRunId,
steerId: expect.stringMatching(/^project-supervisor-steer-/),
instruction: '补充:先检查已有素材',
runProfile: 'autonomous-game-build',
source: 'project-supervisor-gui',
},
);
});
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(0);
});
it.skip('reopens merged legacy and Project Supervisor history in stable unique order', async () => {
const projectMessages = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '共享历史',
agentId: null,
messageId: 'shared-message',
updatedAt: 100,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '旧项目需求',
agentId: null,
messageId: 'legacy-user',
updatedAt: 300,
},
];
const supervisorMessages = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '共享历史',
agentId: 'project-supervisor',
messageId: 'shared-message',
updatedAt: 200,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '总控补充',
agentId: 'project-supervisor',
messageId: 'supervisor-user',
updatedAt: 250,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '总控回复',
agentId: 'project-supervisor',
messageId: 'supervisor-assistant',
updatedAt: 400,
},
];
const harness = createProjectSupervisorRuntimeHarness({
projectMessages,
supervisorMessages,
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
const assertMergedHistory = async () => {
await openMainProject(harness.projectPath);
await screen.findByText('总控回复');
const chat = within(screen.getByLabelText('聊天'));
expect(chat.getAllByText('共享历史')).toHaveLength(1);
expect(
['共享历史', '总控补充', '旧项目需求', '总控回复'].map(
(text) => chat.getByText(text).textContent,
),
).toEqual(['共享历史', '总控补充', '旧项目需求', '总控回复']);
};
renderAppAt('/');
await assertMergedHistory();
cleanup();
renderAppAt('/');
await assertMergedHistory();
});
it('confirms and rejects Project Supervisor pending tool actions', async () => {
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId: 'supervisor-run-confirm',
status: 'waiting-for-confirmation',
phase: 'waiting-for-confirmation',
pendingToolAction: {
actionId: 'action-confirm',
actionFingerprint: 'fingerprint-confirm',
tool: 'file.write',
inputSummary: 'game/index.html',
reason: null,
requestedAt: 3000,
},
},
});
harness.setConfirmRuntime(
harness.runtimeState({
runId: 'supervisor-run-confirm',
status: 'waiting-for-confirmation',
phase: 'waiting-for-confirmation',
pendingToolAction: {
actionId: 'action-reject',
actionFingerprint: 'fingerprint-reject',
tool: 'command.exec',
inputSummary: 'npm test',
reason: null,
requestedAt: 4000,
},
}),
);
harness.setRejectRuntime(
harness.runtimeState({
runId: 'supervisor-run-confirm',
status: 'running',
phase: 'planning',
pendingToolAction: null,
}),
);
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
let pendingAction =
await screen.findByLabelText('项目总控 Agent 待确认动作');
fireEvent.click(
within(pendingAction).getByRole('button', { name: '确认' }),
);
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'confirm_game_creator_agent_runtime_task',
{
projectPath: harness.projectPath,
agentId: 'project-supervisor',
runId: 'supervisor-run-confirm',
actionId: 'action-confirm',
note: '用户已确认待执行工具动作',
},
);
});
pendingAction = await screen.findByLabelText('项目总控 Agent 待确认动作');
expect(within(pendingAction).getByText('command.exec')).not.toBeNull();
fireEvent.click(
within(pendingAction).getByRole('button', { name: '拒绝' }),
);
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'reject_game_creator_agent_runtime_task',
{
projectPath: harness.projectPath,
agentId: 'project-supervisor',
runId: 'supervisor-run-confirm',
actionId: 'action-reject',
note: '用户已拒绝待执行工具动作',
},
);
});
});
it('answers Project Supervisor Needs input on the same run and retries with one response id', async () => {
const runId = 'supervisor-needs-input-run';
const request = agentRuntimeUserInputRequest({
agentId: 'project-supervisor',
sessionId: 'supervisor-session-active',
runId,
});
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId,
status: 'waiting-for-user-input',
phase: 'waiting-for-user-input',
currentTask: '准备首版角色规范图',
currentAction: '等待用户补充关键信息',
waitingOn: '你的澄清回答',
nextStep: '提交全部回答后继续同一 Run',
userInputRequest: request,
updatedAt: 6000,
},
});
harness.failNextAnswers();
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
const card = await screen.findByLabelText('Needs input');
expect(screen.getByText('项目总控 Agent · 需要回答')).not.toBeNull();
expect(await composerDisabled()).toBe(true);
expect(screen.getByRole('button', { name: '等待回答' })).not.toBeNull();
fireEvent.click(within(card).getByRole('button', { name: /像素风/ }));
fireEvent.click(within(card).getByRole('button', { name: '提交回答' }));
expect(
await screen.findByText(/回答提交失败:模拟回答提交中断/),
).not.toBeNull();
const firstAnswerCall = harness.invoke.mock.calls.find(
([command]) => command === 'answer_game_creator_agent_runtime_user_input',
);
const firstResponseId = String(firstAnswerCall?.[1]?.responseId ?? '');
expect(firstResponseId).toMatch(/^app-user-input-/);
fireEvent.click(within(card).getByRole('button', { name: '提交回答' }));
await waitFor(() => {
const answerCalls = harness.invoke.mock.calls.filter(
([command]) =>
command === 'answer_game_creator_agent_runtime_user_input',
);
expect(answerCalls).toHaveLength(2);
expect(answerCalls[1]?.[1]).toEqual({
projectPath: harness.projectPath,
agentId: 'project-supervisor',
runId,
actionId: request.actionId,
requestId: request.requestId,
responseId: firstResponseId,
answers: { visual_direction: '像素风' },
});
});
expect(await screen.findByText('像素风')).not.toBeNull();
expect(screen.queryByLabelText('Needs input')).toBeNull();
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'steer_game_creator_agent_runtime_task',
),
).toHaveLength(0);
});
it('renders only the question card when the pending action is the user input carrier', async () => {
const runId = 'supervisor-user-input-carrier-run';
const request = agentRuntimeUserInputRequest({
agentId: 'project-supervisor',
sessionId: 'supervisor-session-active',
runId,
actionId: 'action-user-input-carrier',
});
// Runtime 在 parent-wake 屏障处把子 Agent 的澄清问题包成 user.input_request pending
// 同一份问题再投影成 userInputRequest。pending 只是载体,没有确认语义,通用待确认卡
// 套上去就是把同一个请求画两遍,还会把 questionsSha256 这类取证摘要推到用户面前。
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId,
status: 'waiting-for-user-input',
phase: 'waiting-for-user-input',
currentTask: '准备首版角色规范图',
currentAction: '等待用户补充关键信息',
waitingOn: '你的澄清回答',
nextStep: '提交全部回答后继续同一 Run',
userInputRequest: request,
pendingToolAction: {
actionId: request.actionId,
actionFingerprint: 'fingerprint-user-input-carrier',
tool: 'user.input_request',
inputSummary:
'questionCount=1 · optionCount=2 · questionChars=302 · questionsSha256=6aecb0cf',
reason: '代 Supervisor 汇总子 Agent 的澄清问题',
requestedAt: 6000,
},
updatedAt: 6000,
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
await screen.findByLabelText('Needs input');
expect(screen.queryByLabelText('项目总控 Agent 待确认动作')).toBeNull();
expect(screen.queryByText('user.input_request')).toBeNull();
expect(screen.queryByText(/questionsSha256=/)).toBeNull();
});
it('recovers a Project Supervisor transient reply from runtime polling without persisting it', async () => {
const runId = 'supervisor-response-stream-recovery-run';
const initialRuntime = {
runId,
status: 'running',
phase: 'response',
currentTask: '恢复后台总控最终回复',
loopIteration: 1,
appliedSteerCursor: 0,
updatedAt: 6000,
};
const initialResponseStream = projectSupervisorResponseStream({
runId,
sequence: 2,
status: 'ready',
accumulatedText: '这是从 Runtime 私有快照恢复的总控回复。',
});
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime,
initialResponseStream,
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
expect(
(await screen.findByLabelText('项目总控 Agent 实时回复')).textContent,
).toBe('这是从 Runtime 私有快照恢复的总控回复。');
expect(screen.getByText('项目总控 Agent · 回复中')).not.toBeNull();
expect(
harness.invoke.mock.calls.filter(
([command, args]) =>
command === 'append_local_conversation_message' &&
((args as Record<string, unknown>)?.message as { content?: string })
?.content === '这是从 Runtime 私有快照恢复的总控回复。',
),
).toHaveLength(0);
});
it('merges Project Supervisor response deltas monotonically and invalidates them after steer', async () => {
const runId = 'supervisor-response-stream-monotonic-run';
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId,
status: 'running',
phase: 'response',
currentTask: '验证总控流式回复',
loopIteration: 1,
appliedSteerCursor: 0,
updatedAt: 6000,
},
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
const runtime = harness.runtimeState({
runId,
status: 'running',
phase: 'response',
currentTask: '验证总控流式回复',
loopIteration: 1,
appliedSteerCursor: 0,
updatedAt: 6000,
});
await act(async () => {
harness.emitRuntime(
runtime,
projectSupervisorResponseStream({
runId,
sequence: 1,
accumulatedText: '第一段',
}),
);
});
expect(
(await screen.findByLabelText('项目总控 Agent 实时回复')).textContent,
).toBe('第一段');
await act(async () => {
harness.emitRuntime(
{ ...runtime, updatedAt: 6002 },
projectSupervisorResponseStream({
runId,
sequence: 3,
accumulatedText: '第一段,第二段。',
}),
);
harness.emitRuntime(
{ ...runtime, updatedAt: 6001 },
projectSupervisorResponseStream({
runId,
sequence: 2,
accumulatedText: '过期正文不得回退',
}),
);
harness.emitRuntime(
{ ...runtime, updatedAt: 6003 },
projectSupervisorResponseStream({
runId,
sequence: 3,
accumulatedText: '同序冲突不得覆盖',
}),
);
});
expect(screen.getByLabelText('项目总控 Agent 实时回复').textContent).toBe(
'第一段,第二段。',
);
const steeredRuntime = harness.runtimeState({
...runtime,
appliedSteerCursor: 1,
queuedSteerCount: 1,
updatedAt: 7000,
});
await act(async () => {
harness.emitRuntime(
steeredRuntime,
projectSupervisorResponseStream({
runId,
sequence: 4,
accumulatedText: '纠偏前旧回复',
appliedSteerCursor: 0,
}),
);
});
expect(screen.queryByLabelText('项目总控 Agent 实时回复')).toBeNull();
await act(async () => {
harness.emitRuntime(
{ ...steeredRuntime, queuedSteerCount: 0, updatedAt: 7001 },
projectSupervisorResponseStream({
runId,
sequence: 1,
accumulatedText: '纠偏后的新回复。',
appliedSteerCursor: 1,
}),
);
});
expect(
(await screen.findByLabelText('项目总控 Agent 实时回复')).textContent,
).toBe('纠偏后的新回复。');
expect(
harness.invoke.mock.calls.filter(
([command, args]) =>
command === 'append_local_conversation_message' &&
['第一段', '第一段,第二段。', '纠偏后的新回复。'].includes(
String(
(
(args as Record<string, unknown>)?.message as {
content?: string;
}
)?.content ?? '',
),
),
),
).toHaveLength(0);
});
it('ignores a stale null polling result that returns after a newer Supervisor stream event', async () => {
const runId = 'supervisor-response-stream-poll-race-run';
const runtime = {
runId,
status: 'running',
phase: 'response',
currentTask: '验证流事件与轮询竞态',
loopIteration: 1,
appliedSteerCursor: 0,
updatedAt: 6002,
};
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: runtime,
initialResponseStream: projectSupervisorResponseStream({
runId,
sequence: 1,
accumulatedText: '旧前缀',
}),
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
expect(
(await screen.findByLabelText('项目总控 Agent 实时回复')).textContent,
).toBe('旧前缀');
let readStarted = false;
let resolveStaleRead!: (value: Record<string, unknown>) => void;
const staleRead = new Promise<Record<string, unknown>>((resolve) => {
resolveStaleRead = resolve;
});
const staleSnapshot = harness.runtimeResult(
harness.runtimeState(runtime),
null,
);
let readCount = 0;
harness.setRuntimeReader(async () => {
readCount += 1;
if (readCount === 1) {
readStarted = true;
return staleRead;
}
return harness.runtimeResult();
});
await waitFor(() => expect(readStarted).toBe(true), { timeout: 2_000 });
await act(async () => {
harness.emitRuntime(
harness.runtimeState(runtime),
projectSupervisorResponseStream({
runId,
sequence: 2,
accumulatedText: '旧前缀,新事件。',
}),
);
});
expect(screen.getByLabelText('项目总控 Agent 实时回复').textContent).toBe(
'旧前缀,新事件。',
);
await act(async () => {
resolveStaleRead(staleSnapshot);
await staleRead;
});
expect(screen.getByLabelText('项目总控 Agent 实时回复').textContent).toBe(
'旧前缀,新事件。',
);
await waitFor(() => expect(readCount).toBeGreaterThan(1), {
timeout: 2_000,
});
expect(screen.getByLabelText('项目总控 Agent 实时回复').textContent).toBe(
'旧前缀,新事件。',
);
harness.setRuntimeReader(null);
});
it('does not revive a stale nonempty Supervisor stream after a newer same-run event', async () => {
const runId = 'supervisor-response-stream-stale-nonempty-run';
const runtime = {
runId,
status: 'running',
phase: 'response',
currentTask: '验证旧非空流不会复活',
loopIteration: 1,
appliedSteerCursor: 0,
queuedSteerCount: 0,
updatedAt: 6002,
};
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: runtime,
initialResponseStream: projectSupervisorResponseStream({
runId,
sequence: 1,
accumulatedText: '纠偏前回复',
}),
});
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
expect(
(await screen.findByLabelText('项目总控 Agent 实时回复')).textContent,
).toBe('纠偏前回复');
let readCount = 0;
let resolveStaleRead!: (value: Record<string, unknown>) => void;
const staleRead = new Promise<Record<string, unknown>>((resolve) => {
resolveStaleRead = resolve;
});
const staleSnapshot = harness.runtimeResult(
harness.runtimeState(runtime),
projectSupervisorResponseStream({
runId,
sequence: 2,
accumulatedText: '这条旧轮询正文不得复活',
}),
);
harness.setRuntimeReader(async () => {
readCount += 1;
if (readCount === 1) {
return staleRead;
}
return harness.runtimeResult();
});
await waitFor(() => expect(readCount).toBe(1), { timeout: 2_000 });
await act(async () => {
harness.emitRuntime(
harness.runtimeState({
...runtime,
appliedSteerCursor: 1,
updatedAt: 6002,
}),
null,
);
});
expect(screen.queryByLabelText('项目总控 Agent 实时回复')).toBeNull();
await act(async () => {
resolveStaleRead(staleSnapshot);
await staleRead;
});
expect(screen.queryByLabelText('项目总控 Agent 实时回复')).toBeNull();
await waitFor(() => expect(readCount).toBeGreaterThan(1), {
timeout: 2_000,
});
expect(screen.queryByLabelText('项目总控 Agent 实时回复')).toBeNull();
harness.setRuntimeReader(null);
});
it('refreshes one Project Supervisor assistant after the terminal event', async () => {
const harness = createProjectSupervisorRuntimeHarness();
window.__TAURI__ = {
core: { invoke: harness.invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
await waitFor(() => {
expect(harness.listen).toHaveBeenCalledWith(
'game-creator-agent-runtime-update',
expect.any(Function),
);
});
await submitChat('完成本轮总控任务');
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) =>
command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
const startCall = harness.invoke.mock.calls.find(
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const runId = String(startCall?.[1]?.runId ?? '');
const respondingRuntime = harness.runtimeState({
runId,
status: 'running',
phase: 'response',
loopIteration: 1,
appliedSteerCursor: 0,
updatedAt: 8000,
});
await act(async () => {
harness.emitRuntime(
respondingRuntime,
projectSupervisorResponseStream({
runId,
sequence: 2,
status: 'ready',
accumulatedText: '唯一的总控回复',
}),
);
});
expect(
(await screen.findByLabelText('项目总控 Agent 实时回复')).textContent,
).toBe('唯一的总控回复');
const assistant = {
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '唯一的总控回复',
agentId: 'project-supervisor',
messageId: 'supervisor-final-assistant',
updatedAt: 9000,
};
harness.appendSupervisorMessage(assistant);
harness.appendSupervisorMessage({ ...assistant });
const completedRuntime = harness.runtimeState({
runId,
status: 'idle',
phase: 'completed',
lastResponse: '唯一的总控回复',
updatedAt: 9000,
});
await act(async () => {
const committedStream = projectSupervisorResponseStream({
runId,
sequence: 3,
status: 'committed',
accumulatedText: '唯一的总控回复',
});
harness.emitRuntime(completedRuntime, committedStream);
harness.emitRuntime(completedRuntime, committedStream);
});
expect(await screen.findByText('唯一的总控回复')).not.toBeNull();
expect(screen.getAllByText('唯一的总控回复')).toHaveLength(1);
expect(screen.queryByLabelText('项目总控 Agent 实时回复')).toBeNull();
expect(
harness.invoke.mock.calls.filter(
([command, args]) =>
command === 'append_local_conversation_message' &&
((args as Record<string, unknown>)?.message as { content?: string })
?.content === '唯一的总控回复',
),
).toHaveLength(0);
});
it('offers an in-project Supervisor retry while a professional Agent keeps running', async () => {
const supervisorRunId = 'supervisor-failed-run';
const harness = createProjectSupervisorRuntimeHarness({
initialRuntime: {
runId: supervisorRunId,
status: 'failed',
phase: 'failed',
currentAction: '等待用户处理失败',
error: 'LLM 服务暂时不可用,请检查配置后重试',
},
runtimeMapLoader: async () => [
{
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'code-prototype',
taskId: 'code-prototype',
sessionId: 'code-session-active',
runId: 'code-still-running',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'code-still-running-delegation',
status: 'running',
phase: 'planning',
currentTask: '继续实现当前游戏原型',
currentGoal: '完成程序原型',
currentAction: '分析下一步修改',
waitingOn: '',
nextStep: '修改游戏文件',
plan: ['读取现有代码', '实现玩法'],
planSteps: [
{
index: 0,
title: '读取现有代码',
step: '读取现有代码',
status: 'completed',
updatedAt: 6100,
},
{
index: 1,
title: '实现玩法',
step: '实现玩法',
status: 'active',
updatedAt: 6200,
},
],
activePlanStepIndex: 1,
observations: [],
allowedTools: [],
pendingToolAction: null,
lastResponse: null,
error: null,
updatedAt: 6200,
},
{
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-foundation',
taskId: 'design-foundation',
sessionId: 'design-session-failed',
runId: 'design-failed-under-terminal-supervisor',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: supervisorRunId,
delegationId: 'design-failed-delegation',
status: 'failed',
phase: 'failed',
currentTask: '补齐玩法设计',
currentGoal: '完成玩法设计',
currentAction: '等待恢复',
waitingOn: '',
nextStep: '',
plan: [],
observations: [],
allowedTools: [],
pendingToolAction: null,
lastResponse: null,
error: 'kind=transport fingerprint=private-child-fingerprint',
updatedAt: 6150,
},
],
});
let failNextRetry = true;
let acceptedRetryRunId: string | null = null;
let releaseAcceptedRuntimePoll: (() => void) | null = null;
const acceptedRuntimePollGate = new Promise<void>((resolve) => {
releaseAcceptedRuntimePoll = resolve;
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'confirm_retry_game_creator_agent_runtime_task') {
if (failNextRetry) {
failNextRetry = false;
throw new Error(
'kind=transport fingerprint=private-supervisor-retry-fingerprint',
);
}
acceptedRetryRunId = String(args?.nextRunId ?? '');
const staleFailedResult = harness.runtimeResult(
harness.runtimeState({
runId: supervisorRunId,
status: 'failed',
phase: 'failed',
currentAction: '等待用户处理失败',
error: 'LLM 服务暂时不可用,请检查配置后重试',
updatedAt: 6500,
}),
);
return {
...staleFailedResult,
acceptedRunId: acceptedRetryRunId,
taskQueue: {
...staleFailedResult.taskQueue,
latestRunId: acceptedRetryRunId,
},
};
}
if (
command === 'read_game_creator_agent_runtime' &&
acceptedRetryRunId
) {
await acceptedRuntimePollGate;
return harness.runtimeResult(
harness.runtimeState({
runId: acceptedRetryRunId,
status: 'waiting-for-confirmation',
phase: 'planning',
currentTask: '重新接管当前项目',
currentGoal: '继续完成当前项目',
currentAction: '等待确认后继续规划',
waitingOn: '用户确认',
nextStep: '确认后继续协调专业 Agent',
error: null,
updatedAt: 7000,
}),
);
}
return harness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: harness.listen },
};
renderAppAt('/');
await openMainProject(harness.projectPath);
expect(await screen.findByText('项目总控 Agent · 失败')).not.toBeNull();
expect(
screen.getByText('LLM 服务暂时不可用,请检查配置后重试'),
).not.toBeNull();
const professionalList =
await screen.findByLabelText('专业 Agent 实时状态');
expect(within(professionalList).getByText('程序原型 Agent')).not.toBeNull();
expect(within(professionalList).getByText('分析中')).not.toBeNull();
expect(
within(professionalList).getByText(
'请先重试项目总控,再由新总控继续安排此任务',
),
).not.toBeNull();
expect(
within(professionalList).queryByRole('button', {
name: '在当前项目重试',
}),
).toBeNull();
expect(screen.getByText('程序原型 Agent仍在运行。')).not.toBeNull();
expect(
screen.getByText('在当前项目重新启动总控,不会新建项目。'),
).not.toBeNull();
const retryButton = screen.getByRole('button', {
name: '在当前项目重试总控',
});
fireEvent.click(retryButton);
expect(
await screen.findByText('项目总控 Agent 服务连接失败,请稍后重试'),
).not.toBeNull();
expect(
screen.queryByText(/private-supervisor-retry-fingerprint/),
).toBeNull();
expect(screen.getByText('项目总控 Agent · 失败')).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: '在当前项目重试总控' }));
expect(
screen.getByRole('button', { name: '正在重试项目总控…' }),
).toHaveProperty('disabled', true);
expect(screen.getByRole('status').textContent).toBe('正在重试项目总控…');
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'confirm_retry_game_creator_agent_runtime_task',
{
projectPath: harness.projectPath,
agentId: 'project-supervisor',
runId: supervisorRunId,
nextRunId: expect.stringMatching(/^project-supervisor-retry-/),
},
);
});
const acceptedButton = await screen.findByRole('button', {
name: '重试已受理',
});
expect(acceptedButton).toHaveProperty('disabled', true);
expect(screen.getByRole('status').textContent).toBe(
'重试已受理,正在同步新一轮项目总控状态',
);
fireEvent.click(acceptedButton);
expect(
invoke.mock.calls.filter(
([command]) =>
command === 'confirm_retry_game_creator_agent_runtime_task',
),
).toHaveLength(2);
releaseAcceptedRuntimePoll?.();
expect(await screen.findByText('项目总控 Agent · 等待确认')).not.toBeNull();
expect(screen.getByText('当前阶段:待确认')).not.toBeNull();
expect(screen.queryByText('项目总控 Agent · 失败')).toBeNull();
expect(
screen.queryByText('LLM 服务暂时不可用,请检查配置后重试'),
).toBeNull();
expect(screen.queryByText(/项目总控 Agent 执行失败/)).toBeNull();
});
it('keeps the main App usable when persistent runtime listens reject', async () => {
const listen = vi.fn(async (eventName: string) => {
if (
eventName === 'game-creator-agent-progress' ||
eventName === 'game-creator-agent-runtime-update'
) {
throw new Error('core:event:allow-listen denied');
}
throw new Error(`unexpected listen ${eventName}`);
});
window.__TAURI__ = { event: { listen } };
renderAppAt('/');
expect(
await screen.findByText(
'run: 实时状态不可用:core:event:allow-listen denied',
),
).not.toBeNull();
expect(screen.getByText('陶泥儿')).not.toBeNull();
expect(listen).toHaveBeenCalledWith(
'game-creator-agent-progress',
expect.any(Function),
);
expect(listen).toHaveBeenCalledWith(
'game-creator-agent-runtime-update',
expect.any(Function),
);
});
it('streams agent generation progress into the user chat', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
let progressHandler:
| ((event: {
payload: { projectPath: string; stage: string; message: string };
}) => void)
| null = null;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'append_local_permission_log') {
return {};
}
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: [],
};
}
if (command === 'read_local_project_file') {
throw new Error(
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
);
}
if (command === 'list_local_project_files') {
return { projectPath: String(args?.projectPath ?? ''), files: [] };
}
if (command === '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}`);
},
);
const listen = vi.fn(
async (eventName: string, handler: typeof progressHandler) => {
if (eventName === 'game-creator-agent-progress') {
progressHandler = handler;
return vi.fn();
}
if (eventName === 'game-creator-agent-runtime-update') {
return vi.fn();
}
throw new Error(`unexpected listen ${eventName}`);
},
);
window.__TAURI__ = { core: { invoke }, event: { listen } };
renderAppAt('/');
await submitChat('/project /tmp/authorized-game');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
).not.toBeNull();
await act(async () => {
progressHandler?.({
payload: {
projectPath: '/tmp/other-game',
stage: 'llm.planner',
message: '不应该显示',
},
});
progressHandler?.({
payload: {
projectPath: '/tmp/authorized-game',
stage: 'llm.planner',
message: 'Planner 正在调用 LLM 整理规格和专业组分工',
},
});
});
expect(listen).toHaveBeenCalledWith(
'game-creator-agent-progress',
expect.any(Function),
);
expect(screen.queryByText('不应该显示')).toBeNull();
expect(
screen.getByText('Planner 正在调用 LLM 整理规格和专业组分工'),
).not.toBeNull();
});
it('shows agent loop evidence in chat after generation completes', async () => {
const manifest = createGameCreationAppManifest(
'local-project-draft',
'未命名游戏原型',
);
const traceTasks = createGameCreationAppSeedTasks();
traceTasks[0]!.status = 'completed';
traceTasks[1]!.status = 'completed';
traceTasks[9]!.status = 'completed';
traceTasks[10]!.status = 'completed';
traceTasks[11]!.status = 'completed';
const trace: GameCreationAgentRunTrace = {
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
runId: 'run-chat-generate',
commandId: 'game.generate_draft',
status: 'passed',
passes: 2,
maxPasses: 3,
toolCallCount: 36,
maxToolCalls: 128,
stopReason: 'evaluator-passed',
goal: '做一个反弹弹幕厨房游戏',
coordination: 'filesystem',
steps: [
{
pass: 0,
agent: 'Planner',
phase: 'planning',
taskId: 'design-director',
group: 'design',
role: 'Director',
status: 'completed',
inputPaths: [
'memory/session.md',
'memory/project.md',
'.agent/manifest.json',
],
outputPaths: ['.agent/spec.md'],
summary: '完成玩法规格和专业组分工',
toolCalls: [
{
toolId: 'llm.chat.planner',
status: 'completed',
inputPaths: ['memory/session.md', 'memory/project.md'],
outputPaths: ['.agent/spec.md'],
summary: 'Planner 规格生成',
},
],
},
{
pass: 2,
agent: 'Generator',
phase: 'generate',
taskId: 'code-prototype',
group: 'code',
role: 'Code',
status: 'completed',
inputPaths: [
'.agent/spec.md',
'.agent/findings.md',
'.agent/passes/pass-2/agenda.md',
],
outputPaths: ['.agent/passes/pass-2/draft.json'],
summary: '生成结构化游戏草案',
toolCalls: [
{
toolId: 'llm.chat.generator',
status: 'completed',
inputPaths: ['.agent/spec.md', '.agent/findings.md'],
outputPaths: ['.agent/passes/pass-2/draft.json'],
summary: 'Generator 草案生成',
},
],
},
{
pass: 2,
agent: 'Evaluator',
phase: 'evaluation',
taskId: 'quality-review',
group: 'code',
role: 'Review',
status: 'passed',
inputPaths: ['.agent/passes/pass-2/draft.json'],
outputPaths: ['.agent/findings.md'],
summary: '质量评审通过',
toolCalls: [
{
toolId: 'evaluator.quality_review',
status: 'completed',
inputPaths: ['.agent/passes/pass-2/draft.json'],
outputPaths: ['.agent/findings.md'],
summary: 'Evaluator 质量评审',
},
],
},
{
pass: 2,
agent: '美术组 / Asset',
phase: 'role-brief',
taskId: 'art-asset-plan',
group: 'art',
role: 'Asset',
status: 'completed',
inputPaths: ['.agent/manifest.json'],
outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'],
summary: '需要回流画板角色素材',
toolCalls: [
{
toolId: 'agent.tool.suggest.canvas.project_sync',
status: 'suggested',
inputPaths: ['.agent/manifest.json'],
outputPaths: [],
summary:
'项目还没有画板回流资产;建议用户确认 /sync-canvas-project <画板项目ID>。',
},
],
},
],
artifacts: [
{
path: 'game/index.html',
sizeBytes: 1024,
checksum: 'fnv1a64:game',
},
{
path: 'game/game_design.md',
sizeBytes: 256,
checksum: 'fnv1a64:design',
},
{
path: 'assets/manifest.art.json',
sizeBytes: 128,
checksum: 'fnv1a64:art',
},
],
taskGraph: {
goal: '做一个反弹弹幕厨房游戏',
readyTaskIds: ['preview-playtest'],
activeTaskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
carriedTaskIds: ['design-director', 'design-foundation'],
repairFocus: ['缺少输入监听'],
repairRoutes: [
{
issue: '缺少输入监听',
taskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
reason: 'code-runtime',
},
],
tasks: traceTasks,
},
passPlans: [
{
pass: 2,
mode: 'repair',
summary: '第 2 轮按 Evaluator 反馈返工',
activeTaskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
carriedTaskIds: ['design-director', 'design-foundation'],
dependencyWaves: [
['code-director'],
['code-prototype'],
['quality-review'],
['preview-readiness'],
],
repairFocus: ['缺少输入监听'],
repairRoutes: [
{
issue: '缺少输入监听',
taskIds: [
'code-director',
'code-prototype',
'quality-review',
'preview-readiness',
],
reason: 'code-runtime',
},
],
},
],
nextStep: 'preview-playtest',
error: null,
updatedAt: 1,
};
const generatedManifest = {
...manifest,
goal: '做一个反弹弹幕厨房游戏',
tasks: traceTasks,
};
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 === 'generate_local_game_draft') {
return {
projectPath: String(args?.projectPath ?? ''),
gameIndexPath: `${String(args?.projectPath ?? '')}/game/index.html`,
designPath: `${String(args?.projectPath ?? '')}/game/game_design.md`,
shortMemoryPath: `${String(args?.projectPath ?? '')}/memory/session.md`,
longMemoryPath: `${String(args?.projectPath ?? '')}/memory/project.md`,
manifest: generatedManifest,
};
}
if (command === 'read_local_project_file') {
return {
path: String(args?.relativePath ?? ''),
absolutePath: `${String(args?.projectPath ?? '')}/${String(
args?.relativePath ?? '',
)}`,
content: JSON.stringify(trace),
};
}
if (command === 'list_local_project_files') {
return {
projectPath: String(args?.projectPath ?? ''),
files: [
{
path: '.agent/runs/run-chat-generate.json',
kind: 'file',
size: 2048,
},
],
};
}
if (command === 'start_local_game_preview') {
return {
status: 'running',
url: 'http://127.0.0.1:3210/',
port: 3210,
root: String(args?.projectPath ?? ''),
};
}
if (command === 'activate_local_game_preview') {
return {
status: 'running',
url: 'http://127.0.0.1:3210/',
port: 3210,
root: String(args?.projectPath ?? ''),
};
}
if (command === 'read_project_permission_policy') {
return emptyProjectPolicy();
}
if (command === 'get_local_game_manifest') {
return generatedManifest;
}
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('/generate 做一个反弹弹幕厨房游戏');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(/开始调用 LLM:Planner 正在整理规格。/),
).not.toBeNull();
expect(screen.getByText(/Generator 生成代码和资产清单/)).not.toBeNull();
expect(
await screen.findByText(
/已保存并在客户端运行视图启动预览:http:\/\/127\.0\.0\.1:3210\//,
),
).not.toBeNull();
expect(screen.getByText(/Runrun-chat-generate/)).not.toBeNull();
expect(
screen.getByText(/状态:passed · 2\/3 轮 · evaluator-passed/),
).not.toBeNull();
expect(screen.getByText(/工具调用:36\/128/)).not.toBeNull();
expect(screen.getByText(/LLM 对话:/)).not.toBeNull();
expect(
screen.getByText(
/Planner #0 · completed · planning · llm\.chat\.planner/,
),
).not.toBeNull();
expect(
screen.getByText(
/Generator #2 · completed · generate · llm\.chat\.generator/,
),
).not.toBeNull();
expect(
screen.getByText(
/active 任务:程序组 \/ Director 拆解程序实现\(code-director\), 程序组 \/ Code 生成可运行原型\(code-prototype\), 程序组 \/ Review 执行质量评审\(quality-review\), 程序组 \/ Preview 执行静态自检\(preview-readiness\)/,
),
).not.toBeNull();
expect(screen.getByText(/carry-over 任务:设计实现组/)).not.toBeNull();
expect(screen.getByText(/返工焦点:缺少输入监听/)).not.toBeNull();
expect(
screen.getByText(/agent\.tool\.suggest\.canvas\.project_sync/),
).not.toBeNull();
expect(
screen.getByText(/\/sync-canvas-project <画板项目ID>/),
).not.toBeNull();
expect(screen.getByText(/编排轮次:/)).not.toBeNull();
expect(screen.getByText(/产物快照:/)).not.toBeNull();
expect(screen.getByText(/game\/index\.html · fnv1a64:game/)).not.toBeNull();
expect(screen.getByText(/最近步骤:/)).not.toBeNull();
expect(
screen.getByText(/Evaluator #2 · passed · evaluation/),
).not.toBeNull();
expect(screen.getByText(/完整 trace\/trace/)).not.toBeNull();
expect(screen.queryByLabelText('开发环境')).toBeNull();
expect(screen.queryByText('编排 Trace')).toBeNull();
expect(invoke).toHaveBeenCalledWith('generate_local_game_draft', {
projectPath: '/tmp/authorized-game',
prompt: '做一个反弹弹幕厨房游戏',
});
expect(invoke).toHaveBeenCalledWith('read_local_project_file', {
projectPath: '/tmp/authorized-game',
relativePath: '.agent/run.latest.json',
commandId: 'agent.trace_read',
});
expect(invoke).toHaveBeenCalledWith('start_local_game_preview', {
projectPath: '/tmp/authorized-game',
});
expect(invoke).not.toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: '/tmp/authorized-game',
});
});
it('rejects unsafe generated project paths before preview side effects', 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 === 'generate_local_game_draft') {
return {
projectPath: 'relative-game',
gameIndexPath: 'relative-game/game/index.html',
designPath: 'relative-game/game/game_design.md',
shortMemoryPath: 'relative-game/memory/session.md',
longMemoryPath: 'relative-game/memory/project.md',
manifest,
};
}
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('/generate 做一个厨房弹幕游戏');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(await screen.findByText('生成结果项目路径无效')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('generate_local_game_draft', {
projectPath: '/tmp/authorized-game',
prompt: '做一个厨房弹幕游戏',
});
expect(invoke).not.toHaveBeenCalledWith(
'read_local_project_file',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'start_local_game_preview',
expect.anything(),
);
expect(invoke).not.toHaveBeenCalledWith(
'activate_local_game_preview',
expect.anything(),
);
});
it('opens runtime config when game generation is missing LLM configuration', 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 === 'generate_local_game_draft') {
throw new Error(
'LLM 未配置:请在 /tmp/game-creator.config.json 的 llm.apiKey 中设置 API Key',
);
}
if (command === 'read_local_project_file') {
throw new Error('trace missing');
}
if (command === 'read_game_creator_app_config') {
return {
path: '/tmp/game-creator.config.json',
config: {
llm: {
apiKey: '',
baseUrl: 'https://api.example.test/v1',
model: 'gpt-4.1',
apiKind: 'openai_responses',
stream: false,
webSearchEnabled: false,
requestTimeoutMs: 60000,
maxRetries: 0,
retryBackoffMs: 500,
},
editorApi: {
baseUrl: 'https://editor.example.test',
apiKey: '',
},
},
};
}
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('/generate 做一个厨房弹幕游戏');
fireEvent.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText(
'LLM 未配置:请在 /tmp/game-creator.config.json 的 llm.apiKey 中设置 API Key',
),
).not.toBeNull();
expect(
await screen.findByRole('dialog', { name: '运行时配置' }),
).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('read_game_creator_app_config');
});
}