/** * 迟到的人事件不再推一拍(2026-08-27,用户:「你叫了人我都没填你就直接推进了」)。 * * 回传落库那一刻回传口已 wake、推进器随即跑了那一拍(读留痕里的 card-reply,不等事件); * 人事件从队列里投过来晚十几秒,那时上一拍早已发出下一张卡 —— 再 tick 一拍,模型会把没人答的 * 那一步收掉。判法只有一个数:这一单在这条回传之后推进过没有(lastProgressAt > occurredAt)。 */ import { describe, expect, it } from 'vitest'; import type { StaffId, TaskId } from '@contracts'; import type { LandedFacts, Task } from '../contract.ts'; import { createTaskService } from '../internal/usecase/task-service.ts'; import type { TaskServiceDeps } from '../internal/usecase/task-service.ts'; function harness(lastProgressAt: Date | undefined, runtime: Task['runtime'] = 'awaiting-human') { const task: Task = { id: 't-1' as TaskId, activityId: 'w05', ownerId: 's-1' as StaffId, title: 'x', runtime, envelope: {}, data: {}, caps: { steps: 40, tokens: 200000, humanCalls: 4, activeMinutes: 60 }, used: { steps: 0, tokens: 0, humanCalls: 0, activeMs: 0 }, startedAt: '2026-08-27T09:40:00.000Z', }; const tasks = new Map([[task.id, task]]); const appended: Array<{ kind: string; title: string; detail?: Record }> = []; let advanced = 0; let onEvents = 0; const deps: TaskServiceDeps = { store: { get: async (id) => tasks.get(id), insert: async (t) => void tasks.set(t.id, t), update: async (id, patch) => { const next = { ...tasks.get(id)!, ...patch }; tasks.set(id, next); return next; }, addConsumption: async (id) => tasks.get(id)!.used, lastProgressAt: async () => lastProgressAt, markProgress: async () => undefined, listByOwner: async () => [], }, activities: { load: async () => ({ caps: task.caps, envelope: {}, inputSchema: {}, businessModel: {}, stallAfter: 'PT30M' }) }, schemas: { validate: () => ({ ok: true, errors: [] }) }, engine: { load: async () => undefined, advance: async () => { advanced += 1; return { status: 'awaiting' }; }, onEvent: async () => { onEvents += 1; return { status: 'running' }; }, }, memory: { append: async (entry) => void appended.push({ kind: entry.kind, title: entry.title, ...(entry.detail === undefined ? {} : { detail: entry.detail }) }), hasHistory: async () => true, seal: async () => undefined, distill: async () => undefined, }, landing: { land: async () => ({ converged: true, emittedPorts: [] as string[], ledgerRowsMerged: 0, facts: {} as LandedFacts }) }, listeners: { hasArmedListener: async () => true }, persistence: { hasDrafts: async () => false, hasRuntimeState: async () => true }, events: { publishTerminal: async () => undefined, publishControl: async () => undefined }, notifier: { notify: async () => undefined }, interaction: { setPhase: async () => undefined }, intake: { askCompletion: async () => undefined }, comms: { record: async () => undefined }, clock: { now: () => new Date('2026-08-27T09:48:30.000Z') }, ids: { next: () => 't-x' }, }; return { service: createTaskService(deps), appended, advanced: () => advanced, onEvents: () => onEvents }; } const REPLY_AT = '2026-08-27T09:47:48.000Z'; describe('迟到的人事件', () => { it('回传之后这一单已推进过 → 只记一条到达痕(标 stale),不 onEvent、不推一拍', async () => { const h = harness(new Date('2026-08-27T09:48:00.200Z')); const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-p10', payload: {}, occurredAt: REPLY_AT }); expect(out.progressed).toBe(false); expect(h.advanced()).toBe(0); expect(h.onEvents()).toBe(0); const arrival = h.appended.find((e) => e.title === '事件到达(human):card-p10'); expect(arrival?.detail?.['stale']).toBe(true); }); /* * **判了 stale 还得管它有没有人叫醒**(2026-09-01,两次实测撞停):卡发出 270ms 后人就答了 —— * 把 `lastProgressAt` 往前挪的那一拍,**正是发出这张卡的那一拍**,它不可能消费一条当时还不存在的回传; * 而回传口的 `wake` 也白做了(那一拍随后收尾,把任务 park 回 `awaiting-event`,晚到的写盖了早到的)。 * 于是卡答完了、没有新卡在等、单子停着,**再没有第三样东西会叫醒它** —— 实测停了 15 分钟, * 手工把 runtime 拨回 running,同一份代码立刻把该走的几拍走完。 * 这一拍照旧不跑(stale 的本意不变),但停着的单子要拨回 running 交给推进器。 */ it('**stale 但单子停着** → 这一拍照旧不跑,可要把它拨回 running,不然再没人叫醒它', async () => { const h = harness(new Date('2026-08-27T09:48:00.200Z')); const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-p10', payload: {}, occurredAt: REPLY_AT }); expect(h.advanced()).toBe(0); expect(h.onEvents()).toBe(0); expect(out.task.runtime).toBe('running'); }); it('stale 而单子本来就在跑 → 一个字不动(推进器自己会跑到它)', async () => { const h = harness(new Date('2026-08-27T09:48:00.200Z'), 'running'); const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-p10', payload: {}, occurredAt: REPLY_AT }); expect(out.task.runtime).toBe('running'); expect(h.advanced()).toBe(0); }); it('回传之后没推进过 → 照常投递并推一拍(这就是叫醒它的那一下)', async () => { const h = harness(new Date('2026-08-27T09:47:40.000Z')); await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-s1', payload: {}, occurredAt: REPLY_AT }); expect(h.onEvents()).toBe(1); expect(h.advanced()).toBe(1); }); it('放行裁决即使迟到也要投:它只走事件这一条路,拍里读不到它', async () => { const h = harness(new Date('2026-08-27T09:48:00.000Z')); const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-acc', payload: { mode: 'acceptance', verdict: 'pass', values: {} }, occurredAt: REPLY_AT, }); expect(h.onEvents()).toBe(1); expect(h.advanced()).toBe(1); expect(out.progressed).toBe(true); const arrival = h.appended.find((e) => e.kind === 'observation' && e.title.startsWith('事件到达')); expect(arrival?.detail?.['stale']).toBeUndefined(); }); it('没带 occurredAt(老调用方)/ 不是人事件 → 行为照旧', async () => { const h = harness(new Date('2026-08-27T09:48:00.200Z')); await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-x', payload: {} }); await h.service.deliver('t-1' as TaskId, { kind: 'data', ref: 'row-1', payload: {}, occurredAt: REPLY_AT }); expect(h.advanced()).toBe(2); }); });