b42966eb9e
- `projectRuntimeVisibleError` 增加宿主 `Display` 事实句模式:`执行通道已断开`(TransportClosed)、`等待模型回合结束达到硬上限`(TimedOut 的硬上限那档)、`宿主任务提前结束`(host-dropped),并给落盘那档补上 `收尾历史失败` / `未确认历史完整落盘` / `写入本项目对话历史失败`——改动前这几种都掉进「执行失败,请稍后重试」 - 只认宿主写死的短语、不回落原文:原文带 `exitStatus=` / `stderrClass=` 这类内部字段,`TransportClosed` 就是这种 - 修掉宿主收口文案的版本口径:解析只认 `v1`,而宿主发的是多一段 `code=` 的 `v2`,于是脱敏摘要永远命中不了;现在两版都认,并把解析结果拆成 parts,供拒单文案复用(`projectRuntimeVisibleRejectionError`,不带阶段标签——拒单这一轮没有开始) - `directTurnFailureNoticeText` 的文档注释写明「不加模式就只会看到通用文案」是有意取舍,加模式时补 `agentRuntimeModel.test.ts` 用例 - 用例:`agentRuntimeModel.test.ts` 补 v2 收口文案、拒单文案与三句宿主事实句;`directThreadChat.test.ts` 的「收尾历史失败」期望改成映射后的句子
695 lines
28 KiB
TypeScript
695 lines
28 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
||
|
||
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import type {
|
||
AgentRuntimeResult,
|
||
AgentRuntimeState,
|
||
AgentRuntimeTaskRecord,
|
||
} from '../src/app/types';
|
||
import {
|
||
agentRuntimeConversationStatus,
|
||
agentRuntimeStateFromResult,
|
||
formatAgentRecentRuntimeTask,
|
||
formatAgentRuntimeEvent,
|
||
isAgentRuntimeTerminalState,
|
||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||
projectRuntimeVisibleCurrentWork,
|
||
projectRuntimeVisibleError,
|
||
projectRuntimeVisibleRejectionError,
|
||
} 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: 'planning-agent-v2',
|
||
taskId: 'task-provider-retry',
|
||
sessionId: 'session-provider-retry',
|
||
runId: 'run-provider-retry',
|
||
source: 'planning-gui',
|
||
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('流式开启,联网检索关闭');
|
||
});
|
||
});
|
||
|
||
describe('Agent 最近任务失败摘要', () => {
|
||
test('重试已入队时立即投影新 Run 并保留旧失败历史', () => {
|
||
const previous: AgentRuntimeState = {
|
||
...providerRetryRuntime(),
|
||
runId: 'code-failed',
|
||
source: 'agent-delegate',
|
||
parentAgentId: 'planning-agent-v2',
|
||
parentRunId: 'planning-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: 'planning-agent-v2',
|
||
parentRunId: 'planning-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('planning-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 运行状态需要核对,正在同步记录',
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('Agent Runtime Provider 状态投影', () => {
|
||
test('等待 503 重试时优先显示 Runtime 的真实动作和等待时间', () => {
|
||
const runtime = providerRetryRuntime();
|
||
const expected =
|
||
'智能服务 上游返回 HTTP 503,准备自动重试 1/3;预计 30 秒后重试';
|
||
|
||
expect(projectRuntimeVisibleCurrentWork(runtime)).toBe(expected);
|
||
});
|
||
|
||
test('旧等待投影不再显示内部 upstream-5xx 分类', () => {
|
||
const legacyRuntime = {
|
||
...providerRetryRuntime(),
|
||
currentAction: '等待 Provider 瞬态重试 1/3',
|
||
waitingOn: 'Provider upstream-5xx 瞬态故障退避到期',
|
||
};
|
||
const expected = '智能服务暂时不可用,准备自动重试 1/3';
|
||
|
||
expect(projectRuntimeVisibleCurrentWork(legacyRuntime)).toBe(expected);
|
||
expect(projectRuntimeVisibleCurrentWork(legacyRuntime)).not.toContain(
|
||
'upstream-5xx',
|
||
);
|
||
});
|
||
|
||
test('严格解析 503 重试耗尽的稳定字段', () => {
|
||
const fingerprint = 'a'.repeat(64);
|
||
const message =
|
||
`agentLlm.planning-agent-v2 规划调用 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)',
|
||
);
|
||
});
|
||
|
||
test('明确说明泥点余额不足导致的中断并隐藏附加诊断', () => {
|
||
for (const message of [
|
||
'agentLlm.planning-agent-v2 规划调用 LLM 失败:kind=mud-points-insufficient',
|
||
'平台图片生成任务失败:泥点余额不足;operationId=private-operation-id',
|
||
]) {
|
||
expect(projectRuntimeVisibleError(message, '项目总控 Agent', true)).toBe(
|
||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||
);
|
||
}
|
||
|
||
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-url> <absolute-path> [redacted-secret];建议:平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe(
|
||
'陶泥儿智能创作:平台资源准备失败:读取陶泥儿画布资源失败:[已隐藏链接] [已隐藏路径] [已隐藏凭据]。平台资源暂时无法完成准备,请稍后重试;如持续失败请检查项目诊断(可直接重试)',
|
||
);
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'direct-codex-failure:v1 stage=art-preparation retryable=true summary=请求失败 authorization=<redacted-url> [redacted-config] token=[redacted-sensitive-field];建议:请检查已隐藏配置并稍后重试;已保存脱敏项目诊断',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe(
|
||
'陶泥儿智能创作:平台资源准备失败:请求失败 authorization=[已隐藏链接] [已隐藏配置] token=[已隐藏敏感字段]。请检查已隐藏配置并稍后重试(可直接重试)',
|
||
);
|
||
});
|
||
|
||
test('直连阶段收口文案的 v2 形状也要认出脱敏摘要', () => {
|
||
// 宿主发的是 v2(比 v1 多一段 `code=`):只认 v1 时这一路永远命中不了,脱敏摘要等于白写。
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'direct-codex-failure:v2 stage=code-generation code=runtime-failure retryable=false summary=DirectProject 收尾历史失败:未确认历史完整落盘;建议:请检查项目目录后重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe(
|
||
'陶泥儿智能创作:代码生成失败:DirectProject 收尾历史失败:未确认历史完整落盘。请检查项目目录后重试;如持续失败请检查项目诊断',
|
||
);
|
||
// 脱敏标记照旧要换成中文标记,带路径 / 凭据的摘要照样拒收。
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'direct-codex-failure:v2 stage=art-preparation code=runtime-failure retryable=true summary=读取陶泥儿画布资源失败:<redacted-url>;建议:请稍后重试;已保存脱敏项目诊断',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe(
|
||
'陶泥儿智能创作:平台资源准备失败:读取陶泥儿画布资源失败:[已隐藏链接]。请稍后重试(可直接重试)',
|
||
);
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'direct-codex-failure:v2 stage=art-preparation code=runtime-failure retryable=true summary=读取失败:https://provider.example/private;建议:请稍后重试;已保存脱敏项目诊断',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe('陶泥儿智能创作 执行失败,请稍后重试');
|
||
});
|
||
|
||
test('拒单文案只取收口文案里的脱敏摘要与建议,不套阶段标签', () => {
|
||
// 阶段说的是"失败发生在交付的哪一步",而拒单是"这一轮没有开始":阶段只会是默认值,
|
||
// 套上去会把没发生的事讲成发生了。
|
||
expect(
|
||
projectRuntimeVisibleRejectionError(
|
||
'direct-codex-failure:v2 stage=code-generation code=runtime-failure retryable=true summary=Codex app-server 启动失败:找不到可执行文件;建议:请重试;如持续失败请检查项目诊断;已保存脱敏项目诊断',
|
||
'陶泥儿智能创作',
|
||
),
|
||
).toBe(
|
||
'陶泥儿智能创作:Codex app-server 启动失败:找不到可执行文件。请重试;如持续失败请检查项目诊断(可直接重试)',
|
||
);
|
||
// 不是收口形状时退回同一份运行错误映射(宿主 `Display` 的事实句仍然只给一句可读的话)。
|
||
expect(
|
||
projectRuntimeVisibleRejectionError(
|
||
'执行通道已断开,不能自动重放未确认操作:Codex app-server 已退出;exitStatus=signal: 9 (SIGKILL)',
|
||
'陶泥儿智能创作',
|
||
),
|
||
).toBe('陶泥儿智能创作 服务连接已断开,请稍后重试');
|
||
// 已知边界(本轮不动):映射里"拒绝"那条子串分支会先认领"拒绝访问"这类文件系统事实;
|
||
// 目录锚不定的拒单在聊天里走 `Display` 原样显示(它不在上报名单里),所以摸不到这句。
|
||
expect(
|
||
projectRuntimeVisibleRejectionError(
|
||
'无法锚定 Direct 调用项目目录:拒绝访问',
|
||
'陶泥儿智能创作',
|
||
),
|
||
).toBe('陶泥儿智能创作 被项目权限或安全策略阻止,请检查审批配置');
|
||
});
|
||
|
||
test('宿主 `Display` 的事实句不再掉进通用兜底', () => {
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'执行通道已断开,不能自动重放未确认操作:Codex app-server 已退出;exitStatus=signal: 9 (SIGKILL) stderrClass=crash',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe('陶泥儿智能创作 服务连接已断开,请稍后重试');
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'等待模型回合结束达到硬上限,已停止本轮并核对后台操作。',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe('陶泥儿智能创作 响应超时,请稍后重试');
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'陶泥儿回合的宿主任务提前结束(崩溃或任务被取消),本轮已按失败收口,请重试。',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe('陶泥儿智能创作 本轮执行已中断,请重试');
|
||
expect(
|
||
projectRuntimeVisibleError(
|
||
'DirectProject 收尾历史失败:未确认历史完整落盘',
|
||
'陶泥儿智能创作',
|
||
true,
|
||
),
|
||
).toBe('陶泥儿智能创作 保存运行记录失败,请检查项目目录后重试');
|
||
});
|
||
|
||
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=<absolute-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 本轮预算已耗尽,请缩小任务范围后重试');
|
||
});
|
||
|
||
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';
|
||
const formattedEvent = formatAgentRuntimeEvent({
|
||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||
agentId: 'planning-agent-v2',
|
||
taskId: 'task-provider-retry',
|
||
sessionId: 'session-provider-retry',
|
||
runId: 'run-provider-retry',
|
||
source: 'planning-gui',
|
||
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: 'planning-agent-v2',
|
||
taskId: 'legacy-private-detail',
|
||
sessionId: 'legacy-private-detail-session',
|
||
runId: 'legacy-private-detail-run',
|
||
source: 'planning-gui',
|
||
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: 'planning-agent-v2',
|
||
taskId: 'task-provider-retry',
|
||
sessionId: 'session-provider-retry',
|
||
runId: 'run-provider-retry',
|
||
source: 'planning-gui',
|
||
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);
|
||
});
|
||
});
|