W05 在途修订:差异评估结论审核件一个候选一张表 · 关键台账数据看板件(ledger-board)· S1 清单不再把人刚填的参数摆成缺口

活动包 `part-substitution-assessment`(case-model / manifest / report / target-style / badge / ledger-board 与两份包内测试)、
目标件「验收工单-01-差异评估结论审核」、`docs/ss02-act-05` 关键台账数据两份、端到端测试记录、e2e 脚本。
逐条口径见同批 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:
xcs
2026-08-27 23:20:15 +08:00
co-authored by Claude Fable 5
parent 0923fbfe4d
commit 909b1fe4b7
13 changed files with 1943 additions and 208 deletions
+51 -1
View File
@@ -42,6 +42,13 @@ const E2E_CLIENT_SECRET = process.env.KEYCLOAK_E2E_CLIENT_SECRET ?? '';
* 不设它才走服务账号那条正路(ADR-012)。
*/
const DIRECT_BEARER = (process.env.JCAI_E2E_BEARER ?? '').replace(/^Bearer\s+/i, '').trim();
/**
* **要不要落真库**(`JCAI_E2E_LAND_MASTER=1`)—— 默认关:起单接口默认开着(人手点那条路要看见数据真变),
* 而 e2e 关它是为了保护「只能测一次」的样例(A1 新客户来函 / W05 候选缺库:主数据一旦真建进去,
* 第二次跑那条路就成了老客户 / 都在库)。开着跑一次能验「数据工单确认即入库」真的落到了正式表 ——
* 跑完记得把建进去的料号清掉,不然 W05 案例 0/2 从此测不出缺库那条路。
*/
const LAND_MASTER = process.env.JCAI_E2E_LAND_MASTER === '1';
/** access token 只活 5 分钟:超过 4 分钟就换一枚,免得长案例中途 401 */
const TOKEN_REFRESH_MS = 4 * 60 * 1000;
@@ -553,6 +560,8 @@ const SUITES = {
rowKey: 'internal_pn',
blankCol: 'brand_name',
expectGateBlock: true,
// 数据工单确认即入库(2026-08-27 用户定)—— 交卷那一刻这张卡的表就该进正式表
landsOnConfirm: true,
edits: [],
// 案例没点名的料号兜底(不该走到这里 —— 每个案例都在 stageEdits 里逐颗给了)
fillAllBlank: {
@@ -580,6 +589,7 @@ const SUITES = {
rowKey: 'internal_pn',
blankCol: 'document_title',
expectGateBlock: true,
landsOnConfirm: true,
edits: [],
fillAllBlank: {
doc_seq: '01',
@@ -838,6 +848,27 @@ function draftRows(drafts, address) {
return (drafts ?? []).filter((d) => d.address === address).map((d) => ({ ...d.key, ...d.patch }));
}
/**
* **数据工单确认即入库**(2026-08-27 用户定:「数据工单里填的数据,确认后就应该入库」)——
* 交卷那一刻就验,不等放行:
* · 痕上必须有一条「数据工单确认入库 / 已确认(试跑」(基座在回传路由里同步落的,交卷返回时已在);
* · 落真库时(`JCAI_E2E_LAND_MASTER=1`)正式表里必须查得到件上填的那几颗料;
* · 不落真库时痕必须如实写「只留在活动缓存」—— 界面不撒谎那一条也要验。
* 回 'landed' / 'held' / 'missing'(没这条痕 = 这道门没开)/ 'absent'(说落了、正式表里没有)。
*/
async function expectLandedOnConfirm(taskId, stage, rows) {
const { body } = await api(`/api/gov/testbed/activity/${taskId}`);
const note = [...(body.trail ?? [])].reverse().find((t) => t.kind === 'ledger-write' && /数据工单(确认入库|已确认)/.test(t.title ?? ''));
if (note === undefined) return 'missing';
if (/试跑|未落正式表/.test(note.title)) return LAND_MASTER ? 'absent' : 'held';
if (!LAND_MASTER) return 'landed';
const [domain, object] = stage.address.split('.');
const formal = await api(`/api/gov/data/${domain}/${object}`);
const keys = Array.isArray(stage.rowKey) ? stage.rowKey : [stage.rowKey];
const present = (row) => (formal.body.rows ?? []).some((f) => keys.every((k) => String(f[k] ?? '') === String(row[k] ?? '')));
return rows.every(present) ? 'landed' : 'absent';
}
/** 交卷执行卡 —— 载荷零字段(执行卡上没有任何输入,填写全在件上) */
async function submitExecution(cardId) {
await api('/api/desk/work-orders/reply', {
@@ -1135,7 +1166,7 @@ async function runCase(suite, kase) {
data: sample.input,
message: sample.message ?? '',
ownerId: '',
landMaster: false,
landMaster: LAND_MASTER,
}),
});
const taskId = started.taskId;
@@ -1147,6 +1178,8 @@ async function runCase(suite, kase) {
// 录入卡的已答名单单独记 —— 与 decided 共用会把 decideSeq 按 size 取的序号顶掉(实测:点名驳回被跳过)
const entered = new Set();
let gateBlocked = false;
/** 数据工单确认了却没入库(或说入库了而表里没有)—— 收工判失败 */
let landingBroken = false;
/** 资料文件那一次检查的结论(在办中验,见下面那段注释) */
let fileCheck;
let gateLeaked = false;
@@ -1331,6 +1364,19 @@ async function runCase(suite, kase) {
}
console.log(` 交卷执行卡:${card.title}`);
replied.add(card.id);
if (stage.landsOnConfirm === true) {
const verdict = await expectLandedOnConfirm(taskId, stage, tables[stage.address] ?? []);
console.log(
verdict === 'landed'
? ` 数据工单确认即入库:${stage.address} 已进正式表`
: verdict === 'held'
? ` 数据工单已确认(试跑不落主数据,痕如实写「只留在活动缓存」)`
: verdict === 'missing'
? ` ⚠️ 数据工单确认了,痕上没有「确认入库」那一条 —— 这道门没开,判失败`
: ` ⚠️ 痕说入库了,正式表里查不到件上填的那几行 —— 判失败`,
);
if (verdict === 'missing' || verdict === 'absent') landingBroken = true;
}
} else if (card.mode === 'acceptance') {
if (kase.acceptEdits && !acceptEdited) {
acceptEdited = true;
@@ -1379,6 +1425,10 @@ async function runCase(suite, kase) {
console.log(' ⚠️ 提交闸没拦住空手交卷 —— 判失败');
ok = false;
}
if (landingBroken) {
console.log(' ⚠️ 数据工单确认即入库那道门没兑现 —— 判失败');
ok = false;
}
// 「超限才叫人」是 A4 的要害:该叫的那一单没叫,等于底价门禁是摆设
if (missedRows.size > 0) {
console.log(` ⚠️ 案例点名要填的行,整例跑完也没在件上出现过(${missedRows.size} 处:${[...missedRows].slice(0, 3).join('、')}…)—— 判失败`);