c2d5273873
上次合并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>
1738 lines
58 KiB
TypeScript
1738 lines
58 KiB
TypeScript
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 artifact:game/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_limited:ok · game.static_smoke 已完成',
|
||
updatedAt: 200,
|
||
});
|
||
const previewPassed = gameChatProjectionEvent(main, {
|
||
summary: 'preview.validate:ok · 浏览器验证已通过',
|
||
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.validate:failed · 后续回归',
|
||
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);
|
||
});
|
||
});
|