给 parseAgentRunTrace 与 formatAgentRunStatus 补上纯函数用例
- 新增 tests/agentRunTrace.test.ts,用旧 trace 夹具断言归一化结果(路径/工具数组/上限/任务图/通行计划) - 覆盖 toolCalls 总数回填、taskGraph 缺失时回退种子任务图 - 覆盖畸形 trace 的拒绝路径(steps / schemaVersion / artifacts / taskGraph / dependencyWaves) - 覆盖 formatAgentRunStatus 带与不带 lifecycleStatus 的两种文案
This commit is contained in:
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user