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 后已移除的历史对话、附件入口和旧工具组流程断言。 保留当前界面仍使用的工具调用、时间戳和资源预览契约。
2053 lines
68 KiB
TypeScript
2053 lines
68 KiB
TypeScript
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(/Run:run-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');
|
||
});
|
||
}
|