/** L3 动作过程记录:计划项 × 运行态 × 引擎痕 */ import { describe, expect, it } from 'vitest'; import { parseCmmn } from '@modules/loop-engine'; import type { ItemRuntime } from '@modules/loop-engine'; import { followRes, stepsOf } from '../activity-ops/instance-steps.ts'; const XML = ` `; const rt = (states: Record): ItemRuntime[] => Object.entries(states).map(([id, state]) => ({ id, state, instance: 1 })); const line = (title: string, at: string) => ({ kind: 'engine-transition', title, at }); const intents = new Map([['S1', '把来件归一成台账行'], ['S2', '派一张执行单交人']]); describe('stepsOf', () => { const items = parseCmmn(XML).items; it('只摆任务项(里程碑不算),按文档序;人工激活的项是人办的;时刻与用时来自引擎痕', () => { const steps = stepsOf({ items, runtimeItems: rt({ S1: 'Completed', S2: 'Active', S3: 'Available' }), intents, history: [ line('开始办:归一', '2026-08-27T02:00:00.000Z'), line('完成:归一', '2026-08-27T02:03:00.000Z'), line('开始办:交人确认', '2026-08-27T02:03:30.000Z'), ], state: 'running', ownerName: '柴浩', llm: 'deepseek-v4-flash', }); expect(steps.map((s) => [s.seq, s.name, s.by, s.byName, s.res])).toEqual([ [1, '归一', 'agent', 'deepseek-v4-flash', 'done'], [2, '交人确认', 'human', '柴浩', 'current'], [3, '放行', 'agent', 'deepseek-v4-flash', 'pending'], ]); expect(steps[0]).toMatchObject({ at: '2026-08-27T02:00:00.000Z', durationMs: 180_000, note: '把来件归一成台账行' }); expect(steps[1]).toMatchObject({ at: '2026-08-27T02:03:30.000Z', durationMs: null, note: '派一张执行单交人' }); expect(steps[2]).toMatchObject({ at: null, durationMs: null, note: '' }); }); it('自动收步的项拿收步那一刻当时刻;没有 runtime 的项是 pending', () => { const steps = stepsOf({ items, runtimeItems: rt({ S1: 'Completed' }), intents, history: [line('自动收步:S1 的成果已在台账里(rows > 0)—— 不必再问模型', '2026-08-27T02:01:00.000Z')], state: 'running', ownerName: '柴浩', llm: 'm', }); expect(steps[0]).toMatchObject({ res: 'done', at: '2026-08-27T02:01:00.000Z', durationMs: null }); expect(steps[1]?.res).toBe('pending'); }); it('终止 / 失败:正在办的那一步记 fail;都没开就记第一个没到的;驳回记最后一个已完成的', () => { const facts = { items, intents, history: [], ownerName: '柴浩', llm: 'm' }; expect(stepsOf({ ...facts, runtimeItems: rt({ S1: 'Completed', S2: 'Active' }), state: 'failed' }).map((s) => s.res)).toEqual(['done', 'fail', 'pending']); expect(stepsOf({ ...facts, runtimeItems: rt({ S1: 'Terminated', S2: 'Terminated' }), state: 'failed' }).map((s) => s.res)).toEqual(['fail', 'pending', 'pending']); expect(stepsOf({ ...facts, runtimeItems: rt({ S1: 'Completed', S2: 'Completed', S3: 'Completed' }), state: 'rejected' }).map((s) => s.res)).toEqual(['done', 'done', 'fail']); expect(stepsOf({ ...facts, runtimeItems: rt({ S1: 'Failed' }), state: 'running' }).map((s) => s.res)).toEqual(['fail', 'pending', 'pending']); }); it('重复实例化的项看最新那一实例', () => { const steps = stepsOf({ items, runtimeItems: [{ id: 'S2', state: 'Completed', instance: 1 }, { id: 'S2', state: 'Active', instance: 2 }], intents, history: [], state: 'running', ownerName: '柴浩', llm: 'm', }); expect(steps[1]?.res).toBe('current'); }); }); describe('followRes:第 9 步跟第 8 步(放行那一步没有自己的收步事件)', () => { const step = (seq: number, res: 'done' | 'current' | 'pending' | 'fail') => ({ seq, name: `S${seq}`, by: 'agent' as const, byName: 'llm', at: null, durationMs: null, res, note: '' }); it('已放行的单:第 8 步 done → 第 9 步也 done、时刻跟第 8 步;别的字段不动', () => { const steps = [...Array.from({ length: 7 }, (_, i) => step(i + 1, 'done')), { ...step(8, 'done'), at: '2026-08-27T14:30:26.080Z' }, { ...step(9, 'pending'), by: 'human' as const, byName: '柴浩' }]; const out = followRes(steps, { seq: 9, from: 8 }); expect(out[8]).toEqual({ ...step(9, 'done'), at: '2026-08-27T14:30:26.080Z', by: 'human', byName: '柴浩' }); expect(out.slice(0, 8)).toEqual(steps.slice(0, 8)); }); it('第 8 步 fail / pending 时第 9 步照样跟;步数不够 9 的活动什么都不动', () => { expect(followRes([step(1, 'done'), ...Array.from({ length: 7 }, (_, i) => step(i + 2, 'pending')), step(9, 'pending')].map((s) => (s.seq === 8 ? { ...s, res: 'fail' as const } : s)), { seq: 9, from: 8 })[8]?.res).toBe('fail'); const seven = Array.from({ length: 7 }, (_, i) => step(i + 1, 'done')); expect(followRes(seven, { seq: 9, from: 8 })).toEqual(seven); }); });