Files
JX-BPOS/apps/api/src/modules/event/__tests__/delivery.test.ts
T

99 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 投递五诺的纯判定部分(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(/大于零/);
});
});