Archived
活动包 `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
458 lines
28 KiB
TypeScript
458 lines
28 KiB
TypeScript
/**
|
|
* 步骤剧本与呈现面 —— 这段错了不报错,只是活动开始转圈 / 界面开始撒谎(与 E04 / I01 同一套惯例)。
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import { evaluateCondition } from '@sfeel';
|
|
import { ASSESSMENT_PHASES, ASSESSMENT_STEPS, CMMN_XML, NARRATIVE } from '../src/case-model.ts';
|
|
import { manifest, D } from '../src/manifest.ts';
|
|
|
|
const ITEM_IDS = [...CMMN_XML.matchAll(/<(?:humanTask|decisionTask|stage|milestone)\s+id="([^"]+)"/g)].map((m) => m[1] ?? '');
|
|
|
|
function doneOf(item: string): string {
|
|
const found = ASSESSMENT_STEPS.find((s) => s.item === item)?.doneWhen;
|
|
if (found === undefined) throw new Error(`${item} 没有 doneWhen`);
|
|
return found;
|
|
}
|
|
/** 谓词写坏了引擎当作「没算出来」——测里也照这个口径 */
|
|
function safeEval(expression: string, scope: Record<string, unknown>): boolean {
|
|
try {
|
|
return evaluateCondition(expression, scope);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
/** 开局局面:入参 + 所有基座读数归零 */
|
|
const fresh = (input: Record<string, unknown>): Record<string, unknown> => ({
|
|
...input,
|
|
demand_request_rows: 0,
|
|
demand_request_missing: 0,
|
|
[`${D}_verdict_rows`]: 0,
|
|
[`${D}_pn_check_rows`]: 0,
|
|
[`${D}_pn_check_missing`]: 0,
|
|
[`${D}_pn_check_exists_status_absent`]: 0,
|
|
[`${D}_pn_check_exists_status_abandoned`]: 0,
|
|
[`${D}_input_gap_rows`]: 0,
|
|
[`${D}_input_gap_missing`]: 0,
|
|
input_confirmed_total: 0,
|
|
review_approved_total: 0,
|
|
landing_prepared_total: 0,
|
|
substitution_relation_rows: 0,
|
|
substitution_relation_missing: 0,
|
|
substitution_relation_analysis_status_fae_confirmed: 0,
|
|
substitution_difference_item_rows: 0,
|
|
substitution_difference_item_missing: 0,
|
|
substitution_difference_item_distinct_substitution_relation_id: 0,
|
|
substitution_difference_item_difference_status_match: 0,
|
|
substitution_difference_item_difference_status_mismatch: 0,
|
|
substitution_difference_item_difference_status_unknown: 0,
|
|
substitution_difference_item_difference_status_conditional: 0,
|
|
tool_part_substitution_assessment_compare_parameters_calls: 0,
|
|
tool_part_substitution_assessment_recommend_calls: 0,
|
|
});
|
|
|
|
describe('剧本与案例模型对得上', () => {
|
|
it('每一条剧本都指向一个真实存在的计划项,图上每一步也都有剧本', () => {
|
|
for (const step of ASSESSMENT_STEPS) expect(ITEM_IDS, `剧本里的 ${step.item} 在 CMMN 里不存在`).toContain(step.item);
|
|
for (const id of ITEM_IDS) expect(ASSESSMENT_STEPS.map((s) => s.item), `计划项 ${id} 没有剧本`).toContain(id);
|
|
});
|
|
|
|
it('清单里挂的就是这份剧本与这份阶段', () => {
|
|
expect(manifest.assets.businessModel.steps).toBe(ASSESSMENT_STEPS);
|
|
expect(manifest.assets.businessModel.phases).toBe(ASSESSMENT_PHASES);
|
|
});
|
|
});
|
|
|
|
describe('剧本声明的读写面 ⊆ 清单声明', () => {
|
|
const declaredAddresses = new Set(manifest.declarations.ledger.map((d) => `${d.domain}.${d.object}`));
|
|
const declaredTools = new Set(manifest.declarations.tools.map((t) => t.name));
|
|
|
|
it('steps[].writes / reads(台账那几个)全落在声明过的对象上', () => {
|
|
for (const step of ASSESSMENT_STEPS) {
|
|
for (const address of step.writes) expect(declaredAddresses.has(address), `${step.item} 要写 ${address}`).toBe(true);
|
|
for (const address of step.reads.filter((r) => r.includes('.') && !r.startsWith('case:'))) {
|
|
expect(declaredAddresses.has(address), `${step.item} 要读 ${address}`).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('steps[].tools 全部是声明过的工具;只有 S3 调工具', () => {
|
|
for (const step of ASSESSMENT_STEPS) {
|
|
for (const tool of step.tools) expect(declaredTools.has(tool), `${step.item} 要调 ${tool}`).toBe(true);
|
|
if (step.item !== 'S3_assess') expect(step.tools, step.item).toEqual([]);
|
|
}
|
|
expect(ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.tools).toHaveLength(2);
|
|
});
|
|
|
|
it('customer_approval_status 在任何剧本里都不许写(只读边界)', () => {
|
|
for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toMatch(/customer_approval_status\s*=/);
|
|
});
|
|
});
|
|
|
|
describe('doneWhen —— 开局不许真,干完必须真', () => {
|
|
it('**开局一步都不该被自动收掉** —— 五条样例的入参形状逐一试', () => {
|
|
for (const sample of manifest.samples) {
|
|
for (const step of ASSESSMENT_STEPS) {
|
|
if (step.doneWhen === undefined) continue;
|
|
expect(safeEval(step.doneWhen, fresh(sample.input as Record<string, unknown>)), `样例「${sample.name}」下 ${step.item} 开局就为真`).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('S0:两颗候选 + 原 PN = 3 行核对齐、需求与结论都写了 → 收得掉;少写一颗 → 收不掉', () => {
|
|
const base = { candidateInternalPns: ['XL2596S-5.0', 'LM2596S-ADJ'], demand_request_rows: 1, demand_request_missing: 0, [`${D}_verdict_rows`]: 1, [`${D}_pn_check_missing`]: 0 };
|
|
expect(safeEval(doneOf('S0_bind'), { ...base, [`${D}_pn_check_rows`]: 3 })).toBe(true);
|
|
expect(safeEval(doneOf('S0_bind'), { ...base, [`${D}_pn_check_rows`]: 2 })).toBe(false);
|
|
});
|
|
|
|
it('S2:未放弃的候选各一条关系且每条都有明细 → 收得掉;放弃一颗后关系数随之减一', () => {
|
|
const two = { candidateInternalPns: ['A', 'B'], [`${D}_pn_check_exists_status_abandoned`]: 0, substitution_difference_item_rows: 14 };
|
|
expect(safeEval(doneOf('S2_normalize'), { ...two, substitution_relation_rows: 2, substitution_difference_item_distinct_substitution_relation_id: 2 })).toBe(true);
|
|
expect(safeEval(doneOf('S2_normalize'), { ...two, substitution_relation_rows: 2, substitution_difference_item_distinct_substitution_relation_id: 1 })).toBe(false);
|
|
expect(safeEval(doneOf('S2_normalize'), { ...two, [`${D}_pn_check_exists_status_abandoned`]: 1, substitution_relation_rows: 1, substitution_difference_item_distinct_substitution_relation_id: 1 })).toBe(true);
|
|
});
|
|
|
|
it('S3:四态之和 = 明细行数、必填齐、两枚工具都调过 → 收得掉;工具没调过 → 收不掉', () => {
|
|
const ok = {
|
|
substitution_difference_item_missing: 0,
|
|
substitution_relation_missing: 0,
|
|
substitution_difference_item_rows: 7,
|
|
substitution_difference_item_difference_status_match: 4,
|
|
substitution_difference_item_difference_status_mismatch: 1,
|
|
substitution_difference_item_difference_status_unknown: 1,
|
|
substitution_difference_item_difference_status_conditional: 1,
|
|
tool_part_substitution_assessment_compare_parameters_calls: 1,
|
|
tool_part_substitution_assessment_recommend_calls: 1,
|
|
};
|
|
expect(safeEval(doneOf('S3_assess'), ok)).toBe(true);
|
|
expect(safeEval(doneOf('S3_assess'), { ...ok, tool_part_substitution_assessment_recommend_calls: 0 })).toBe(false);
|
|
expect(safeEval(doneOf('S3_assess'), { ...ok, substitution_difference_item_difference_status_match: 3 })).toBe(false);
|
|
});
|
|
|
|
it('S5:旗 + 全部关系 FAE 确认 + P-01 回写齐 → 收得掉;缺一头都收不掉', () => {
|
|
const ok = { landing_prepared_total: 1, substitution_relation_analysis_status_fae_confirmed: 2, substitution_relation_rows: 2, substitution_relation_missing: 0, demand_request_missing: 0 };
|
|
expect(safeEval(doneOf('S5_prepare_landing'), ok)).toBe(true);
|
|
expect(safeEval(doneOf('S5_prepare_landing'), { ...ok, substitution_relation_analysis_status_fae_confirmed: 1 })).toBe(false);
|
|
expect(safeEval(doneOf('S5_prepare_landing'), { ...ok, demand_request_missing: 1 })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('说明书与剧本的骨架', () => {
|
|
it('五张卡的骨架都住在说明书里(压拍之后只有说明书还在)', () => {
|
|
expect(NARRATIVE).toContain('"kind":"entry","itemId":"S0_bind"');
|
|
expect(NARRATIVE).not.toContain('"kind":"decide","itemId":"S0d_p08_master"');
|
|
// 两张数据工单都得标 variant:"data" —— 对话区凭它换成「数据工单」那副面孔
|
|
expect(NARRATIVE.match(/"variant":"data"/g)?.length).toBe(2);
|
|
// 但模型抄卡会把它丢掉(2026-08-27 实测)—— 真正作数的是剧本上的声明,引擎按它决定
|
|
for (const item of ['S0d_p08_master', 'S0d_p10_document']) {
|
|
expect(ASSESSMENT_STEPS.find((s) => s.item === item)?.cardVariant, item).toBe('data');
|
|
}
|
|
expect(ASSESSMENT_STEPS.find((s) => s.item === 'S4_review')?.cardVariant).toBe('review');
|
|
for (const step of ASSESSMENT_STEPS) if (!['S0d_p08_master', 'S0d_p10_document', 'S4_review'].includes(step.item)) expect(step.cardVariant, step.item).toBeUndefined();
|
|
expect(NARRATIVE).toContain('"title":"数据工单 · 芯片主数据创建(P-08)');
|
|
expect(NARRATIVE).toContain('"title":"数据工单 · 原厂技术资料创建(P-10)');
|
|
expect(NARRATIVE).toContain('"title":"评估输入与资料补齐确认');
|
|
expect(NARRATIVE).toContain('"kind":"decide","itemId":"S4_review"');
|
|
});
|
|
|
|
it('剧本里不引反例原话;S3 固定三拍且写明「工具后同拍作废」', () => {
|
|
const s3 = ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.guide ?? '';
|
|
expect(s3).toContain('第一拍');
|
|
expect(s3).toContain('第二拍');
|
|
expect(s3).toContain('第三拍');
|
|
expect(s3).toContain('工具调用后同拍其余动作作废');
|
|
});
|
|
|
|
it('S3 第三拍写盘前先自检两枚工具的调用次数(2026-08-27 实测:漏调 recommend 会让 doneWhen 恒假、模型死循环重写同样的行)', () => {
|
|
const s3 = ASSESSMENT_STEPS.find((s) => s.item === 'S3_assess')?.guide ?? '';
|
|
const thirdPass = s3.slice(s3.indexOf('第三拍'));
|
|
expect(thirdPass).toContain('tool_part_substitution_assessment_recommend_calls');
|
|
expect(thirdPass).toContain('tool_part_substitution_assessment_compare_parameters_calls');
|
|
expect(thirdPass).toMatch(/自检|先.*找这两个数|必须\s*>\s*0/);
|
|
// 第二拍那一段要点出「推荐等级只能由工具算,不调它收不了步」,别让模型自己拍推荐等级
|
|
const secondPass = s3.slice(s3.indexOf('第二拍'), s3.indexOf('第三拍'));
|
|
expect(secondPass).toContain('推荐等级只能由它算');
|
|
expect(secondPass).toMatch(/收不了步|收不掉/);
|
|
});
|
|
|
|
/*
|
|
* 数据工单确认即入库(2026-08-27 用户定:「数据工单里填的数据,确认后就应该入库」)。
|
|
* 基座在确认那一刻把卡上 gateLedgers 那几张表并进正式表;剧本这一头不能再教模型「已经落草稿层」——
|
|
* 那句话会让它以为放行前还能改、还能 drop,而在库的键写下来会被当场拒。
|
|
*/
|
|
it('两张数据工单的剧本都写明:确认后已入正式表,别再写、别 drop 那几行;P-10 的卡骨架也说「确认后即入库」', () => {
|
|
for (const item of ['S0d_p08_master', 'S0d_p10_document']) {
|
|
const guide = ASSESSMENT_STEPS.find((s) => s.item === item)?.guide ?? '';
|
|
expect(guide, item).toContain('入了正式表');
|
|
expect(guide, item).not.toContain('已经落草稿层');
|
|
expect(guide, item).toContain('别去 drop');
|
|
}
|
|
expect(NARRATIVE).toContain('确认后这批主数据即入库');
|
|
expect(NARRATIVE).toContain('确认后这批资料记录即入库');
|
|
});
|
|
|
|
/*
|
|
* S1 的「待确认」行只许来自真写着「未披露」的参数(2026-08-27 用户:「我在第一步已经补充了
|
|
* XL2596S-5.0 使能阈值,当前这一步还要填写一次?」)—— 剧本从前给了一句原话示例
|
|
* 「input_item 如『XL2596S-5.0 使能阈值』」,模型照抄示例、不读现值,把人刚填的 2.5 摆成缺口。
|
|
* 提示词里唯一该出现的原话,是你想听到的那句;示例行一律不引具体料号 + 参数名。
|
|
*/
|
|
/*
|
|
* 第三次同一病(2026-08-27):措辞改了两遍照样复现 —— 台账里每个参数都有值,模型还是写了一行
|
|
* 「使能阈值 · 原厂未披露」。参数值有没有是台账里的事实,不该由模型在清单里宣布:
|
|
* S1 从此**一行参数都不写**,件上摆一张基座现读的「候选参数现值」表;说明书全文不再出现「使能阈值」。
|
|
*/
|
|
it('S1 不写参数行(item_key 不许以 unknown_ 开头);说明书全文没有「使能阈值」这个可照抄的名字', () => {
|
|
const s1 = ASSESSMENT_STEPS.find((s) => s.item === 'S1_input_confirm')?.guide ?? '';
|
|
expect(s1).toContain('候选参数一行都不写');
|
|
expect(s1).toContain('不许以 unknown_ 开头');
|
|
expect(s1).not.toContain('待确认');
|
|
expect(NARRATIVE).not.toContain('使能阈值');
|
|
for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toContain('使能阈值');
|
|
const skeleton = NARRATIVE.slice(NARRATIVE.indexOf('⑤ 执行工单 01'), NARRATIVE.indexOf('"standard":"每一条「待补」行'));
|
|
expect(skeleton).toContain('候选参数现值');
|
|
expect(skeleton).not.toContain('待确认');
|
|
});
|
|
|
|
it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本写明 found 的一颗都不建', () => {
|
|
const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
|
|
expect(s0).toContain('当下可为');
|
|
expect(s0).toMatch(/都在库/);
|
|
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(s0d).toContain('absent');
|
|
expect(s0d).toMatch(/found" 的一行都不建/);
|
|
});
|
|
|
|
it('S0d_p08_master:查无记录一律派数据工单,不问人 —— 决策卡退役(2026-08-27 用户定:「只要有缺失就要自动派数据工单不需要人选择」)', () => {
|
|
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(s0d).toMatch(/一律建,不问人/);
|
|
expect(s0d).toMatch(/exists_status = "found" 的一行都不建/);
|
|
expect(s0d).not.toMatch(/发一张决策卡问人/);
|
|
expect(s0d).not.toMatch(/候选总数\s*≥\s*2/);
|
|
});
|
|
|
|
it('s_bound 哨兵真的检查了「有料号不在库」这件事(都在库时门不该开;退回旧写法会红)', () => {
|
|
expect(CMMN_XML).toContain('sub_eval_pn_check_exists_status_absent > 0');
|
|
const sBoundBlock = /<sentry id="s_bound">[\s\S]*?<\/sentry>/.exec(CMMN_XML)?.[0] ?? '';
|
|
expect(sBoundBlock).toContain('caseFileItemRef="sub_eval_pn_check_exists_status_absent"');
|
|
expect(sBoundBlock).toContain('sub_eval_pn_check_exists_status_absent > 0');
|
|
});
|
|
|
|
it('说明书与全部剧本里不会教模型对计划项调用 disable(允许出现在「不要 disable」这类禁止句里)', () => {
|
|
// 只逮「disable + 计划项 id」这种祈使形(教模型真的去调用它);
|
|
// 「不要…用 `disable`」「Disabled 只能从 Enabled 来」这类禁止句/状态名不含「disable + id」,不会被误伤。
|
|
const disableCallPattern = /disable\s+(S\d[a-zA-Z_]*|T_release)/;
|
|
expect(NARRATIVE).not.toMatch(disableCallPattern);
|
|
for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toMatch(disableCallPattern);
|
|
});
|
|
|
|
it('S0d 拆成两项各自绑自己的 view(P-10 那份件不会永远推不出来)', () => {
|
|
const p08 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master');
|
|
const p10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document');
|
|
expect(p08?.view).toBe('p08-master-create');
|
|
expect(p10?.view).toBe('p10-document-create');
|
|
});
|
|
|
|
/*
|
|
* **两张数据工单的 doneWhen 只读入库事实**(2026-08-27 晚):引擎只在人答过卡之后求它 —— 值是人在件上填的、
|
|
* 引擎数得出来,不再靠模型记得 finish(剧本写了两处它照样忘,件停在 P-08)。
|
|
*/
|
|
it('P-08:要建的每颗料主数据与参数都落了、必填不缺 → 收得掉;少建一颗 / 还缺必填 / 参数行对不上账 → 收不掉', () => {
|
|
const ok = { parts_profile_rows: 2, [`${D}_pn_check_exists_status_absent`]: 2, [`${D}_pn_check_exists_status_created`]: 0, parts_profile_missing: 0, parts_parameter_missing: 0, parts_parameter_rows: 14, parts_parameter_distinct_param_name: 7 };
|
|
expect(safeEval(doneOf('S0d_p08_master'), ok)).toBe(true);
|
|
// 模型把其中一颗改成 created 也一样算(absent + created 才是要建的料数)
|
|
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, [`${D}_pn_check_exists_status_absent`]: 1, [`${D}_pn_check_exists_status_created`]: 1 })).toBe(true);
|
|
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_profile_rows: 1, parts_parameter_rows: 7 })).toBe(false);
|
|
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_profile_missing: 1 })).toBe(false);
|
|
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_parameter_rows: 13 })).toBe(false);
|
|
});
|
|
|
|
it('P-10:建过的每颗料都有一行资料记录且必填不缺 → 收得掉;资料少一行 / 骨架还空着 → 收不掉', () => {
|
|
const ok = { parts_document_rows: 2, parts_profile_rows: 2, parts_document_missing: 0 };
|
|
expect(safeEval(doneOf('S0d_p10_document'), ok)).toBe(true);
|
|
expect(safeEval(doneOf('S0d_p10_document'), { ...ok, parts_document_rows: 1 })).toBe(false);
|
|
expect(safeEval(doneOf('S0d_p10_document'), { ...ok, parts_document_missing: 3 })).toBe(false);
|
|
});
|
|
|
|
it('S1 的入场哨兵不再等模型写 created 旗:入库事实齐了门就开', () => {
|
|
const cond = /<sentry id="s_pns_resolved">[\s\S]*?<condition>([\s\S]*?)<\/condition>/.exec(CMMN_XML)?.[1] ?? '';
|
|
expect(cond).not.toContain('sub_eval_pn_check_exists_status_absent = 0');
|
|
expect(cond).toContain('sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created');
|
|
});
|
|
|
|
it('P-08 交回之后的收步不再靠模型记得 finish:剧本写明引擎按入库事实自己收,finish 只是省一拍(2026-08-27 晚:「顺序写死」两处模型照样忘)', () => {
|
|
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(s0d).toContain('finish S0d_p08_master');
|
|
expect(s0d).toMatch(/引擎也会自己收/);
|
|
expect(s0d).not.toMatch(/顺序不能反/);
|
|
const s0d10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document')?.guide ?? '';
|
|
expect(s0d10).toMatch(/引擎按入库事实自己收/);
|
|
});
|
|
|
|
it('「发卡前清单必须先有行」写在说明书里,不只在剧本里(2026-08-27 实测:模型把 P-10 收步与 S1 发卡压成一拍,那一拍 S1 剧本没摊开,卡发出去时 input_gap 一行都没有,人对着空表无从下手)', () => {
|
|
expect(NARRATIVE).toContain('sub_eval.input_gap` 必须先有行');
|
|
expect(NARRATIVE).toMatch(/压拍/);
|
|
expect(NARRATIVE).toMatch(/顺序不能反/);
|
|
});
|
|
|
|
it('存在性核对的剧本教的是「填得出依据才算在库」,不是「你看着办」(结构治,配 found_pn_ref 那道墙)', () => {
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
|
|
expect(g).toContain('found_pn_ref');
|
|
expect(g).toMatch(/查无此值当场拒/);
|
|
expect(g).toMatch(/名单里没有就是没有/);
|
|
expect(g).toMatch(/把这一行改成 absent/);
|
|
});
|
|
|
|
it('数据工单的名单只认 absent,「建哪几颗」与「要不要问人」分开写(2026-08-27 实测:原 PN 缺库那单,模型把在库的候选派成了数据工单,缺的那颗没人建)', () => {
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(g).toMatch(/【名单】/);
|
|
expect(g).toMatch(/一律建,不问人/);
|
|
expect(g).toMatch(/exists_status = "found" 的一行都不建/);
|
|
});
|
|
|
|
it('数据录入卡自带 basis,不让基座兜上内部家具(2026-08-27 实测:没声明 basis 的那张卡,参考信息栏摆的是「这一步 / 当下可为 / 目标缺口」加九个计划项全名)', () => {
|
|
const i = NARRATIVE.indexOf('"kind":"entry"');
|
|
expect(i).toBeGreaterThan(-1);
|
|
const block = NARRATIVE.slice(i, i + 900);
|
|
expect(block).toContain('"basis"');
|
|
expect(block).toContain('触发原因');
|
|
});
|
|
|
|
it('三张执行工单各自声明 gateLedgers —— 零输入卡不声明,闸就盯着与它无关的表(2026-08-27 e2e 实测:两张数据工单空手交卷都放行了)', () => {
|
|
for (const [item, ledger] of [
|
|
['芯片主数据创建', '"gateLedgers":["parts.profile","parts.parameter"]'],
|
|
['原厂技术资料创建', '"gateLedgers":["parts.document"]'],
|
|
['评估输入与资料补齐确认', '"gateLedgers":["sub_eval.input_gap"]'],
|
|
] as const) {
|
|
expect(NARRATIVE, item).toContain(ledger);
|
|
}
|
|
});
|
|
|
|
it('数据工单也要「先写骨架再发卡」写进说明书,且哨兵不认零行主数据(2026-08-27 e2e:模型压拍没写骨架就发卡,人打开是空表,零行同时骗过闸与哨兵,一路走到 S2 才被引用检查拦下)', () => {
|
|
// 两张数据工单各要一条(P-08 的 profile/parameter、P-10 的 document)
|
|
expect(NARRATIVE.match(/必须先有骨架行/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
|
|
expect(NARRATIVE).toContain('parts.document` 必须先有骨架行');
|
|
expect(NARRATIVE).toMatch(/压拍/);
|
|
// 标了几颗建好,主数据就得有几行 —— 逐颗对账(第一刀写成「≥1 行就算数」太松,三候选那单从缝里过去了)
|
|
expect(CMMN_XML).toContain('parts_profile_rows >= sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created');
|
|
});
|
|
|
|
it('评估输入那张卡的**前提**写在说明书的卡骨架上(2026-08-27 e2e:两颗缺库,模型只问只建了第一颗,CN 还 absent 就把 S1 的卡发了出去 —— 发卡的授权面是调人预算,绕得过哨兵)', () => {
|
|
expect(NARRATIVE).toContain('一颗 absent 都不剩');
|
|
expect(NARRATIVE).toMatch(/有几颗 absent 就要办几颗/);
|
|
});
|
|
|
|
it('发数据录入卡那一拍不许顺手写需求行(客户还没落草稿层,引用检查必拒)', () => {
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
|
|
expect(g).toMatch(/那一拍只发卡/);
|
|
expect(g).toContain('引用检查');
|
|
});
|
|
|
|
it('absent 没清零之前不许收掉数据工单那一项(2026-08-27 e2e:建完第一颗就 finish,再想给第二颗发卡撞上「这一步已经办完收掉了」,连拒四次)', () => {
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(g).toMatch(/还有没有 absent 的料号/);
|
|
expect(g).toMatch(/这一项千万别 finish/);
|
|
expect(g).toMatch(/收掉了就回不去了/);
|
|
});
|
|
|
|
it('参数名一律中文、与库里既有行同名(2026-08-27 e2e:模型写成 input_voltage_range,而原 PN 是「输入电压范围」—— 主键的一半对不上,27 条里 14 条判成 UNKNOWN)', () => {
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(g).toMatch(/参数名一律用中文/);
|
|
// 措辞写过两遍都没拦住 → 上结构:把「同一套名字」变成基座算得出的数
|
|
expect(CMMN_XML).toContain('parts_parameter_rows = parts_parameter_distinct_param_name * (sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created)');
|
|
expect(g).toContain('input_voltage_range');
|
|
expect(g).toMatch(/不是同一行/);
|
|
});
|
|
|
|
it('第二颗缺库的候选也要先问(每一颗都有被放弃的权利)', () => {
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(g).toMatch(/不问人,直接建/);
|
|
expect(g).not.toMatch(/这一颗也要先发一张决策卡问人/);
|
|
});
|
|
|
|
it('created 认的是「卡交回来了」,不是「表里有行」—— 写骨架 ≠ 建好了(2026-08-27 e2e:模型铺完骨架就标 created、直接 finish,撞「从没给它发过卡」连拒五拍)', () => {
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
|
|
expect(g).toMatch(/写骨架 ≠ 建好了/);
|
|
expect(g).toMatch(/写骨架 → \*\*发卡\*\* → 这一拍结束 → 人交回/);
|
|
expect(g).not.toMatch(/只有它在 \\`parts\.profile\\` 里真有一行/);
|
|
});
|
|
|
|
it('件上三处样式毛病钉死(2026-08-27 用户:表显示不全不能右滑 / 编辑态内部料号只读字与输入框并排 / input 与 select 没对齐)', () => {
|
|
const html = manifest.assets.viewTemplates.find((v) => v.name === 'p08-master-create')?.html ?? '';
|
|
// 两种包装都要能横滑:验收件用目标件的 .tbl-wrap(它的验收版写着 overflow:hidden),2026-08-27 用户在结论审核页撞上
|
|
// 运营侧的看板也在这条纪律里(它六张表都宽)
|
|
for (const v of manifest.assets.viewTemplates) expect(v.html, v.name).toContain('.jc-tbl-wrap, .tbl-wrap { overflow-x: auto;');
|
|
expect(html).toContain('#tbl1.on td > .jc-vcell:not(.ro) { display: none; }');
|
|
expect(html).toContain('input.jc-cell, select.jc-cell { height: 32px;');
|
|
// 徽标不再包 .ro(编辑态要随只读格一起藏,只留下拉框)
|
|
expect(html).not.toContain('<span class="jc-vcell ro"><span class="stx"');
|
|
});
|
|
|
|
it('表头与「字段」格只留中文:没有英文列名、没有 .thd 小字 —— 五张件一律(2026-08-27 用户在数据工单页、审核页各指出一次)', () => {
|
|
for (const v of manifest.assets.viewTemplates) {
|
|
expect(v.html, v.name).not.toContain('class="thd"');
|
|
for (const th of v.html.match(/<th[^>]*>[^<]*<\/th>/g) ?? []) expect(th, `${v.name} th`).not.toMatch(/[a-z]_[a-z]/);
|
|
for (const td of v.html.match(/<td class="cust">[^<]*<\/td>/g) ?? []) expect(td, `${v.name} 字段格`).not.toMatch(/[a-z]_[a-z]/);
|
|
}
|
|
});
|
|
|
|
it('审核通过与否读基座数 decision_last,不由模型自写的旗说了算;驳回分支明写不许写旗、不许 finish(2026-08-27 实测:人答「重做」它照样推到放行)', () => {
|
|
expect(CMMN_XML).toContain('decision_last = "approve"');
|
|
expect(CMMN_XML).toContain('<caseFileItem id="decision_last"');
|
|
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S4_review')?.guide ?? '';
|
|
expect(g).toMatch(/不许写 review_approved_total、不许 finish/);
|
|
expect(g).toMatch(/ask 的开头必须写明这一轮改了什么/);
|
|
});
|
|
|
|
it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => {
|
|
expect(ASSESSMENT_PHASES).toHaveLength(5);
|
|
for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10);
|
|
const all = ASSESSMENT_PHASES.flatMap((p) => p.items);
|
|
expect(new Set(all).size).toBe(all.length);
|
|
expect([...all].sort()).toEqual([...ITEM_IDS.filter((i) => i !== 'T_release')].sort());
|
|
});
|
|
});
|
|
|
|
/*
|
|
* 人工验收清单(2026-08-27 用户定):验收卡上的条目是人核这份工单要走的步骤,不是系统判的判据;
|
|
* 每份验收工单固定 —— 差异评估结论审核(S4_review 审核卡)与评审放行与落账确认(T_release 终局验收卡)各一份。
|
|
*/
|
|
describe('两张验收工单的人工验收清单', () => {
|
|
it('S4_review 与 T_release 各声明一份固定清单:至少 4 条,每条是一句可核对的动作,互不重复', () => {
|
|
for (const item of ['S4_review', 'T_release']) {
|
|
const list = ASSESSMENT_STEPS.find((s) => s.item === item)?.acceptanceChecklist ?? [];
|
|
expect(list.length, item).toBeGreaterThanOrEqual(4);
|
|
expect(new Set(list).size, item).toBe(list.length);
|
|
for (const line of list) expect(line.length, `${item}: ${line}`).toBeGreaterThan(12);
|
|
}
|
|
// 别的步骤不发验收卡,不该有清单
|
|
for (const step of ASSESSMENT_STEPS) {
|
|
if (step.item === 'S4_review' || step.item === 'T_release') continue;
|
|
expect(step.acceptanceChecklist, step.item).toBeUndefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
/*
|
|
* 评估输入清单每行六列都要写(2026-08-27 实测:模型 11 行全漏了必填列 input_item,件上红带点名「缺输入项」,
|
|
* 人把别的格填满也过不了闸;而剧本从前只在括号里提过一次 input_item,逐行的做法里没再说)。
|
|
*/
|
|
describe('S1 清单:每行六列都要写', () => {
|
|
it('剧本逐行写明 input_item 的人读名与 source_ledger,并让模型发卡前核 _missing = 0', () => {
|
|
const s1 = ASSESSMENT_STEPS.find((s) => s.item === 'S1_input_confirm')?.guide ?? '';
|
|
expect(s1).toContain('每一行六列都要写');
|
|
for (const name of ['customer「客户」', 'request「客户需求」', 'original_pn「原 PN」', 'application_scenario「应用场景」', 'bom_position「BOM 位」', 'original_doc_version「原 PN 资料版本」']) {
|
|
expect(s1, name).toContain(name);
|
|
}
|
|
expect(s1).toContain(`${D}_input_gap_missing`);
|
|
});
|
|
});
|
|
|
|
/* 放行那一步只 submit,不发工单(2026-08-27 实测:先一张执行单「评审放行与落账确认」,人答完才 submit 又一张验收单) */
|
|
describe('T_release 只 submit', () => {
|
|
it('剧本写明不发执行工单 / 决策卡', () => {
|
|
const t = ASSESSMENT_STEPS.find((s) => s.item === 'T_release')?.guide ?? '';
|
|
expect(t).toContain('不发执行工单');
|
|
expect(t).toContain('submit');
|
|
});
|
|
});
|