forked from ChaiHao/BPOS-MVP
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
99 lines
3.7 KiB
TypeScript
99 lines
3.7 KiB
TypeScript
/** 投递五诺的纯判定部分(OS 6.3 / 编码规范 §7.1) */
|
||
import { describe, expect, it } from 'vitest';
|
||
import { consumptionKey, decideRetry, fanOutTargets, planConsumption } from '../internal/domain/delivery.ts';
|
||
import { deadlineMillis } from '../internal/domain/listener-match.ts';
|
||
|
||
describe('decideRetry —— 处理有回执', () => {
|
||
it('未超限即重投,退避递增', () => {
|
||
expect(decideRetry(1)).toEqual({ kind: 'retry', delayMs: 1_000, attempt: 2 });
|
||
expect(decideRetry(2)).toEqual({ kind: 'retry', delayMs: 5_000, attempt: 3 });
|
||
expect(decideRetry(4)).toEqual({ kind: 'retry', delayMs: 120_000, attempt: 5 });
|
||
});
|
||
|
||
it('超限入死信', () => {
|
||
expect(decideRetry(5)).toEqual({ kind: 'dead-letter', attempts: 5 });
|
||
});
|
||
});
|
||
|
||
describe('fanOutTargets —— 扇出与幂等', () => {
|
||
it('同一任务多条监听命中只产生一份投递', () => {
|
||
expect(fanOutTargets([{ taskId: 'a' }, { taskId: 'a' }, { taskId: 'b' }])).toEqual(['a', 'b']);
|
||
});
|
||
});
|
||
|
||
describe('planConsumption —— 三级消费', () => {
|
||
it('监听与触发独立判定:可既推进旧单又起新单', () => {
|
||
const plan = planConsumption({
|
||
listenerHits: [{ taskId: 't1' }],
|
||
triggerHits: ['tr1'],
|
||
ownedTaskId: 't1',
|
||
hasOwner: true,
|
||
});
|
||
expect(plan.taskTargets).toEqual(['t1']);
|
||
expect(plan.triggerTargets).toEqual(['tr1']);
|
||
expect(plan.messageFallback).toBe(false);
|
||
});
|
||
|
||
it('皆未命中且无主 → 消息兜底', () => {
|
||
const plan = planConsumption({ listenerHits: [], triggerHits: [], ownedTaskId: undefined, hasOwner: false });
|
||
expect(plan.messageFallback).toBe(true);
|
||
});
|
||
|
||
it('有主但零监听命中 → 仅记录,不兜底、不降级为无主', () => {
|
||
const plan = planConsumption({ listenerHits: [], triggerHits: [], ownedTaskId: 't7', hasOwner: true });
|
||
expect(plan.messageFallback).toBe(false);
|
||
expect(plan.recordOnlyTask).toBe('t7');
|
||
expect(plan.taskTargets).toEqual([]);
|
||
});
|
||
|
||
it('主不是任务也算有主 —— 引擎发的工作流事件不得被当成无主件塞进收件箱', () => {
|
||
// 起单 / 解析负责人 / 起任务这些事件的 target 是 workflow,不是 task。
|
||
// 只认 ownedTaskId 的话,每起一单都会给人推一条「无主入站待认领」。
|
||
const plan = planConsumption({
|
||
listenerHits: [],
|
||
triggerHits: [],
|
||
ownedTaskId: undefined,
|
||
hasOwner: true,
|
||
});
|
||
expect(plan.messageFallback).toBe(false);
|
||
expect(plan.recordOnlyTask).toBeUndefined();
|
||
});
|
||
|
||
it('有主且监听命中 → 不是仅记录', () => {
|
||
const plan = planConsumption({
|
||
listenerHits: [{ taskId: 't7' }],
|
||
triggerHits: [],
|
||
ownedTaskId: 't7',
|
||
hasOwner: true,
|
||
});
|
||
expect(plan.recordOnlyTask).toBeUndefined();
|
||
});
|
||
|
||
it('无主但触发命中 → 不兜底', () => {
|
||
const plan = planConsumption({ listenerHits: [], triggerHits: ['tr1'], ownedTaskId: undefined, hasOwner: false });
|
||
expect(plan.messageFallback).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('consumptionKey —— 幂等键', () => {
|
||
it('事件 ID × 消费者', () => {
|
||
expect(consumptionKey('e1', 'task:t1')).toBe('e1::task:t1');
|
||
});
|
||
});
|
||
|
||
describe('deadlineMillis —— 监听时限必配', () => {
|
||
it('解析 ISO-8601 duration', () => {
|
||
expect(deadlineMillis('P3D')).toBe(3 * 86_400_000);
|
||
expect(deadlineMillis('PT30M')).toBe(30 * 60_000);
|
||
expect(deadlineMillis('P1DT2H30M')).toBe(86_400_000 + 2 * 3_600_000 + 30 * 60_000);
|
||
});
|
||
|
||
it('非法表达式抛错', () => {
|
||
expect(() => deadlineMillis('3天')).toThrow(/非法时限/);
|
||
});
|
||
|
||
it('零时限抛错 —— 无死等', () => {
|
||
expect(() => deadlineMillis('PT0S')).toThrow(/大于零/);
|
||
});
|
||
});
|