forked from ChaiHao/BPOS-MVP
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的: 件上就地改 · 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名, 于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 —— 而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。 · **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。 删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)—— 「没回传就是删了」会把一次不完整的回传变成一次删表。 · **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。 · 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。 判据与门 · 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来, 人填好的目标价它照样判「客户未提供」的毛病由此消掉。 · 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。 · 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开, 于是一单没打标就进了放行门。三个数全由基座现数。 · 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。 **指向两扇打不开的门,比不指还糟。** 界面不撒谎 · 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。 · 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、 往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。 · 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。 · 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。 · 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。 A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。 五道门禁全绿(971 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
157 lines
5.9 KiB
TypeScript
157 lines
5.9 KiB
TypeScript
/**
|
|
* 受理归一(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<typeof createTaskService>;
|
|
asked: Array<{ fields: Array<{ key: string; label: string }> }>;
|
|
tasks: Map<string, Task>;
|
|
};
|
|
|
|
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<string, Task>();
|
|
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);
|
|
});
|
|
});
|