Files
Genarrative/apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts
T
lhk229 0dc24a4c43 合并最新 master 到 M0 分支
冲突解决:

- provider_request_builders:M0A-2 的固定 owner 收束边界与 master 新增的
  动态目标验收图改在同一段 prompt 构建。保留 owner 分支(固定 owner 仍不
  广告 project.verify / command.run_limited),把 master 的动态目标分支提到
  owner 判断之外对两条路径生效(固定 owner 属 goal_contract_participant,
  本就该收到禁止调用 agent.goal_contract 的边界声明),git 提交协议改为仅
  非 owner 路径追加以维持 M0A-2 边界。工具裁剪三个块并列保留,双方新增
  测试全部保留。
- SupervisorChatOnlyView:保留 M0B-2 的谱系分支,采纳 master 的
  projectWorkspaceStatusForDisplay 包装,避免该 import 变成死引用。
- runtime_actions:两侧各加一个导出,取并集。
- main_loop_tests:保留 M0B-1 需要的项目预初始化与 runtime lane。
- decision-log / pitfalls:双方条目都保留,各自维持原序。

验证:cargo check --all-targets 通过;前端 typecheck 干净;
provider_request_builders / game_chat 委派 / goal-control 共 17 条 Rust
用例通过;agentRuntimeModel 与 appSurface 共 391 条前端用例通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 03:48:19 +00:00

1218 lines
39 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 type {
AgentRuntimeEventRecord,
AgentRuntimeResponseStream,
AgentRuntimeResult,
AgentRuntimeState,
ChatMessage,
LocalConversationMessageRecord,
} from '../src/app/types';
import {
canArchiveGameChatStage,
gameChatRuntimeClaimsDynamicArtLineage,
latestGameChatPlayableRevision,
projectCurrentGameChatRuntimeLineage,
projectGameChatPrimaryProgress,
} from '../src/features/agent-runtime/gameChatRuntimeProjection';
import {
formatAgentRuntimeEvent,
mergeGameChatRuntimeResponseMessagesIntoHistory,
mergeProjectSupervisorConversation,
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
projectRuntimeVisibleCurrentWork,
projectRuntimeVisibleError,
projectSupervisorChatRuntimeStatus,
projectSupervisorCollaboratingAgentRuntimes,
projectSupervisorResponseStreamIdentity,
projectSupervisorVisibleConversationText,
projectWorkspaceStatusForDisplay,
submitProjectSupervisorRuntimeTask,
} from '../src/features/agent-runtime/model';
import { 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(
'正在读取项目',
);
});
});
describe('Runtime-owned public statuses', () => {
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('game-chat source is forwarded for start and steer without changing default source behavior', 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',
});
expect(invoke.mock.calls[0]?.[1]).not.toHaveProperty('source');
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({
runtime: runtimeResult,
steerId: 'steer-2',
sequence: 2,
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',
});
expect(invoke.mock.calls[0]?.[1]).not.toHaveProperty('source');
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('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('等待 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('隐藏旧失败对话与事件中的内部诊断标记', () => {
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);
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 本轮处理失败。',
detail:
`<absolute-path> [redacted sensitive context] ` +
`errorSha256=${fingerprint} · errorChars=99`,
updatedAt: 1,
});
expect(formattedEvent).toBe(
'turn.failed · failed / failed · Agent Runtime 本轮处理失败。',
);
expect(formattedEvent).not.toContain('errorSha256');
expect(formattedEvent).not.toContain(fingerprint);
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);
});
});