/** 人类工具三规矩的机检 —— domain 测试零 mock(OS 05 / AM 06) */ import { describe, expect, it } from 'vitest'; import type { AcceptanceWorkOrder, ChatCard, DeliverableId, ExecutionWorkOrder, WorkOrderId, WorkOrderReply, } from '@contracts'; import { allCriteriaGreen, arrivalOf, checkChatCard, checkChatCardReply, checkWorkOrder, checkWorkOrderReply, producesCompletion, } from '../internal/domain/card-rules.ts'; const OID = 'wo-1' as WorkOrderId; const execution = (over: Partial = {}): ExecutionWorkOrder => ({ id: OID, at: '2026-07-27T10:00:00.000Z', mode: 'execution', title: '补齐 RFQ 关键字段', basis: [{ k: '缺口', v: 'PN / EAU / 交期' }], ask: '请向客户追问缺失字段', fields: [{ key: 'pn', label: 'PN', kind: 'text', required: true }], ...over, }); const acceptance = (over: Partial = {}): AcceptanceWorkOrder => ({ id: OID, at: '2026-07-27T10:00:00.000Z', mode: 'acceptance', title: '验收统一受理记录', basis: [{ k: '条目数', v: '12' }], deliverable: 'd-1' as DeliverableId, criteria: [{ label: '八字段齐', level: 'L1', pass: true }], fields: [], ...over, }); describe('调用必附上下文 —— 裸调用基座拒发', () => { it('basis 为空即拒', () => { expect(checkWorkOrder(execution({ basis: [] })).map((v) => v.code)).toContain('bare-call'); }); it('决策卡 basis 为空即拒', () => { const card: ChatCard = { id: 'c1' as never, at: '2026-07-27T10:00:00.000Z', k: 'decision', ask: '选一个', basis: [], options: [ { value: 'a', label: 'A' }, { value: 'b', label: 'B' }, ], allowCustom: false, }; expect(checkChatCard(card).map((v) => v.code)).toContain('bare-call'); }); }); describe('工单形态机检', () => { it('规范的执行工单通过', () => { expect(checkWorkOrder(execution())).toEqual([]); }); it('执行工单 = 指导书 + 空表单,表单不得为空', () => { expect(checkWorkOrder(execution({ fields: [] })).map((v) => v.code)).toContain('fields-missing'); }); it('radio 字段缺选项即拒', () => { const v = checkWorkOrder(execution({ fields: [{ key: 'x', label: 'X', kind: 'radio' }] })); expect(v.map((i) => i.code)).toContain('options-missing'); }); it('字段 key 重复即拒', () => { const v = checkWorkOrder( execution({ fields: [ { key: 'x', label: 'X', kind: 'text' }, { key: 'x', label: 'Y', kind: 'text' }, ], }), ); expect(v.map((i) => i.code)).toContain('field-key-duplicate'); }); it('验收工单必须列判据', () => { expect(checkWorkOrder(acceptance({ criteria: [] })).map((v) => v.code)).toContain('criteria-missing'); }); }); describe('到达面两路', () => { it('负责人的卡进任务对话流', () => { expect(arrivalOf('s1', 's1')).toBe('task-chat'); }); it('别人的卡走系统消息 —— 跨人卡不是责任转移', () => { expect(arrivalOf('s2', 's1')).toBe('system-message'); }); }); describe('回传校验', () => { it('必填未回填即拒', () => { const reply: WorkOrderReply = { mode: 'execution', workOrderId: OID, values: {} }; expect(checkWorkOrderReply(execution(), reply).map((v) => v.code)).toContain('required-missing'); }); it('必填已填即过', () => { const reply: WorkOrderReply = { mode: 'execution', workOrderId: OID, values: { pn: 'LM358' } }; expect(checkWorkOrderReply(execution(), reply)).toEqual([]); }); it('驳回必附意见', () => { const reply: WorkOrderReply = { mode: 'acceptance', workOrderId: OID, verdict: 'revise', values: {}, selectedPorts: {}, }; expect(checkWorkOrderReply(acceptance(), reply).map((v) => v.code)).toContain('comment-required'); }); it('放行通过无需意见', () => { const reply: WorkOrderReply = { mode: 'acceptance', workOrderId: OID, verdict: 'pass', values: {}, selectedPorts: {}, }; expect(checkWorkOrderReply(acceptance(), reply)).toEqual([]); }); it('已回传的工单不得再答 —— 只增不改', () => { const done = acceptance({ done: { at: 'x', by: 's1' as never, result: 'pass' } }); const reply: WorkOrderReply = { mode: 'acceptance', workOrderId: OID, verdict: 'pass', values: {}, selectedPorts: {}, }; expect(checkWorkOrderReply(done, reply).map((v) => v.code)).toEqual(['already-answered']); }); it('模式不符即拒', () => { const reply: WorkOrderReply = { mode: 'execution', workOrderId: OID, values: { pn: 'x' } }; expect(checkWorkOrderReply(acceptance(), reply).map((v) => v.code)).toEqual(['mode-mismatch']); }); }); describe('对话卡回传', () => { const decision: ChatCard = { id: 'c1' as never, at: '2026-07-27T10:00:00.000Z', k: 'decision', ask: '降不降价', basis: [{ k: '底价', v: '1.80' }], options: [ { value: 'hold', label: '守价' }, { value: 'cut', label: '让价' }, ], allowCustom: false, }; it('选项内的分支通过', () => { expect(checkChatCardReply(decision, { k: 'decision', cardId: 'c1' as never, value: 'cut' })).toEqual([]); }); it('选项外的分支在不允许自定义时即拒', () => { const v = checkChatCardReply(decision, { k: 'decision', cardId: 'c1' as never, value: 'other' }); expect(v.map((i) => i.code)).toEqual(['unknown-option']); }); it('自定义收尾项必附理由', () => { const open = { ...decision, allowCustom: true }; expect( checkChatCardReply(open, { k: 'decision', cardId: 'c1' as never, value: 'other' }).map((i) => i.code), ).toEqual(['note-required']); expect( checkChatCardReply(open, { k: 'decision', cardId: 'c1' as never, value: 'other', note: '客户口头承诺追单' }), ).toEqual([]); }); it('补全卡两条路必走其一', () => { const card: ChatCard = { id: 'c2' as never, at: '2026-07-27T10:00:00.000Z', k: 'elicitation', ask: '补交期', fields: [{ key: 'lead', label: '交期' }], }; expect( checkChatCardReply(card, { k: 'elicitation', cardId: 'c2' as never, values: {} }).map((i) => i.code), ).toEqual(['answer-required']); expect(checkChatCardReply(card, { k: 'elicitation', cardId: 'c2' as never, values: { lead: '8周' } })).toEqual([]); expect( checkChatCardReply(card, { k: 'elicitation', cardId: 'c2' as never, values: {}, reason: '客户未答复' }), ).toEqual([]); }); }); describe('完成只出自放行通过', () => { it('放行通过才产生完成', () => { expect( producesCompletion({ mode: 'acceptance', workOrderId: OID, verdict: 'pass', values: {}, selectedPorts: {} }), ).toBe(true); }); it('驳回不产生完成 —— 环内回转,任务继续', () => { expect( producesCompletion({ mode: 'acceptance', workOrderId: OID, verdict: 'redo', values: {}, selectedPorts: {}, comment: '重做', }), ).toBe(false); }); it('执行工单回传不产生完成', () => { expect(producesCompletion({ mode: 'execution', workOrderId: OID, values: {} })).toBe(false); }); it('判据全绿只是可提交,不等于完成', () => { expect(allCriteriaGreen(acceptance())).toBe(true); expect(allCriteriaGreen(acceptance({ criteria: [{ label: 'x', level: 'L2', pass: false }] }))).toBe(false); }); }); /** * **一张录入卡可以落好几张表**(2026-08-02 用户:「新建客户 联系人和公司都需要确认」)。 * * 建一位新客户落的是两条主数据:公司档 + 联系人档。从前卡上只有公司那一半, * 联系人由 AI 自己写 —— 而联系人档的 `contact_code` 是它编出来的,人一眼没看过。 * 值按段隔开(`段号.列名`):两张表都有 `name` 这一列,平铺就会互相盖掉, * **而盖掉之后没人看得出来**。 */ describe('录入卡分段:段号.列名', () => { /** 与 `routes/desk.ts` 那段拆分同一口径 —— 改一处这里要跟着改 */ const split = (values: Record, at: number): Record => { const out: Record = {}; for (const [k, v] of Object.entries(values)) { const mark = `${at}.`; if (k.startsWith(mark)) out[k.slice(mark.length)] = v; else if (at === 0 && !/^\d+\./.test(k)) out[k] = v; } return out; }; const values = { '0.customer_code': 'CU-9001', '0.name': '上海精密电子有限公司', '1.customer_code': 'CU-9001', '1.contact_code': 'CT-01', '1.name': '陈刚', '1.email': 'chen@sh-elec.com', }; it('**同名列不互相盖** —— 公司叫「上海精密电子」,联系人叫「陈刚」', () => { expect(split(values, 0)['name']).toBe('上海精密电子有限公司'); expect(split(values, 1)['name']).toBe('陈刚'); }); it('每段只拿自己那几格 —— 联系人编码不该落进公司档', () => { expect(split(values, 0)['contact_code']).toBeUndefined(); expect(Object.keys(split(values, 1)).sort()).toEqual(['contact_code', 'customer_code', 'email', 'name']); }); it('**老形状(没段号的平键)仍旧管用** —— 一张卡只有一段时照旧落主段', () => { expect(split({ customer_code: 'CU-1', name: '老写法' }, 0)).toEqual({ customer_code: 'CU-1', name: '老写法' }); }); it('老形状不喂给后面的段 —— 它说不清自己是哪一张表的', () => { expect(split({ customer_code: 'CU-1', name: '老写法' }, 1)).toEqual({}); }); });