import { describe, expect, test } from 'vitest'; import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import type { AgentRuntimeResult, AgentRuntimeState, AgentRuntimeTaskRecord, LocalConversationMessageRecord, } from '../src/app/types'; import { agentRuntimeConversationStatus, agentRuntimeStateFromResult, formatAgentRecentRuntimeTask, formatAgentRuntimeEvent, isAgentRuntimeTerminalState, mergeProjectSupervisorConversation, MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE, projectRuntimeVisibleCurrentWork, projectRuntimeVisibleError, projectSupervisorChatRuntimeStatus, projectSupervisorVisibleConversationText, projectWorkspaceStatusForDisplay, resolveProjectSupervisorRuntimeSubmission, } from '../src/features/agent-runtime/model'; import { deriveAgentStatusCards, formatAgentCardRuntimeStatus, formatCodexRuntimeCapabilities, } from '../src/features/project-summary/agentPresentation'; 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('普通用户工作区状态', () => { test('官方智能服务状态跟随流式与联网检索设置', () => { expect( formatCodexRuntimeCapabilities({ agentMode: 'codex_app_server', stream: true, webSearchEnabled: true, }), ).toBe('流式开启,联网检索开启'); expect( formatCodexRuntimeCapabilities({ agentMode: 'codex_app_server', stream: false, webSearchEnabled: false, }), ).toBe('流式关闭,联网检索关闭'); expect( formatCodexRuntimeCapabilities({ agentMode: 'codex_cli', stream: true, webSearchEnabled: true, }), ).toBe('流式开启,联网检索关闭'); }); 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, }, expected: { runProfile: 'standard', source: 'project-supervisor-gui', }, }, { name: '显式项目总控调试', input: { workspaceProjectKind: 'web' as const, orchestrationMode: 'professional-dag' as const, supervisorChatOnly: true, }, expected: { runProfile: 'standard', source: 'project-supervisor-gui', }, }, { name: 'Godot 项目调试', input: { workspaceProjectKind: 'godot' as const, orchestrationMode: 'single-supervisor' as const, supervisorChatOnly: false, }, expected: { runProfile: 'standard', source: 'project-supervisor-gui', }, }, { name: '显式专业 DAG Web 工作台', input: { workspaceProjectKind: 'web' as const, orchestrationMode: 'professional-dag' as const, supervisorChatOnly: 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('Agent Runtime Provider 状态投影', () => { test('等待 503 重试时优先显示 Runtime 的真实动作和等待时间', () => { const runtime = providerRetryRuntime(); const expected = '智能服务 上游返回 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 = '智能服务暂时不可用,准备自动重试 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 智能服务鉴权失败,请重新登录后重试'); }); test('直连智能创作的错误码也展示可行动原因并识别终态未知', () => { expect( projectRuntimeVisibleError( 'codex-app-server-error:usage-limit-exceeded', '陶泥儿智能创作', true, ), ).toBe('陶泥儿智能创作 用量已达上限,请检查账户额度后重试'); expect( projectRuntimeVisibleError( 'codex-app-server-error:request-too-large', '陶泥儿智能创作', 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-secret];建议:平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断;已保存脱敏项目诊断', '陶泥儿智能创作', true, ), ).toBe( '陶泥儿智能创作:平台资源准备失败:读取陶泥儿画布资源失败:[已隐藏链接] [已隐藏路径] [已隐藏凭据]。平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断(可直接重试)', ); expect( projectRuntimeVisibleError( 'direct-codex-failure:v1 stage=art-preparation retryable=true summary=请求失败 authorization= [redacted-config] token=[redacted-sensitive-field];建议:请检查已隐藏配置并稍后重试;已保存脱敏项目诊断', '陶泥儿智能创作', true, ), ).toBe( '陶泥儿智能创作:平台资源准备失败:请求失败 authorization=[已隐藏链接] [已隐藏配置] token=[已隐藏敏感字段]。请检查已隐藏配置并稍后重试(可直接重试)', ); }); test('直连平台错误保留 HTTP 诊断字段但只显示已脱敏的敏感值', () => { const safe = projectRuntimeVisibleError( '陶泥儿美术包生成失败(规范图):请求平台图片生成失败:HTTP 401;code=invalid-token;field=authorization;message=token=[redacted-secret];detail=登录态已失效', '陶泥儿智能创作', true, ); expect(safe).toContain('HTTP 401'); expect(safe).toContain('code=invalid-token'); expect(safe).toContain('field=authorization'); expect(safe).toContain('登录态已失效'); expect(safe).toContain('[已隐藏凭据]'); expect(safe).not.toContain('[redacted sensitive context]'); const unsafe = projectRuntimeVisibleError( '陶泥儿美术包生成失败(规范图):请求平台图片生成失败:HTTP 401;code=invalid-token;field=authorization;message=token=raw-secret-value', '陶泥儿智能创作', true, ); expect(unsafe).not.toContain('raw-secret-value'); expect(unsafe).toBe('陶泥儿智能创作 鉴权失败,请检查运行时配置'); const unsafeBearer = projectRuntimeVisibleError( '陶泥儿美术包生成失败(规范图):请求平台图片生成失败:HTTP 401;bearer=raw-bearer-value', '陶泥儿智能创作', true, ); expect(unsafeBearer).not.toContain('raw-bearer-value'); expect(unsafeBearer).toBe('陶泥儿智能创作 鉴权失败,请检查运行时配置'); const unsafeEscapedJson = projectRuntimeVisibleError( String.raw`陶泥儿美术包生成失败(规范图):请求平台图片生成失败:HTTP 401;payload={\"token\":\"raw-escaped-json-value\"}`, '陶泥儿智能创作', true, ); expect(unsafeEscapedJson).not.toContain('raw-escaped-json-value'); expect(unsafeEscapedJson).toBe('陶泥儿智能创作 鉴权失败,请检查运行时配置'); const markerSafe = projectRuntimeVisibleError( '陶泥儿美术包生成失败(规范图):请求平台图片生成失败:HTTP 403;detail=authorization=[redacted-config];field=[redacted-sensitive-field];path=', '陶泥儿智能创作', true, ); expect(markerSafe).toContain('HTTP 403'); expect(markerSafe).toContain('authorization=[已隐藏配置]'); expect(markerSafe).toContain('field=[已隐藏敏感字段]'); expect(markerSafe).toContain('path=[已隐藏路径]'); expect(markerSafe).not.toContain('[redacted-sensitive-field]'); expect(markerSafe).not.toBe('陶泥儿智能创作 鉴权失败,请检查运行时配置'); }); test('直连 Codex 失败保留安全原因并隐藏链接与路径', () => { expect( projectRuntimeVisibleError( 'direct-codex-error:Codex app-server turn 失败:HTTP 400', '陶泥儿智能创作', true, ), ).toBe( '陶泥儿智能创作:智能服务执行失败:Codex app-server turn 失败:HTTP 400', ); expect( projectRuntimeVisibleError( 'direct-codex-error:请求失败 https://provider.example/private C:\\Users\\private\\project', '陶泥儿智能创作', true, ), ).toBe( '陶泥儿智能创作:智能服务执行失败:请求失败 [已隐藏链接] [已隐藏路径]', ); expect( projectRuntimeVisibleError( 'Codex 已返回,但客户端登记生成产物失败:游戏代码已生成,但未在实际渲染中同时使用陶泥儿背景图 assets/direct-game-background.png 与四类核心切片;不会将项目标记为完成', '陶泥儿智能创作', true, ), ).toBe( '陶泥儿智能创作:Codex 已返回,但客户端登记生成产物失败:游戏代码已生成,但未在实际渲染中同时使用陶泥儿背景图 assets/direct-game-background.png 与四类核心切片;不会将项目标记为完成', ); expect( projectRuntimeVisibleError( '陶泥儿美术包不完整,已终止代码生成', '陶泥儿智能创作', 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 = '后台任务失败: [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: ` [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: ` [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); }); });