Archived
放行那一步(T_release,人工激活)**没有自己的收步事件** —— 人签字后 `submit` 直接把整单落终态, 计划项本身从没被 finish 过。于是一单已 `released` 的详情里第 9 步永远是 `pending`, 而它前面拟落账那一步早就 `done`:同一张单上两句话互相打脸。 `followRes` 纯函数在服务端交出详情之前把第 9 步的 `res` 与 `at` 跟上第 8 步(2026-08-28 用户定,`at` 也跟); **用时不编** —— 那一步没有自己的起止痕;两步有一步不在(别的活动步数不同)就什么都不动。 《API文档-活动模块》§ 时间线补一行口径。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
102 lines
5.4 KiB
TypeScript
102 lines
5.4 KiB
TypeScript
/** 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 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL" id="d">
|
|
<case id="c" name="试">
|
|
<casePlanModel id="p" name="p">
|
|
<humanTask id="S1" name="归一"/>
|
|
<humanTask id="S2" name="交人确认">
|
|
<itemControl><manualActivationRule id="m_s2"/></itemControl>
|
|
</humanTask>
|
|
<humanTask id="S3" name="放行"/>
|
|
<milestone id="M1" name="里程碑"/>
|
|
</casePlanModel>
|
|
</case>
|
|
</definitions>`;
|
|
|
|
const rt = (states: Record<string, ItemRuntime['state']>): 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);
|
|
});
|
|
});
|