7b1f7bdf17
- 新增 tests/agentRunTrace.test.ts,用旧 trace 夹具断言归一化结果(路径/工具数组/上限/任务图/通行计划) - 覆盖 toolCalls 总数回填、taskGraph 缺失时回退种子任务图 - 覆盖畸形 trace 的拒绝路径(steps / schemaVersion / artifacts / taskGraph / dependencyWaves) - 覆盖 formatAgentRunStatus 带与不带 lifecycleStatus 的两种文案
156 lines
5.1 KiB
TypeScript
156 lines
5.1 KiB
TypeScript
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',
|
||
);
|
||
});
|
||
});
|