forked from ChaiHao/BPOS-MVP
M3(2/5): human-tool 与 release —— 卡协议与放行门
human-tool(L3): - 三规矩机检:调用必附上下文(裸调用拒发)/ 回传即留痕 / 完成只出自放行通过 - 工单两模式 + 对话卡族的形态校验与回传校验;驳回必附意见;补全卡「没法填」旁路; 决策卡自定义收尾项必附理由;已答不得再答(只增不改) - 到达面两路(负责人 → 任务对话流;他人 → 系统消息);驳回重答 = 新卡实例、原卡闭合 - 调人预算由基座在环外强制;落地失败卡 = 执行工单的 preset release(L4): - 落地预检五项(入参 schema / 工具可用与授权 / 连接可达 / 目标预校验 / 数据冲突) - 预检全过才发验收工单,卡面即已验证的动作清单;不过即打回环内,不发卡 - 放行写合并 + 幂等落地(任务 × 意向);落地收敛才发射输出口; 受阻走落地失败卡且 emittedPorts 恒空;已成动作留存不回滚 215 个测试通过,门禁五道全绿 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
146e9bbfad
commit
561a2474ae
@@ -0,0 +1,230 @@
|
||||
/** 人类工具三规矩的机检 —— 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> = {}): 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> = {}): 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user