/** * 受理归一(2026-07-30 宽进,用户定的逻辑)—— * 「调用活动,输入数据可以不定或者为空;进入活动后受理归一,信息不全派补全单。」 * * 这里守四条: * ① 缺必填**不拒单**:任务落成「等人」,补全卡发给负责人,谱一步不走; * ② 补全答复只收契约声明过的键,合并进 data,齐了转 running; * ③ 答一半:合并已给的、就缺的部分再派一张卡,还是等人; * ④ 「没法填」不硬拦:人写明原因就带缺口开跑 —— 收敛是活动的事,不是门卫的事。 */ import { describe, expect, it } from 'vitest'; import type { 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'; type Harness = { service: ReturnType; asked: Array<{ fields: Array<{ key: string; label: string }> }>; tasks: Map; }; const INPUT_SCHEMA = { type: 'object', properties: { customerCode: { type: 'string', title: '客户码' }, rfqRef: { type: 'string', title: '询价单号' }, subject: { type: 'string' }, }, required: ['customerCode', 'rfqRef'], }; function harness(): Harness { const asked: Harness['asked'] = []; const tasks = new Map(); let seq = 0; const deps: TaskServiceDeps = { store: { get: async (id) => tasks.get(id), insert: async (t) => { 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 () => new Date('2026-07-30T00:00:01.000Z'), markProgress: async () => undefined, listByOwner: async () => [], }, activities: { load: async () => ({ caps: { steps: 40, tokens: 200000, humanCalls: 2, activeMinutes: 60 }, envelope: {}, inputSchema: INPUT_SCHEMA, businessModel: {}, stallAfter: 'PT30M', }), }, schemas: { validate: () => ({ ok: true, errors: [] }) }, engine: { load: async () => undefined, advance: async () => ({ status: 'running' }), onEvent: async () => ({ status: 'running' }) }, memory: { append: async () => undefined, 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 (input) => { asked.push({ fields: input.fields.map((f) => ({ key: f.key, label: f.label })) }); }, }, comms: { record: async () => undefined }, clock: { now: () => new Date('2026-07-30T00:00:00.000Z') }, ids: { next: () => `t-${(seq += 1)}` }, }; return { service: createTaskService(deps), asked, tasks }; } const START = { activityId: 'a1', ownerId: 's-1' as never, title: '客户RFQ受理', } as const; describe('受理归一 —— 宽进,信息不全派补全单', () => { it('① 缺必填不拒单:任务落成等人,补全卡带中文标签发出去', async () => { const h = harness(); const task = await h.service.start({ ...START, data: { subject: '来信' } }); expect(task.runtime, '不开跑,先等人').toBe('awaiting-human'); expect(h.asked).toHaveLength(1); expect(h.asked[0]!.fields).toEqual([ { key: 'customerCode', label: '客户码' }, { key: 'rfqRef', label: '询价单号' }, ]); }); it('② 补全到齐:合并进 data、转 running;不认契约之外的键', async () => { const h = harness(); const task = await h.service.start({ ...START, data: {} }); await h.service.deliver(task.id as TaskId, { kind: 'human', ref: 'card-1', payload: { values: { customerCode: 'C014', rfqRef: 'RFQ-1', hacker: '塞私货' } }, }); const after = h.tasks.get(task.id)!; expect(after.runtime).toBe('running'); expect(after.data['customerCode']).toBe('C014'); expect(after.data['rfqRef']).toBe('RFQ-1'); expect('hacker' in after.data, '契约没声明的键不收').toBe(false); }); it('③ 答一半:合并已给的,就缺的再派一张卡,还是等人', async () => { const h = harness(); const task = await h.service.start({ ...START, data: {} }); await h.service.deliver(task.id as TaskId, { kind: 'human', ref: 'card-1', payload: { values: { customerCode: 'C014' } }, }); const after = h.tasks.get(task.id)!; expect(after.runtime).toBe('awaiting-human'); expect(after.data['customerCode']).toBe('C014'); expect(h.asked, '第二张卡只要还缺的').toHaveLength(2); expect(h.asked[1]!.fields.map((f) => f.key)).toEqual(['rfqRef']); }); it('④ 没法填:写明原因就带缺口开跑 —— 收敛是活动的事', async () => { const h = harness(); const task = await h.service.start({ ...START, data: {} }); await h.service.deliver(task.id as TaskId, { kind: 'human', ref: 'card-1', payload: { values: {}, reason: '客户没给单号,先按来信受理' }, }); expect(h.tasks.get(task.id)!.runtime).toBe('running'); }); it('输入齐全照旧直接开跑 —— 宽进不该让正常路多等一站', async () => { const h = harness(); const task = await h.service.start({ ...START, data: { customerCode: 'C014', rfqRef: 'RFQ-1' } }); expect(task.runtime).toBe('running'); expect(h.asked).toHaveLength(0); }); });