给 parseAgentRunTrace 与 formatAgentRunStatus 补上纯函数用例

- 新增 tests/agentRunTrace.test.ts,用旧 trace 夹具断言归一化结果(路径/工具数组/上限/任务图/通行计划)
- 覆盖 toolCalls 总数回填、taskGraph 缺失时回退种子任务图
- 覆盖畸形 trace 的拒绝路径(steps / schemaVersion / artifacts / taskGraph / dependencyWaves)
- 覆盖 formatAgentRunStatus 带与不带 lifecycleStatus 的两种文案
This commit is contained in:
2026-09-21 11:36:02 +08:00
parent abb2988875
commit 7b1f7bdf17
@@ -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<string, unknown> {
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<string, unknown> = {}) {
return {
pass: 1,
agent: 'Generator',
phase: 'generate',
taskId: 'code-prototype',
group: 'code',
role: 'Code',
status: 'completed',
summary: '旧 trace 没有路径和工具数组',
...overrides,
};
}
function traceJson(overrides: Record<string, unknown> = {}) {
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',
);
});
});