From 7b1f7bdf17fa48a6a36d7877e0f46f8311a81ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 21 Sep 2026 11:36:02 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=99=20parseAgentRunTrace=20=E4=B8=8E=20fo?= =?UTF-8?q?rmatAgentRunStatus=20=E8=A1=A5=E4=B8=8A=E7=BA=AF=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tests/agentRunTrace.test.ts,用旧 trace 夹具断言归一化结果(路径/工具数组/上限/任务图/通行计划) - 覆盖 toolCalls 总数回填、taskGraph 缺失时回退种子任务图 - 覆盖畸形 trace 的拒绝路径(steps / schemaVersion / artifacts / taskGraph / dependencyWaves) - 覆盖 formatAgentRunStatus 带与不带 lifecycleStatus 的两种文案 --- .../tests/agentRunTrace.test.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 apps/ai-game-creator-shell/tests/agentRunTrace.test.ts diff --git a/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts b/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts new file mode 100644 index 000000000..8f7d8183f --- /dev/null +++ b/apps/ai-game-creator-shell/tests/agentRunTrace.test.ts @@ -0,0 +1,155 @@ +import { describe, expect, it } from 'vitest'; + +import { + createGameCreationAppSeedTasks, + GAME_CREATION_AGENT_RUN_MAX_PASSES, + GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, + GAME_CREATION_AGENT_TOOL_CALL_MAX, +} from '../../../packages/shared/src/contracts/gameCreationApp'; +import { formatAgentRunStatus } from '../src/features/project-summary/agentTrace'; +import { parseAgentRunTrace } from '../src/features/project-workspace/agentRunTrace'; + +const TRACE_FORMAT_ERROR = 'Agent run trace 格式不正确'; + +/** + * 旧 trace(`876529e66` 之前的 App 命令聊天会在面板里渲染它):步骤没有路径与工具数组、 + * 缺 `artifacts` / `taskGraph` 以外的归一化字段。这里保留同一份夹具,只是改成直接断言 + * 归一化函数的输出,不再渲染整个 App。 + */ +function legacyTraceFixture(): Record { + return { + schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, + runId: 'run-legacy-trace', + commandId: 'game.generate_draft', + status: 'running', + passes: 1, + stopReason: 'planning', + goal: '做一个厨房弹幕游戏', + coordination: 'legacy', + steps: [legacyStep()], + taskGraph: { + goal: '做一个厨房弹幕游戏', + activeTaskIds: ['code-prototype'], + tasks: createGameCreationAppSeedTasks(), + }, + passPlans: [ + { + pass: 1, + mode: 'repair', + summary: '旧 pass plan', + activeTaskIds: ['code-prototype'], + }, + ], + nextStep: 'continue', + error: null, + updatedAt: 1, + }; +} + +function legacyStep(overrides: Record = {}) { + return { + pass: 1, + agent: 'Generator', + phase: 'generate', + taskId: 'code-prototype', + group: 'code', + role: 'Code', + status: 'completed', + summary: '旧 trace 没有路径和工具数组', + ...overrides, + }; +} + +function traceJson(overrides: Record = {}) { + return JSON.stringify({ ...legacyTraceFixture(), ...overrides }); +} + +describe('Agent run trace 归一化与展示', () => { + it('normalizes a legacy trace before it reaches the renderers', () => { + const trace = parseAgentRunTrace(traceJson()); + + expect(trace.steps[0]!.inputPaths).toEqual([]); + expect(trace.steps[0]!.outputPaths).toEqual([]); + expect(trace.steps[0]!.toolCalls).toEqual([]); + expect(trace.artifacts).toEqual([]); + expect(trace.maxPasses).toBe(GAME_CREATION_AGENT_RUN_MAX_PASSES); + expect(trace.maxToolCalls).toBe(GAME_CREATION_AGENT_TOOL_CALL_MAX); + expect(trace.toolCallCount).toBe(0); + expect(trace.taskGraph.readyTaskIds).toEqual([]); + expect(trace.taskGraph.carriedTaskIds).toEqual([]); + expect(trace.taskGraph.repairFocus).toEqual([]); + expect(trace.taskGraph.repairRoutes).toEqual([]); + expect(trace.passPlans[0]!.carriedTaskIds).toEqual([]); + expect(trace.passPlans[0]!.dependencyWaves).toEqual([]); + expect(trace.passPlans[0]!.repairFocus).toEqual([]); + expect(trace.passPlans[0]!.repairRoutes).toEqual([]); + }); + + it('counts tool calls from the steps when the legacy trace omits the total', () => { + const trace = parseAgentRunTrace( + traceJson({ + steps: [ + legacyStep({ toolCalls: [{ toolId: 'file.read' }] }), + legacyStep({ + taskId: 'quality-review', + toolCalls: [ + { toolId: 'game.static_smoke' }, + { toolId: 'game.run_local' }, + ], + }), + ], + }), + ); + + expect(trace.toolCallCount).toBe(3); + }); + + it('falls back to the seed task graph when the trace predates task graphs', () => { + const fixture = legacyTraceFixture(); + delete fixture.taskGraph; + + const trace = parseAgentRunTrace(JSON.stringify(fixture)); + + expect(trace.taskGraph.goal).toBe('做一个厨房弹幕游戏'); + expect(trace.taskGraph.tasks.length).toBe( + createGameCreationAppSeedTasks().length, + ); + expect(trace.taskGraph.activeTaskIds).toEqual([]); + }); + + it('rejects malformed traces instead of rendering a half-parsed panel', () => { + expect(() => parseAgentRunTrace(traceJson({ steps: null }))).toThrow( + TRACE_FORMAT_ERROR, + ); + expect(() => + parseAgentRunTrace(traceJson({ schemaVersion: 'legacy' })), + ).toThrow(TRACE_FORMAT_ERROR); + expect(() => parseAgentRunTrace(traceJson({ artifacts: {} }))).toThrow( + TRACE_FORMAT_ERROR, + ); + expect(() => parseAgentRunTrace(traceJson({ taskGraph: [] }))).toThrow( + TRACE_FORMAT_ERROR, + ); + expect(() => + parseAgentRunTrace( + traceJson({ + passPlans: [ + { pass: 1, mode: 'repair', dependencyWaves: [['a'], 'b'] }, + ], + }), + ), + ).toThrow(TRACE_FORMAT_ERROR); + }); + + it('formats the run status with and without a lifecycle status', () => { + const running = parseAgentRunTrace(traceJson()); + expect(formatAgentRunStatus(running)).toBe('running · 1/3 轮 · planning'); + + const passed = parseAgentRunTrace( + traceJson({ status: 'passed', passes: 2, lifecycleStatus: 'completed' }), + ); + expect(formatAgentRunStatus(passed)).toBe( + 'passed / completed · 2/3 轮 · planning', + ); + }); +});