Files
Genarrative/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts
T
lhk229 c2d5273873
Project CI / Repository checks (pull_request) Successful in 2m58s
Project CI / Frontend tests (pull_request) Successful in 3m4s
Project CI / Backend tests (pull_request) Successful in 4m6s
Project CI / Native shell tests (pull_request) Failing after 11m28s
重做 master 合并,找回上次合并丢失的分支工作
上次合并 3d8abac33 把 24 个双边改动文件里的 7 个整份取了 master,另有数个
实质取了 master 版本,静默回滚了分支工作;其中 main_loop.rs 保留 master 的
调用点、provider_recovery.rs 保留分支的 cfg(test) 门,导致非测试构建编译失败。

本次从 ca6a3cd4c 对最新 master(341761ee3) 重做,逐个人工解析 38 处冲突:

- 澄清等待态:保留分支的 lane 外 parent-wake 设计与锁守卫,套上 master 新增的
  parent task 身份校验与 game-chat 安全默认返工;`_locked` 两种 false 语义在
  delivery.rs 按 trusted_game_chat_autonomous_parent_chain_at 区分。
- decision-log:按日期把 master 三条插进分支条目之间,57 处 M1 记录全部保留。
- manifest.rs:采用 master 的 windows_sys 绑定,保留分支的目录/reparse point 拒绝。
- 做方案入口按 master 的提交重构改造:resolveProjectSupervisorRuntimeSubmission
  新增 planningEntry,startMode 经 launcher context 传到 App;并把策划入口从
  direct-codex 产品默认中摘出,做游戏与做素材保持 master 新默认。
- 首页两条入口用例按 master 已改的创建流程更新;非空目录确认用例因该流程
  在首页入口不再可达而移除。

验证:cargo check --all-targets 通过;agc typecheck 通过;appSurface 395 passed。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:15:16 +00:00

1738 lines
58 KiB
TypeScript
Raw 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 { describe, expect, test, vi } from 'vitest';
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
import { PROJECT_SUPERVISOR_PLAN_SOURCE } from '../src/app/constants';
import type {
AgentRuntimeEventRecord,
AgentRuntimeResponseStream,
AgentRuntimeResult,
AgentRuntimeState,
AgentRuntimeTaskRecord,
ChatMessage,
LocalConversationMessageRecord,
} from '../src/app/types';
import {
canArchiveGameChatStage,
gameChatRuntimeClaimsDynamicArtLineage,
latestGameChatPlayableRevision,
projectCurrentGameChatRuntimeLineage,
projectGameChatPrimaryProgress,
} from '../src/features/agent-runtime/gameChatRuntimeProjection';
import {
agentRuntimeConversationStatus,
agentRuntimeStateFromResult,
formatAgentRecentRuntimeTask,
formatAgentRuntimeEvent,
isAgentRuntimeTerminalState,
mergeGameChatRuntimeResponseMessagesIntoHistory,
mergeProjectSupervisorConversation,
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
projectRuntimeStatusPresentation,
projectRuntimeVisibleCurrentWork,
projectRuntimeVisibleError,
projectSupervisorChatRuntimeStatus,
projectSupervisorCollaboratingAgentRuntimes,
projectSupervisorResponseStreamIdentity,
projectSupervisorRuntimeStatusLabel,
projectSupervisorVisibleConversationText,
projectWorkspaceStatusForDisplay,
resolveProjectSupervisorRuntimeSubmission,
submitProjectSupervisorRuntimeTask,
} from '../src/features/agent-runtime/model';
import {
deriveAgentStatusCards,
formatAgentCardRuntimeStatus,
projectAgentRuntimeSummaries,
} from '../src/features/project-summary/agentPresentation';
describe('普通用户工作区状态', () => {
test('用项目名称替代 Unix 和 Windows 绝对路径', () => {
expect(
projectWorkspaceStatusForDisplay('已打开:/tmp/authorized-game'),
).toBe('已打开:authorized-game');
expect(
projectWorkspaceStatusForDisplay('已打开:C:\\projects\\canvas-game'),
).toBe('已打开:canvas-game');
expect(projectWorkspaceStatusForDisplay('正在读取项目')).toBe(
'正在读取项目',
);
});
test.each([
{
name: '普通 Web 工作台',
input: {
workspaceProjectKind: 'web' as const,
orchestrationMode: 'single-supervisor' as const,
supervisorChatOnly: false,
gameChatOnly: false,
},
expected: {
runProfile: 'autonomous-game-build',
source: 'project-supervisor-game-chat',
},
},
{
name: '游戏生成聊天',
input: {
workspaceProjectKind: 'web' as const,
orchestrationMode: 'professional-dag' as const,
supervisorChatOnly: false,
gameChatOnly: true,
},
expected: {
runProfile: 'autonomous-game-build',
source: 'project-supervisor-game-chat',
},
},
{
name: '显式项目总控调试',
input: {
workspaceProjectKind: 'web' as const,
orchestrationMode: 'professional-dag' as const,
supervisorChatOnly: true,
gameChatOnly: false,
},
expected: {
runProfile: 'standard',
source: 'project-supervisor-gui',
},
},
{
name: 'Godot 项目调试',
input: {
workspaceProjectKind: 'godot' as const,
orchestrationMode: 'single-supervisor' as const,
supervisorChatOnly: false,
gameChatOnly: false,
},
expected: {
runProfile: 'standard',
source: 'project-supervisor-gui',
},
},
{
name: '显式专业 DAG Web 工作台',
input: {
workspaceProjectKind: 'web' as const,
orchestrationMode: 'professional-dag' as const,
supervisorChatOnly: false,
gameChatOnly: false,
},
expected: {
runProfile: 'autonomous-game-build',
source: 'project-supervisor-gui',
},
},
])('$name 使用稳定的 Runtime 提交路由', ({ input, expected }) => {
expect(resolveProjectSupervisorRuntimeSubmission(input)).toEqual(expected);
});
});
describe('Agent 最近任务失败摘要', () => {
test('重试已入队时立即投影新 Run 并保留旧失败历史', () => {
const previous: AgentRuntimeState = {
...providerRetryRuntime(),
runId: 'code-failed',
source: 'agent-delegate',
parentAgentId: 'project-supervisor',
parentRunId: 'supervisor-active',
status: 'failed',
phase: 'failed',
error: 'kind=budget-exhausted',
updatedAt: 20,
};
const retryTask: AgentRuntimeTaskRecord = {
schemaVersion: 'game-creator-agent-runtime-task.v1',
agentId: previous.agentId,
taskId: previous.taskId,
sessionId: previous.sessionId,
runId: 'code-retry',
source: 'agent-delegate-retry',
parentAgentId: 'project-supervisor',
parentRunId: 'supervisor-active',
delegationId: 'retry-delegation',
task: previous.currentTask,
status: 'pending',
phase: 'queued',
currentAction: '等待当前后台任务完成',
error: null,
updatedAt: 21,
};
const result: AgentRuntimeResult = {
state: previous,
acceptedRunId: retryTask.runId,
sessionPath: '.agent/runtime/session.json',
eventPath: '.agent/runtime/events.jsonl',
taskPath: '.agent/runtime/tasks.jsonl',
taskQueue: {
total: 2,
pending: 1,
running: 0,
completed: 0,
failed: 1,
latestRunId: retryTask.runId,
updatedAt: 21,
},
recentEvents: [],
recentTasks: [
{
...retryTask,
runId: previous.runId,
source: previous.source,
status: 'failed',
phase: 'failed',
currentAction: '等待开发者处理失败',
terminalDetail: 'kind=budget-exhausted',
error: 'kind=budget-exhausted',
updatedAt: 20,
},
retryTask,
],
};
const projected = agentRuntimeStateFromResult(result, previous);
expect(projected.runId).toBe('code-retry');
expect(projected.source).toBe('agent-delegate-retry');
expect(projected.status).toBe('pending');
expect(projected.phase).toBe('queued');
expect(projected.error).toBeNull();
expect(projected.parentRunId).toBe('supervisor-active');
expect(projected.recentTasks).toEqual(result.recentTasks);
});
test('展示安全可行动原因且不透传私有诊断', () => {
const task: AgentRuntimeTaskRecord = {
schemaVersion: 'game-creator-agent-runtime-task.v1',
agentId: 'code-prototype',
taskId: 'code-prototype',
sessionId: 'session-code',
runId: 'run-code-failed',
source: 'agent-delegate',
task: '实现首个可玩版本',
status: 'failed',
phase: 'failed',
currentAction: '等待开发者处理失败',
terminalDetail:
'kind=codex-app-server-context-window-exceeded fingerprint=' +
'a'.repeat(64),
error: 'private provider body https://provider.example/api?key=secret',
updatedAt: 1,
};
const visible = formatAgentRecentRuntimeTask(task);
expect(visible).toContain(
'失败原因:程序原型 Agent 模型上下文已超限,请缩小任务范围后重试',
);
expect(visible).not.toContain('fingerprint');
expect(visible).not.toContain('provider.example');
expect(visible).not.toContain('secret');
});
test('状态卡在 Runtime error 为空时使用当前 Run 的终态任务原因', () => {
const terminalDetail =
'kind=codex-app-server-context-window-exceeded fingerprint=' +
'b'.repeat(64);
const manifest = createGameCreationAppManifest(
'local-project-draft',
'failure-card-game',
);
const cards = deriveAgentStatusCards(manifest, null, {
'code-prototype': {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'code-prototype',
taskId: 'code-prototype',
sessionId: 'session-code',
runId: 'run-code-failed',
source: 'agent-delegate',
status: 'failed',
phase: 'failed',
currentTask: '实现首个可玩版本',
plan: [],
observations: [],
allowedTools: [],
error: null,
updatedAt: 2,
recentTasks: [
{
schemaVersion: 'game-creator-agent-runtime-task.v1',
agentId: 'code-prototype',
taskId: 'code-prototype',
sessionId: 'session-code',
runId: 'run-code-failed',
source: 'agent-delegate',
task: '实现首个可玩版本',
status: 'failed',
phase: 'failed',
currentAction: '等待开发者处理失败',
terminalDetail,
error: null,
updatedAt: 2,
},
],
},
});
const card = cards.find((candidate) => candidate.id === 'code-prototype');
expect(card?.runtimeError).toBe(terminalDetail);
expect(formatAgentCardRuntimeStatus(card!)).toContain(
'程序原型 Agent 模型上下文已超限,请缩小任务范围后重试',
);
expect(formatAgentCardRuntimeStatus(card!)).not.toContain('fingerprint');
});
});
describe('Runtime-owned public statuses', () => {
test('treats needs-reconciliation as a terminal state that requires manual resolution', () => {
const runtime = {
...providerRetryRuntime(),
status: 'needs-reconciliation',
phase: 'needs-reconciliation',
error: 'result-unknown,需要人工核对',
};
expect(isAgentRuntimeTerminalState(runtime)).toBe(true);
expect(agentRuntimeConversationStatus(runtime)).toBe(
'Agent 运行状态需要核对,正在同步记录',
);
expect(projectSupervisorChatRuntimeStatus(runtime)).toBe(
'项目总控 Agent 运行状态需要核对,请打开运行详情后重试',
);
});
test('keeps backend status messages visible without treating them as client-authored conversation', () => {
const projectRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '任务已接收,项目总控 Agent 正在启动处理。',
agentId: null,
messageId:
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-accepted',
updatedAt: 2,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '普通项目消息',
agentId: null,
messageId: 'project-message-1',
updatedAt: 3,
},
];
const supervisorRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '请继续完成当前游戏',
agentId: 'project-supervisor',
messageId: 'runtime-task-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
updatedAt: 2,
},
];
const messages = mergeProjectSupervisorConversation(
projectRecords,
supervisorRecords,
);
expect(messages).toEqual([
expect.objectContaining({
text: '请继续完成当前游戏',
runtimeOwned: true,
}),
expect.objectContaining({
text: '任务已接收,项目总控 Agent 正在启动处理。',
runtimeOwned: true,
}),
expect.objectContaining({
text: '普通项目消息',
runtimeOwned: false,
}),
]);
});
test('interleaves rapid same-second tasks with their accepted and terminal statuses', () => {
const projectRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '任务已接收,项目总控 Agent 正在启动处理。',
agentId: null,
messageId:
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-accepted',
updatedAt: 9,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '项目总控 Agent 执行失败,请稍后重试',
agentId: null,
messageId:
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-failed',
updatedAt: 9,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '任务已接收,项目总控 Agent 正在启动处理。',
agentId: null,
messageId:
'runtime-public-status-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-accepted',
updatedAt: 9,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '项目总控 Agent 已达到执行预算,请缩小任务范围后重试',
agentId: null,
messageId:
'runtime-public-status-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-budget',
updatedAt: 9,
},
];
const supervisorRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '第一条任务',
agentId: 'project-supervisor',
messageId: 'runtime-task-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
updatedAt: 9,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '第二条任务',
agentId: 'project-supervisor',
messageId: 'runtime-task-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
updatedAt: 9,
},
];
expect(
mergeProjectSupervisorConversation(projectRecords, supervisorRecords).map(
(message) => message.text,
),
).toEqual([
'第一条任务',
'任务已接收,项目总控 Agent 正在启动处理。',
'项目总控 Agent 执行失败,请稍后重试',
'第二条任务',
'任务已接收,项目总控 Agent 正在启动处理。',
'项目总控 Agent 已达到执行预算,请缩小任务范围后重试',
]);
});
test('keeps an older sparse terminal before a new same-second task', () => {
const projectRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '项目总控 Agent 执行失败,请稍后重试',
agentId: null,
messageId:
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-failed',
updatedAt: 9,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: '任务已接收,项目总控 Agent 正在启动处理。',
agentId: null,
messageId:
'runtime-public-status-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-accepted',
updatedAt: 9,
},
];
const supervisorRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '旧任务',
agentId: 'project-supervisor',
messageId: 'runtime-task-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
updatedAt: 8,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '新任务',
agentId: 'project-supervisor',
messageId: 'runtime-task-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
updatedAt: 9,
},
];
expect(
mergeProjectSupervisorConversation(projectRecords, supervisorRecords).map(
(message) => message.text,
),
).toEqual([
'旧任务',
'项目总控 Agent 执行失败,请稍后重试',
'新任务',
'任务已接收,项目总控 Agent 正在启动处理。',
]);
});
test('interleaves same-second steer acknowledgements with their user messages', () => {
const acknowledgement =
'收到。我正在判断这条消息是否需要调整当前任务;现有任务会继续运行。';
const projectRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: acknowledgement,
agentId: null,
messageId:
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-steer-1',
updatedAt: 9,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'assistant',
content: acknowledgement,
agentId: null,
messageId:
'runtime-public-status-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-steer-2',
updatedAt: 9,
},
];
const supervisorRecords: LocalConversationMessageRecord[] = [
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '你在做什么?',
agentId: 'project-supervisor',
messageId: 'agent-steer-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
updatedAt: 9,
},
{
schemaVersion: 'game-creator-conversation.v1',
role: 'user',
content: '先把移动端操作做好。',
agentId: 'project-supervisor',
messageId: 'agent-steer-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
updatedAt: 9,
},
];
expect(
mergeProjectSupervisorConversation(projectRecords, supervisorRecords).map(
(message) => message.text,
),
).toEqual([
'你在做什么?',
acknowledgement,
'先把移动端操作做好。',
acknowledgement,
]);
});
});
describe('Game Chat stream identity and source', () => {
test('response stream identity binds run, slot, and revision', () => {
const base: AgentRuntimeResponseStream = {
schemaVersion: 'game-creator-runtime-response-stream.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'session-1',
runId: 'run-1',
requestKind: 'final-reply',
requestSlot: 'final-reply-loop-1-revision-0',
appliedSteerCursor: 0,
responseRevision: 0,
sequence: 1,
status: 'ready',
accumulatedText: 'same text',
finishReason: 'stop',
startedAt: 1,
updatedAt: 2,
};
expect(projectSupervisorResponseStreamIdentity(base)).toBe(
'run-1\u001ffinal-reply-loop-1-revision-0\u001f0',
);
expect(
projectSupervisorResponseStreamIdentity({ ...base, runId: 'run-2' }),
).not.toBe(projectSupervisorResponseStreamIdentity(base));
expect(
projectSupervisorResponseStreamIdentity({
...base,
requestSlot: 'final-reply-loop-2-revision-0',
}),
).not.toBe(projectSupervisorResponseStreamIdentity(base));
expect(
projectSupervisorResponseStreamIdentity({ ...base, responseRevision: 1 }),
).not.toBe(projectSupervisorResponseStreamIdentity(base));
});
test('Project Supervisor source is explicit for every start and steer', async () => {
const runtimeResult = {
state: {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'session-provider-retry',
runId: 'accepted-run',
source: 'project-supervisor-chat',
status: 'running',
phase: 'planning',
currentTask: null,
currentAction: null,
waitingOn: null,
nextStep: null,
plan: [],
planSteps: [],
observations: [],
allowedTools: [],
lastResponse: null,
error: null,
updatedAt: 1,
},
sessionPath: 'session.json',
eventPath: 'events.jsonl',
} satisfies AgentRuntimeResult;
const invoke = vi.fn(
async (_command: string, _args?: Record<string, unknown>) =>
runtimeResult,
);
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: '/tmp/game-chat',
sessionId: 'session-provider-retry',
prompt: 'make a game',
runtime: null,
runProfile: 'autonomous-game-build',
source: 'project-supervisor-game-chat',
});
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.objectContaining({ source: 'project-supervisor-game-chat' }),
);
invoke.mockClear();
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: '/tmp/game-chat',
sessionId: 'session-provider-retry',
prompt: 'chat',
runtime: null,
runProfile: 'standard',
source: 'project-supervisor-gui',
});
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.objectContaining({ source: 'project-supervisor-gui' }),
);
invoke.mockClear();
const steerRuntime = {
...runtimeResult.state,
runId: 'steer-run',
runProfile: 'autonomous-game-build',
source: 'project-supervisor-game-chat',
status: 'running',
phase: 'planning',
} satisfies AgentRuntimeState;
invoke.mockResolvedValueOnce({
runtime: runtimeResult,
steerId: 'steer-1',
sequence: 1,
status: 'applied',
providerInterrupted: false,
});
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: '/tmp/game-chat',
sessionId: 'session-provider-retry',
prompt: 'continue this run',
runtime: steerRuntime,
runProfile: 'autonomous-game-build',
source: 'project-supervisor-game-chat',
});
expect(invoke).toHaveBeenCalledWith(
'steer_game_creator_agent_runtime_task',
expect.objectContaining({ source: 'project-supervisor-game-chat' }),
);
invoke.mockClear();
invoke.mockResolvedValueOnce(runtimeResult);
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: '/tmp/game-chat',
sessionId: 'session-provider-retry',
prompt: 'continue this run',
runtime: steerRuntime,
runProfile: 'autonomous-game-build',
source: 'project-supervisor-gui',
});
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.objectContaining({ source: 'project-supervisor-gui' }),
);
invoke.mockClear();
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: '/tmp/game-chat',
sessionId: 'session-provider-retry',
prompt: 'continue from the app',
runtime: {
...steerRuntime,
source: 'project-supervisor-cli',
},
runProfile: 'autonomous-game-build',
source: 'project-supervisor-game-chat',
});
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.objectContaining({ source: 'project-supervisor-game-chat' }),
);
});
test('planning entry forwards the exact standard profile and trusted plan source', async () => {
const runtimeResult = {
state: {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'session-plan-entry',
runId: 'plan-run',
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
runProfile: 'standard' as const,
status: 'running',
phase: 'planning',
currentTask: '整理游戏立项需求',
currentAction: '',
plan: [],
observations: [],
allowedTools: [],
lastResponse: null,
error: null,
updatedAt: 1,
},
sessionPath: 'session.json',
eventPath: 'events.jsonl',
} satisfies AgentRuntimeResult;
const invoke = vi.fn(async () => runtimeResult);
await submitProjectSupervisorRuntimeTask({
invoke,
projectPath: '/tmp/plan-entry',
sessionId: 'session-plan-entry',
prompt: '做一个双摇杆动作游戏',
runtime: null,
runProfile: 'standard',
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
});
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.objectContaining({
runProfile: 'standard',
source: PROJECT_SUPERVISOR_PLAN_SOURCE,
}),
);
});
test('game-chat hydration keeps identical text from a different runtime response identity', () => {
const history = [
{
role: 'assistant' as const,
text: 'same text',
messageId: 'persisted-message-1',
updatedAt: 20,
},
];
const pending = [
{
role: 'assistant' as const,
text: 'same text',
messageId: 'runtime-response:run-2\u001fslot-1\u001f0',
updatedAt: 10,
runtimeOwned: true,
},
];
expect(
mergeGameChatRuntimeResponseMessagesIntoHistory(history, pending),
).toEqual([pending[0], history[0]]);
expect(
mergeGameChatRuntimeResponseMessagesIntoHistory(history, [
{ ...pending[0], messageId: 'persisted-message-1' },
]),
).toEqual(history);
});
test('ready response survives either ordering of runtime commit and conversation hydration', () => {
const history = [
{
role: 'user' as const,
text: 'build a game',
messageId: 'persisted-message-1',
updatedAt: 1,
},
];
const ready = {
role: 'assistant' as const,
text: 'ready response',
messageId: 'runtime-response:run-1\u001fslot-1\u001f0',
updatedAt: 2,
runtimeOwned: true,
};
const commitReady = (current: ChatMessage[]) =>
current.some((message) => message.messageId === ready.messageId)
? current
: [...current, ready];
const hydrateConversation = (current: ChatMessage[]) =>
mergeGameChatRuntimeResponseMessagesIntoHistory(history, current);
expect(hydrateConversation(commitReady([]))).toContainEqual(ready);
expect(commitReady(hydrateConversation([]))).toContainEqual(ready);
});
});
function providerRetryRuntime(): AgentRuntimeState {
return {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'task-provider-retry',
sessionId: 'session-provider-retry',
runId: 'run-provider-retry',
source: 'project-supervisor-chat',
status: 'running',
phase: 'waiting-for-provider-retry',
currentTask: '生成项目总控计划',
currentAction: 'Provider 上游返回 HTTP 503,准备自动重试 1/3',
waitingOn: '预计 30 秒后重试',
nextStep: '到期后继续同一 Run',
plan: ['生成项目总控计划'],
planSteps: [
{
step: '这个活跃计划步骤不应覆盖真实 Provider 等待进度',
status: 'in_progress',
index: 0,
},
],
observations: [],
allowedTools: [],
lastResponse: null,
error: null,
updatedAt: 1,
};
}
describe('Agent Runtime Provider 状态投影', () => {
test('单主管来源使用中性的游戏生成状态,GUI 调试来源保留专业任务状态', () => {
const gameChatRuntime = {
...providerRetryRuntime(),
source: 'project-supervisor-game-chat',
phase: 'waiting-for-manifest-tasks',
currentAction: null,
waitingOn: null,
planSteps: [],
};
const guiRuntime = {
...gameChatRuntime,
source: 'project-supervisor-gui',
};
expect(projectSupervisorRuntimeStatusLabel(gameChatRuntime, '')).toBe(
'生成中',
);
expect(projectRuntimeStatusPresentation(gameChatRuntime).label).toBe(
'生成中',
);
expect(projectRuntimeVisibleCurrentWork(gameChatRuntime)).toBe(
'正在推进游戏生成',
);
expect(projectSupervisorRuntimeStatusLabel(guiRuntime, '')).toBe(
'等待项目任务',
);
expect(projectRuntimeVisibleCurrentWork(guiRuntime)).toBe(
'正在等待项目专业任务完成',
);
});
test('等待 503 重试时优先显示 Runtime 的真实动作和等待时间', () => {
const runtime = providerRetryRuntime();
const expected =
'Provider 上游返回 HTTP 503,准备自动重试 1/3;预计 30 秒后重试';
expect(projectSupervisorChatRuntimeStatus(runtime)).toBe(expected);
expect(projectRuntimeVisibleCurrentWork(runtime)).toBe(expected);
});
test('旧等待投影不再显示内部 upstream-5xx 分类', () => {
const legacyRuntime = {
...providerRetryRuntime(),
currentAction: '等待 Provider 瞬态重试 1/3',
waitingOn: 'Provider upstream-5xx 瞬态故障退避到期',
};
const expected = 'Provider 上游服务暂时不可用,准备自动重试 1/3';
expect(projectSupervisorChatRuntimeStatus(legacyRuntime)).toBe(expected);
expect(projectRuntimeVisibleCurrentWork(legacyRuntime)).toBe(expected);
expect(projectSupervisorChatRuntimeStatus(legacyRuntime)).not.toContain(
'upstream-5xx',
);
});
test('严格解析 503 重试耗尽的稳定字段', () => {
const fingerprint = 'a'.repeat(64);
const message =
`agentLlm.project-supervisor 规划调用 LLM 失败:kind=upstream-503 ` +
`httpStatus=503 fingerprint=${fingerprint} chars=248 ` +
'retryAttempt=3 maxRetries=3 retryState=exhausted';
expect(projectRuntimeVisibleError(message, '项目总控 Agent', true)).toBe(
'项目总控 Agent 上游服务返回 HTTP 503;自动重试已耗尽(3/3',
);
const failedRuntime = {
...providerRetryRuntime(),
status: 'failed',
phase: 'failed',
error: message,
};
expect(projectSupervisorChatRuntimeStatus(failedRuntime)).toBe(
'项目总控 Agent 上游服务返回 HTTP 503;自动重试已耗尽(3/3',
);
});
test('明确说明泥点余额不足导致的中断并隐藏附加诊断', () => {
for (const message of [
'agentLlm.project-supervisor 规划调用 LLM 失败:kind=mud-points-insufficient',
'平台图片生成任务失败:泥点余额不足;operationId=private-operation-id',
]) {
expect(projectRuntimeVisibleError(message, '项目总控 Agent', true)).toBe(
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
);
expect(
projectSupervisorVisibleConversationText(`后台任务失败:${message}`),
).toBe(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE);
}
const failedRuntime = {
...providerRetryRuntime(),
status: 'failed',
phase: 'failed',
error:
'平台图片生成任务失败:泥点余额不足;operationId=private-operation-id',
};
const status = projectSupervisorChatRuntimeStatus(failedRuntime);
expect(status).toBe(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE);
expect(status).not.toContain('operationId');
expect(status).not.toContain('private-operation-id');
expect(
projectRuntimeVisibleError('上游余额不足', '项目总控 Agent', true),
).toBe('项目总控 Agent 执行失败,请稍后重试');
});
test('即使前缀含恶意上游正文也只展示严格字段派生的摘要', () => {
const fingerprint = 'b'.repeat(64);
const malicious =
'https://provider.example/private?api_key=sk-secret C:\\Users\\victim\\project';
const message =
`${malicious}kind=upstream-503 httpStatus=503 ` +
`fingerprint=${fingerprint} chars=999 retryAttempt=3 ` +
'maxRetries=3 retryState=exhausted';
const visible = projectRuntimeVisibleError(message, '项目总控 Agent', true);
expect(visible).toBe(
'项目总控 Agent 上游服务返回 HTTP 503;自动重试已耗尽(3/3',
);
expect(visible).not.toContain('provider.example');
expect(visible).not.toContain('sk-secret');
expect(visible).not.toContain('victim');
});
test('拒绝字段不一致或带尾随正文的伪造 Provider 摘要', () => {
const fingerprint = 'c'.repeat(64);
const inconsistent =
`kind=upstream-503 httpStatus=500 fingerprint=${fingerprint} chars=1 ` +
'retryAttempt=3 maxRetries=3 retryState=exhausted';
const trailingBody =
`kind=upstream-503 httpStatus=503 fingerprint=${fingerprint} chars=1 ` +
'retryAttempt=3 maxRetries=3 retryState=exhausted provider body';
expect(
projectRuntimeVisibleError(inconsistent, '项目总控 Agent', true),
).toBe('项目总控 Agent 执行失败,请稍后重试');
expect(
projectRuntimeVisibleError(trailingBody, '项目总控 Agent', true),
).toBe('项目总控 Agent 执行失败,请稍后重试');
});
test('展示 Codex app-server 稳定失败分类且隐藏内部诊断', () => {
const fingerprint = 'e'.repeat(64);
const contextError =
`agentLlm.code-prototype 调用 LLM 失败:` +
`kind=codex-app-server-context-window-exceeded ` +
`fingerprint=${fingerprint} chars=2048`;
const visible = projectRuntimeVisibleError(
contextError,
'程序原型 Agent',
true,
);
expect(visible).toBe(
'程序原型 Agent 模型上下文已超限,请缩小任务范围后重试',
);
expect(visible).not.toContain('fingerprint');
expect(visible).not.toContain(fingerprint);
expect(
projectRuntimeVisibleError(
`kind=codex-app-server-unauthorized fingerprint=${fingerprint} chars=1`,
'项目总控 Agent',
true,
),
).toBe('项目总控 Agent Codex 鉴权失败,请重新登录或检查 API Key');
});
test('直连智能创作的错误码也展示可行动原因并识别终态未知', () => {
expect(
projectRuntimeVisibleError(
'codex-app-server-error:usage-limit-exceeded',
'陶泥儿智能创作',
true,
),
).toBe('陶泥儿智能创作 用量已达上限,请检查账户额度后重试');
expect(
projectRuntimeVisibleError(
'codex-app-server-terminal-unknown: 等待 turn/completed 超时',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作服务或网络在执行中断开,最终状态未知;请先检查项目文件是否已修改,再决定是否重试',
);
});
test('直连智能创作展示脱敏的阶段诊断与重试建议', () => {
expect(
projectRuntimeVisibleError(
'direct-codex-failure:v1 stage=art-preparation retryable=true summary=读取陶泥儿画布资源失败:HTTP 503;建议:平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作:平台资源准备失败:读取陶泥儿画布资源失败:HTTP 503。平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断(可直接重试)',
);
expect(
projectRuntimeVisibleError(
'direct-codex-failure:v1 stage=art-preparation retryable=false summary=陶泥儿画布存在多个同源核心图集,身份不唯一,已拒绝恢复;建议:历史画布资源不满足安全恢复条件,请先在资源画布确认唯一可用的核心图集;已保存脱敏项目诊断',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作:平台资源准备失败:陶泥儿画布存在多个同源核心图集,身份不唯一,已拒绝恢复。历史画布资源不满足安全恢复条件,请先在资源画布确认唯一可用的核心图集',
);
expect(
projectRuntimeVisibleError(
'direct-codex-failure:v1 stage=art-preparation retryable=false summary=本机开发者凭据存储目录未安全初始化;未创建远端凭据;建议:请检查当前 Windows 用户对本机私有凭据目录的权限后重试;已保存脱敏项目诊断',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作:平台资源准备失败:本机开发者凭据存储目录未安全初始化;未创建远端凭据。请检查当前 Windows 用户对本机私有凭据目录的权限后重试',
);
expect(
projectRuntimeVisibleError(
'direct-codex-failure:v1 stage=art-preparation retryable=true summary=读取陶泥儿画布资源失败:<redacted-url> <absolute-path> [redacted-secret];建议:平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作:平台资源准备失败:读取陶泥儿画布资源失败:[已隐藏链接] [已隐藏路径] [已隐藏凭据]。平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断(可直接重试)',
);
});
test('直连 Codex 失败保留安全原因并隐藏链接与路径', () => {
expect(
projectRuntimeVisibleError(
'direct-codex-error:Codex app-server turn 失败:HTTP 400',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作:Codex 执行失败:Codex app-server turn 失败:HTTP 400',
);
expect(
projectRuntimeVisibleError(
'direct-codex-error:请求失败 https://provider.example/private C:\\Users\\private\\project',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作:Codex 执行失败:请求失败 [已隐藏链接] [已隐藏路径]',
);
expect(
projectRuntimeVisibleError(
'Codex 已返回,但客户端登记生成产物失败:游戏代码已生成,但未在实际渲染中同时使用陶泥儿背景图 assets/direct-game-background.png 与四类核心切片;不会将项目标记为完成',
'陶泥儿智能创作',
true,
),
).toBe(
'陶泥儿智能创作:Codex 已返回,但客户端登记生成产物失败:游戏代码已生成,但未在实际渲染中同时使用陶泥儿背景图 assets/direct-game-background.png 与四类核心切片;不会将项目标记为完成',
);
expect(
projectRuntimeVisibleError(
'陶泥儿美术包不完整,已终止代码生成',
'陶泥儿智能创作',
true,
),
).toBe('陶泥儿智能创作:陶泥儿美术包不完整,已终止代码生成');
});
test('直连陶泥儿平台图集失败保留安全可操作原因并隐藏 operationId', () => {
const visible = projectRuntimeVisibleError(
'陶泥儿美术包生成失败(陶泥儿首版核心游戏美术图集),已在启动智能创作前终止:平台图片生成任务失败:game-chat 图集必须由 External Editor 以 grid-2x2 固定切片合同生成;operationId=private-operation-id',
'陶泥儿智能创作',
true,
);
expect(visible).toBe(
'陶泥儿智能创作:陶泥儿美术包生成失败(陶泥儿首版核心游戏美术图集),已在启动智能创作前终止:平台图片生成任务失败:game-chat 图集必须由 External Editor 以 grid-2x2 固定切片合同生成',
);
expect(visible).not.toContain('operationId');
expect(
projectRuntimeVisibleError(
'陶泥儿美术包生成失败:https://provider.example/private?api_key=secret',
'陶泥儿智能创作',
true,
),
).toBe('陶泥儿智能创作 执行失败,请稍后重试');
});
test('把常见 Runtime 失败映射为可行动原因', () => {
expect(
projectRuntimeVisibleError(
'动态隔离子 Agent 缺少 expected artifactgame/index.html',
'程序 Agent',
),
).toBe('程序 Agent 未生成要求的产物,请查看任务要求后重试');
expect(
projectRuntimeVisibleError('project.verify 验证失败', '程序 Agent'),
).toBe('程序 Agent 项目验证未通过,请查看运行详情并修复后重试');
expect(
projectRuntimeVisibleError('Agent loop 预算耗尽', '程序 Agent'),
).toBe('程序 Agent 本轮预算已耗尽,请缩小任务范围后重试');
expect(
projectSupervisorChatRuntimeStatus({
...providerRetryRuntime(),
status: 'needs-reconciliation',
phase: 'needs-reconciliation',
error: 'result-unknown,需要人工核对',
}),
).toBe('项目总控 Agent 运行状态需要核对,请打开运行详情后重试');
});
test('隐藏旧失败对话与事件中的内部诊断标记', () => {
const fingerprint = 'd'.repeat(64);
const legacyConversation =
'后台任务失败:<absolute-path> [redacted sensitive context] ' +
`kind=upstream-503 httpStatus=503 fingerprint=${fingerprint} chars=99 ` +
'retryAttempt=3 maxRetries=3 retryState=exhausted';
expect(projectSupervisorVisibleConversationText(legacyConversation)).toBe(
'项目总控 Agent 上游服务返回 HTTP 503;自动重试已耗尽(3/3',
);
expect(
projectSupervisorVisibleConversationText(legacyConversation, 'user'),
).toBe(legacyConversation);
expect(
projectSupervisorVisibleConversationText(
legacyConversation,
'assistant',
'策划 Agent',
),
).toBe('策划 Agent 上游服务返回 HTTP 503;自动重试已耗尽(3/3');
const formattedEvent = formatAgentRuntimeEvent({
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'task-provider-retry',
sessionId: 'session-provider-retry',
runId: 'run-provider-retry',
source: 'project-supervisor-chat',
eventType: 'turn.failed',
status: 'failed',
phase: 'failed',
summary: 'Agent Runtime 本轮处理失败。',
publicText: '项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
detail:
`<absolute-path> [redacted sensitive context] ` +
`errorSha256=${fingerprint} · errorChars=99`,
updatedAt: 1,
});
expect(formattedEvent).toBe(
'turn.failed · failed / failed · 项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
);
expect(formattedEvent).not.toContain('errorSha256');
expect(formattedEvent).not.toContain(fingerprint);
expect(
formatAgentRuntimeEvent({
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'legacy-private-detail',
sessionId: 'legacy-private-detail-session',
runId: 'legacy-private-detail-run',
source: 'project-supervisor-chat',
eventType: 'error',
status: 'failed',
phase: 'failed',
summary: 'Agent Runtime 本轮处理失败。',
detail: 'private provider body without stable diagnostics marker',
updatedAt: 1,
}),
).toBe('error · failed / failed · Agent Runtime 本轮处理失败。');
const emptySummaryEvent = formatAgentRuntimeEvent({
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'task-provider-retry',
sessionId: 'session-provider-retry',
runId: 'run-provider-retry',
source: 'project-supervisor-chat',
eventType: 'error',
status: 'failed',
phase: 'failed',
summary: '',
detail:
`<absolute-path> [redacted sensitive context] ` +
`fingerprint=${fingerprint} chars=99`,
updatedAt: 1,
});
expect(emptySummaryEvent).toBe(
'error · failed / failed · Agent Runtime 本轮处理失败。',
);
expect(emptySummaryEvent).not.toContain('absolute-path');
expect(emptySummaryEvent).not.toContain(fingerprint);
});
});
function gameChatProjectionRuntime(
overrides: Partial<AgentRuntimeState> = {},
): AgentRuntimeState {
return {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'project-supervisor',
taskId: 'project-supervisor',
sessionId: 'game-chat-root-session',
runId: 'game-chat-root-run',
source: 'project-supervisor-game-chat',
status: 'running',
phase: 'execution',
currentTask: '完成当前游戏创作轮次',
currentAction: '等待单主 Agent 完成',
plan: [],
observations: [],
allowedTools: [],
lastResponse: null,
error: null,
updatedAt: 100,
...overrides,
};
}
function gameChatProjectionMain(
root: AgentRuntimeState,
overrides: Partial<AgentRuntimeState> = {},
) {
return gameChatProjectionRuntime({
agentId: 'code-prototype',
taskId: 'code-prototype',
sessionId: 'game-chat-main-session',
runId: 'game-chat-main-run',
source: 'agent-ready-task-scheduler',
parentAgentId: 'project-supervisor',
parentRunId: root.runId,
currentTask: '审计、接入并验收当前原型',
updatedAt: 110,
...overrides,
});
}
function gameChatProjectionEvent(
runtime: AgentRuntimeState,
overrides: Partial<AgentRuntimeEventRecord>,
): AgentRuntimeEventRecord {
return {
schemaVersion: 'game-creator-runtime-event.v1',
agentId: runtime.agentId,
taskId: runtime.taskId,
sessionId: runtime.sessionId,
runId: runtime.runId,
source: runtime.source,
eventType: 'observation',
status: runtime.status,
phase: 'observation',
summary: 'observation',
detail: null,
updatedAt: runtime.updatedAt,
...overrides,
};
}
describe('Game-chat source-aware runtime projection', () => {
test('projects only the current scheduler main and its nested art children', () => {
const root = gameChatProjectionRuntime();
const main = gameChatProjectionMain(root);
const artDirector = gameChatProjectionRuntime({
agentId: 'art-director',
taskId: 'art-director',
sessionId: 'art-director-session',
runId: 'art-director-run',
source: 'agent-delegate',
parentAgentId: 'code-prototype',
parentRunId: main.runId,
status: 'completed',
phase: 'completed',
updatedAt: 120,
});
const legacyRetry = gameChatProjectionRuntime({
agentId: 'art-asset-plan',
taskId: 'art-asset-plan',
sessionId: 'art-asset-plan-retry-session',
runId: 'art-asset-plan-retry-run',
source: 'agent-delegate-retry',
parentAgentId: 'code-prototype',
parentRunId: main.runId,
status: 'failed',
phase: 'failed',
updatedAt: 130,
});
const oldMain = gameChatProjectionMain(root, {
sessionId: 'old-main-session',
runId: 'old-main-run',
parentRunId: 'old-root-run',
updatedAt: 999,
});
const oldRootDirectArt = gameChatProjectionRuntime({
agentId: 'art-director',
taskId: 'art-director',
sessionId: 'old-root-art-session',
runId: 'old-root-art-run',
source: 'agent-ready-task-scheduler',
parentAgentId: 'project-supervisor',
parentRunId: root.runId,
updatedAt: 998,
});
const fullDagPlaytest = gameChatProjectionRuntime({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: 'full-dag-playtest-session',
runId: 'full-dag-playtest-run',
source: 'agent-ready-task-scheduler',
parentAgentId: 'project-supervisor',
parentRunId: root.runId,
updatedAt: 997,
});
const runtimeMap = {
main,
'main-task-alias': main,
artDirector,
legacyRetry,
oldMain,
oldRootDirectArt,
fullDagPlaytest,
};
const lineage = projectCurrentGameChatRuntimeLineage(root, runtimeMap);
expect(lineage?.main).toBe(main);
expect(lineage?.dynamicArtChildren).toEqual([artDirector, legacyRetry]);
expect(lineage?.hasConflict).toBe(false);
expect(gameChatRuntimeClaimsDynamicArtLineage(artDirector)).toBe(true);
expect(gameChatRuntimeClaimsDynamicArtLineage(legacyRetry)).toBe(true);
expect(gameChatRuntimeClaimsDynamicArtLineage(oldRootDirectArt)).toBe(
false,
);
expect(
projectSupervisorCollaboratingAgentRuntimes(root, runtimeMap),
).toEqual([main, artDirector, legacyRetry]);
const malformedGameChatRoot = {
...root,
parentAgentId: 'project-supervisor',
parentRunId: 'ancestor-run',
};
expect(
projectSupervisorCollaboratingAgentRuntimes(malformedGameChatRoot, {
directDelegate: {
...artDirector,
parentAgentId: 'project-supervisor',
parentRunId: malformedGameChatRoot.runId,
},
}),
).toEqual([]);
});
test('projects current game-chat main and nested art into launcher summaries', () => {
const root = gameChatProjectionRuntime();
const main = gameChatProjectionMain(root);
const art = gameChatProjectionRuntime({
agentId: 'art-director',
taskId: 'art-director',
sessionId: 'launcher-art-session',
runId: 'launcher-art-run',
source: 'agent-delegate',
parentAgentId: 'code-prototype',
parentRunId: main.runId,
status: 'completed',
phase: 'completed',
});
const manifest = createGameCreationAppManifest(
'launcher-game-chat',
'Launcher 单主摘要',
);
expect(projectAgentRuntimeSummaries(manifest, root, { main, art })).toEqual(
[
expect.objectContaining({
group: 'art',
label: '美术 Agent',
status: 'completed',
}),
expect.objectContaining({
group: 'code',
label: '程序 Agent',
status: 'running',
}),
],
);
expect(
projectAgentRuntimeSummaries(
manifest,
{ ...root, parentAgentId: 'project-supervisor' },
{ main, art },
),
).toEqual([]);
});
test('uses only the current main for the 0/1 progress denominator', () => {
const root = gameChatProjectionRuntime();
const activeMain = gameChatProjectionMain(root);
const manifest = createGameCreationAppManifest('project-1', '单主进度');
manifest.tasks = manifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
expect(
projectGameChatPrimaryProgress(
manifest,
projectCurrentGameChatRuntimeLineage(root, {}),
),
).toEqual({ completed: 0, total: 1, status: 'pending' });
expect(
projectGameChatPrimaryProgress(
manifest,
projectCurrentGameChatRuntimeLineage(root, [activeMain]),
),
).toEqual({ completed: 0, total: 1, status: 'running' });
const completedMain = {
...activeMain,
status: 'completed',
phase: 'completed',
};
expect(
projectGameChatPrimaryProgress(
manifest,
projectCurrentGameChatRuntimeLineage(root, [completedMain]),
),
).toEqual({ completed: 1, total: 1, status: 'completed' });
const failedMain = {
...activeMain,
status: 'failed',
phase: 'failed',
};
expect(
projectGameChatPrimaryProgress(
manifest,
projectCurrentGameChatRuntimeLineage(root, [failedMain]),
),
).toEqual({ completed: 0, total: 1, status: 'failed' });
const reconciliationRoot = {
...root,
status: 'needs-reconciliation',
phase: 'needs-reconciliation',
};
expect(
projectGameChatPrimaryProgress(
manifest,
projectCurrentGameChatRuntimeLineage(reconciliationRoot, []),
),
).toEqual({
completed: 0,
total: 1,
status: 'needs-reconciliation',
});
const conflictingMain = gameChatProjectionMain(root, {
sessionId: 'conflicting-main-session',
runId: 'conflicting-main-run',
});
expect(
projectGameChatPrimaryProgress(
manifest,
projectCurrentGameChatRuntimeLineage(root, [
activeMain,
conflictingMain,
]),
),
).toEqual({
completed: 0,
total: 1,
status: 'needs-reconciliation',
});
});
test('keeps an unbound manifest out of the verdict until the current main is terminal', () => {
const root = gameChatProjectionRuntime();
const activeMain = gameChatProjectionMain(root);
// The manifest carries no root/run binding, so a `failed` value cannot be
// attributed to any particular round. A leftover from an earlier round must
// not decide the verdict while this round's main is still running.
const staleFailedManifest = createGameCreationAppManifest(
'project-1',
'跨轮残留清单',
);
staleFailedManifest.tasks = staleFailedManifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'failed' as const }
: task,
);
expect(
projectGameChatPrimaryProgress(
staleFailedManifest,
projectCurrentGameChatRuntimeLineage(root, [activeMain]),
),
).toEqual({ completed: 0, total: 1, status: 'running' });
expect(
projectGameChatPrimaryProgress(
staleFailedManifest,
projectCurrentGameChatRuntimeLineage(root, [
{ ...activeMain, phase: 'waiting-for-delegate-receipts' },
]),
),
).toEqual({ completed: 0, total: 1, status: 'running' });
// Mirror gate: a terminal manifest alone must not unlock archiving either.
const terminalRoot = gameChatProjectionRuntime({
status: 'completed',
phase: 'completed',
});
const terminalManifest = createGameCreationAppManifest(
'project-1',
'归档不得只看清单',
);
terminalManifest.tasks = terminalManifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
expect(
canArchiveGameChatStage(
projectCurrentGameChatRuntimeLineage(terminalRoot, [
gameChatProjectionMain(terminalRoot),
]),
terminalManifest,
),
).toBe(false);
// Recorded residual risk (M0B-2 deferred item): once the current main is
// terminal the manifest is trusted verbatim, so a cross-round leftover is
// still reported as this round's failure. Changing this expectation means
// the deferred ruling was revisited — update the decision log with it.
expect(
projectGameChatPrimaryProgress(
staleFailedManifest,
projectCurrentGameChatRuntimeLineage(root, [
{ ...activeMain, status: 'completed', phase: 'completed' },
]),
),
).toEqual({ completed: 0, total: 1, status: 'failed' });
});
test('derives a playable revision only from current-main smoke then structured preview evidence', () => {
const root = gameChatProjectionRuntime();
const main = gameChatProjectionMain(root, { recentEvents: [] });
const smoke = gameChatProjectionEvent(main, {
summary: 'command.run_limitedok · game.static_smoke 已完成',
updatedAt: 200,
});
const previewPassed = gameChatProjectionEvent(main, {
summary: 'preview.validateok · 浏览器验证已通过',
detail: JSON.stringify({
passed: true,
playtestPassed: true,
revision: 7,
}),
updatedAt: 201,
});
main.recentEvents = [smoke, previewPassed];
const lineage = projectCurrentGameChatRuntimeLineage(root, [main]);
expect(latestGameChatPlayableRevision(lineage)).toEqual({
runId: root.runId,
mainRunId: main.runId,
revision: 7,
validatedAt: 201,
});
const laterFailure = gameChatProjectionEvent(main, {
summary: 'preview.validatefailed · 后续回归',
detail: JSON.stringify({
passed: false,
playtestPassed: false,
revision: 7,
}),
updatedAt: 202,
});
main.recentEvents = [smoke, previewPassed, laterFailure];
expect(
latestGameChatPlayableRevision(
projectCurrentGameChatRuntimeLineage(root, [main]),
),
).toBeNull();
main.recentEvents = [previewPassed];
expect(
latestGameChatPlayableRevision(
projectCurrentGameChatRuntimeLineage(root, [main]),
),
).toBeNull();
const oldPlaytest = gameChatProjectionRuntime({
agentId: 'preview-playtest',
taskId: 'preview-playtest',
sessionId: 'old-playtest-session',
runId: 'old-playtest-run',
source: 'agent-ready-task-scheduler',
parentAgentId: 'project-supervisor',
parentRunId: root.runId,
recentEvents: [previewPassed],
});
expect(
latestGameChatPlayableRevision(
projectCurrentGameChatRuntimeLineage(root, [oldPlaytest]),
),
).toBeNull();
main.recentEvents = [previewPassed, { ...smoke, updatedAt: 202 }];
expect(
latestGameChatPlayableRevision(
projectCurrentGameChatRuntimeLineage(root, [main]),
),
).toBeNull();
main.recentEvents = [smoke, { ...previewPassed, updatedAt: -1 }];
expect(
latestGameChatPlayableRevision(
projectCurrentGameChatRuntimeLineage(root, [main]),
),
).toBeNull();
});
test('archives only after root, main, dynamic children and manifest have converged', () => {
const root = gameChatProjectionRuntime({
status: 'completed',
phase: 'completed',
});
const main = gameChatProjectionMain(root, {
status: 'completed',
phase: 'completed',
});
const art = gameChatProjectionRuntime({
agentId: 'art-director',
taskId: 'art-director',
sessionId: 'archive-art-session',
runId: 'archive-art-run',
source: 'agent-delegate',
parentAgentId: 'code-prototype',
parentRunId: main.runId,
status: 'completed',
phase: 'completed',
});
const manifest = createGameCreationAppManifest('project-2', '归档门');
manifest.tasks = manifest.tasks.map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const }
: task,
);
expect(
canArchiveGameChatStage(
projectCurrentGameChatRuntimeLineage(root, [main, art]),
manifest,
),
).toBe(true);
const activeArt = { ...art, status: 'running', phase: 'execution' };
expect(
canArchiveGameChatStage(
projectCurrentGameChatRuntimeLineage(root, [main, activeArt]),
manifest,
),
).toBe(false);
const waitingMain = {
...main,
status: 'running',
phase: 'waiting-for-delegate-receipts',
};
expect(
canArchiveGameChatStage(
projectCurrentGameChatRuntimeLineage(root, [waitingMain, art]),
manifest,
),
).toBe(false);
const activeRoot = { ...root, status: 'running', phase: 'execution' };
expect(
canArchiveGameChatStage(
projectCurrentGameChatRuntimeLineage(activeRoot, [
{ ...main, parentRunId: activeRoot.runId },
art,
]),
manifest,
),
).toBe(false);
const reconciliationArt = {
...art,
status: 'needs-reconciliation',
phase: 'needs-reconciliation',
};
expect(
canArchiveGameChatStage(
projectCurrentGameChatRuntimeLineage(root, [main, reconciliationArt]),
manifest,
),
).toBe(false);
const pendingManifest = createGameCreationAppManifest(
'project-2',
'归档门未收敛',
);
expect(
canArchiveGameChatStage(
projectCurrentGameChatRuntimeLineage(root, [main, art]),
pendingManifest,
),
).toBe(false);
});
});