Archived
harness 与工作台同日修:数据工单确认即入库 · 叫了人不再自动推进(两道结构闸)· 审核卡 = 终局验收卡结构 · 验收清单人点达标 · 迟到守卫不丢放行裁决 · 画面跟着这一拍发出去的卡走 · 成果详情只翻页 · 占位居中 · 进度条
human-tool / ledger / loop-engine / release / task / interaction 六模块与 app 层(desk 路由、engines、consumers、 data-order-landing、case-stamp、reset-runs、seed-activities、容器与 server 装配),web 的 desk-task (ViewRegion / chat-bubbles / review-face)与 app.css、ui-kit 图标;契约 card / manifest; 迁移 0034_task_seq 与迁移日志;api-report 快照;CLAUDE.md 快照、debt、活动开发规范、HTML 报告规范同步。 逐条口径见 CLAUDE.md 快照(2026-08-27 下午至晚)。另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 数据工单确认即入库(2026-08-27 用户定)—— 这段错了不报错:人按下确认、卡记成办结,
|
||||
* 而库里一行没有;要到整单放行才发现,或者永远发现不了(试跑单不落主数据)。
|
||||
*
|
||||
* 这里测的是编排(纯逻辑 + 假台账):落哪几张、落成什么样留什么痕;真库那一侧在
|
||||
* `ledger-store.it.test.ts`(范围放行三条)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TaskId, WorkOrder } from '@contracts';
|
||||
import { workOrderSchema } from '@contracts';
|
||||
import type { ReleaseScope, ReleaseWriteResult } from '@modules/ledger';
|
||||
import { dataOrderLedgers, landDataOrder } from '../data-order-landing.ts';
|
||||
|
||||
const T = 'task-1' as TaskId;
|
||||
|
||||
/** 夹具走契约 schema 造 —— 造出来的就是一张合法工单,不靠 as 硬转 */
|
||||
const order = (shape: Record<string, unknown>): WorkOrder =>
|
||||
workOrderSchema.parse({
|
||||
id: 'wo-1',
|
||||
at: '2026-08-27T10:00:00Z',
|
||||
title: '数据工单 · 芯片主数据创建(P-08):LM2596S-3.3',
|
||||
basis: [{ k: '触发原因', v: '查无记录' }],
|
||||
ask: '请建齐',
|
||||
fields: [],
|
||||
...shape,
|
||||
});
|
||||
|
||||
const dataOrder = (extra: Record<string, unknown> = {}): WorkOrder =>
|
||||
order({ mode: 'execution', variant: 'data', gateLedgers: ['parts.profile', 'parts.parameter'], ...extra });
|
||||
|
||||
describe('dataOrderLedgers —— 确认之后该入库的表', () => {
|
||||
it('数据工单:与提交闸同源 —— 绑了 blocks 看绑的,否则看 gateLedgers;去重、去脏', () => {
|
||||
expect(dataOrderLedgers(dataOrder())).toEqual(['parts.profile', 'parts.parameter']);
|
||||
expect(dataOrderLedgers(dataOrder({ gateLedgers: [' parts.profile ', 'parts.profile', 'bad address', ''] }))).toEqual(['parts.profile']);
|
||||
const bound = dataOrder({ blocks: [{ t: 'table', title: '表', cols: ['a'], rows: [], ledger: 'parts.document' }] });
|
||||
expect(dataOrderLedgers(bound)).toEqual(['parts.document']);
|
||||
});
|
||||
|
||||
it('普通执行工单 / 验收工单 / 没说自己建哪张表的数据工单 → 空(不替它猜,更不回落到必交付物)', () => {
|
||||
expect(dataOrderLedgers(order({ mode: 'execution', gateLedgers: ['parts.profile'] }))).toEqual([]);
|
||||
expect(dataOrderLedgers(dataOrder({ gateLedgers: undefined }))).toEqual([]);
|
||||
expect(dataOrderLedgers(order({ mode: 'acceptance', criteria: [], actions: [], deliverable: 'x', gateLedgers: ['parts.profile'] }))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function harness(reply: ReleaseWriteResult | Error) {
|
||||
const calls: Array<{ taskId: TaskId; scope: ReleaseScope | undefined }> = [];
|
||||
const notes: Array<{ kind: string; title: string; detail: Record<string, unknown> }> = [];
|
||||
let touched = 0;
|
||||
const deps = {
|
||||
ledger: {
|
||||
releaseWrite: async (taskId: TaskId, scope?: ReleaseScope) => {
|
||||
calls.push({ taskId, scope });
|
||||
if (reply instanceof Error) throw reply;
|
||||
return reply;
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
append: async (entry: { kind: string; title: string; detail: Record<string, unknown> }) => {
|
||||
notes.push({ kind: entry.kind, title: entry.title, detail: entry.detail });
|
||||
},
|
||||
},
|
||||
labelOf: async (address: string) => ({ 'parts.profile': '芯片主数据', 'parts.parameter': '标准化参数集' })[address] ?? address,
|
||||
touched: () => {
|
||||
touched += 1;
|
||||
},
|
||||
};
|
||||
return { deps, calls, notes, touched: () => touched };
|
||||
}
|
||||
|
||||
const merged = (rows: Array<[string, string]>): ReleaseWriteResult => ({
|
||||
mergedRows: rows.length,
|
||||
emittedEvents: rows.length,
|
||||
landedEarlierRows: 0,
|
||||
simulatedRows: 0,
|
||||
merged: rows.map(([object, pn]) => ({ domain: 'parts', object, key: { internal_pn: pn }, values: {} })),
|
||||
});
|
||||
|
||||
describe('landDataOrder —— 确认落下之后', () => {
|
||||
it('只并这张卡的那几张表(范围放行),归因带卡号;痕点名哪几张表各几行,并给模型指路;数据域跟着刷', async () => {
|
||||
const h = harness(merged([['profile', 'A'], ['parameter', 'A'], ['parameter', 'A']]));
|
||||
const got = await landDataOrder(h.deps, T, dataOrder());
|
||||
expect(got.landed).toBe(true);
|
||||
expect(h.calls).toEqual([
|
||||
{ taskId: T, scope: { only: [{ domain: 'parts', object: 'profile' }, { domain: 'parts', object: 'parameter' }], cause: 'data-order:wo-1' } },
|
||||
]);
|
||||
expect(h.notes).toHaveLength(1);
|
||||
expect(h.notes[0]?.kind).toBe('ledger-write');
|
||||
expect(h.notes[0]?.title).toContain('芯片主数据 1 行、标准化参数集 2 行已进正式表');
|
||||
expect(h.notes[0]?.title).toContain('别再往这几张表写它们');
|
||||
expect(h.notes[0]?.detail).toMatchObject({
|
||||
workOrderId: 'wo-1',
|
||||
rows: 3,
|
||||
tables: [
|
||||
{ address: 'parts.profile', rows: 1 },
|
||||
{ address: 'parts.parameter', rows: 2 },
|
||||
],
|
||||
});
|
||||
expect(h.touched()).toBe(1);
|
||||
});
|
||||
|
||||
it('试跑单挡下的主数据:痕如实说「只留在活动缓存」,数据域不刷', async () => {
|
||||
const h = harness({ mergedRows: 0, emittedEvents: 0, landedEarlierRows: 0, simulatedRows: 3, merged: [] });
|
||||
await landDataOrder(h.deps, T, dataOrder());
|
||||
expect(h.notes[0]?.title).toContain('试跑');
|
||||
expect(h.notes[0]?.title).toContain('未落正式表');
|
||||
expect(h.touched()).toBe(0);
|
||||
});
|
||||
|
||||
it('同一张卡确认两次:第二次无事发生,痕也这么说', async () => {
|
||||
const h = harness({ mergedRows: 0, emittedEvents: 0, landedEarlierRows: 3, simulatedRows: 0, merged: [] });
|
||||
await landDataOrder(h.deps, T, dataOrder());
|
||||
expect(h.notes[0]?.title).toContain('早已入库');
|
||||
});
|
||||
|
||||
it('不是数据工单就一个字不动(不调台账、不留痕)', async () => {
|
||||
const h = harness(merged([]));
|
||||
const got = await landDataOrder(h.deps, T, order({ mode: 'execution' }));
|
||||
expect(got).toEqual({ landed: false, reason: 'not-data-order' });
|
||||
expect(h.calls).toEqual([]);
|
||||
expect(h.notes).toEqual([]);
|
||||
});
|
||||
|
||||
it('入库失败不抛:人已确认的事实不倒退;原话留痕、行仍在草稿层等放行', async () => {
|
||||
const h = harness(new Error('草稿与正式版并发冲突:1 处字段已被他单改动'));
|
||||
const got = await landDataOrder(h.deps, T, dataOrder());
|
||||
expect(got).toMatchObject({ landed: false, reason: 'failed' });
|
||||
expect(h.notes[0]?.title).toContain('没能入库');
|
||||
expect(h.notes[0]?.title).toContain('并发冲突');
|
||||
expect(h.notes[0]?.title).toContain('放行时再并');
|
||||
});
|
||||
});
|
||||
@@ -565,6 +565,14 @@ describe('createOnly:键已在正式表 → 拒并指路', () => {
|
||||
expect(why).toContain('drop');
|
||||
});
|
||||
|
||||
it('本单经数据工单确认已入库的那一行 → 拒回换一句话:建好了别再写,不点名 drop(撤了读面少一行)', () => {
|
||||
const why = createOnlyViolation(target, { internal_pn: 'LM2596S-ADJ' }, formal, [{ internal_pn: 'LM2596S-ADJ' }]);
|
||||
expect(why).toContain('已入库');
|
||||
expect(why).not.toContain('drop');
|
||||
// 另一颗在库的料号与本单无关,照旧是原来那句
|
||||
expect(createOnlyViolation(target, { internal_pn: 'LM2596S-5.0' }, formal, [{ internal_pn: 'LM2596S-ADJ' }])).toContain('drop');
|
||||
});
|
||||
|
||||
it('库里没有的料号 → 放行;复合主键按全部键列比', () => {
|
||||
expect(createOnlyViolation(target, { internal_pn: 'XL2596S-5.0' }, formal)).toBeUndefined();
|
||||
const sub = { label: '标准化参数集', keys: ['internal_pn', 'param_name'] };
|
||||
|
||||
Reference in New Issue
Block a user