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', ); }); });