/** 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);
});
});