Archived
在库与否是一次 lookup 的结果,不是一句断言:先取 found_pn_ref(refersTo 查无此值当场拒)再按它判。 参数骨架两颗料都铺、值回填现值 —— 于是「在库但当年参数没录全」也自动被红带拦住。 发卡的条件只用基座读数:gate_reason 为空串时那一列不写、未绑定,而 S-FEEL 里未绑定的名字 `!= ""` 求值为 true —— 拿它当条件恰恰会在一切正常时把卡发出去。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
184 lines
9.3 KiB
TypeScript
184 lines
9.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { planStepSchema } from '@contracts';
|
|
import { ASSESSMENT_STEPS, ASSESSMENT_PHASES, CMMN_XML, NARRATIVE } from '../src/case-model.ts';
|
|
import { manifest } from '../src/manifest.ts';
|
|
|
|
const ITEMS = ['S1_inputs', 'S2_assess', 'S3_review', 'T_release'];
|
|
const stepOf = (item: string) => ASSESSMENT_STEPS.find((s) => s.item === item);
|
|
|
|
describe('四步案例模型', () => {
|
|
it('恰好四步,次序即业务次序', () => {
|
|
expect(ASSESSMENT_STEPS.map((s) => s.item)).toEqual(ITEMS);
|
|
});
|
|
|
|
it('每一步都过得了 PlanStep 契约', () => {
|
|
for (const step of ASSESSMENT_STEPS) expect(planStepSchema.safeParse(step).success).toBe(true);
|
|
});
|
|
|
|
it('**全程零 guide、零工具** —— 这是这次重写的立场', () => {
|
|
for (const step of ASSESSMENT_STEPS) {
|
|
expect(step.guide).toBe('');
|
|
expect(step.tools).toEqual([]);
|
|
expect(step.auto).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('CMMN 里四个计划项都在,且触达世界的两步标了人工激活', () => {
|
|
for (const item of ITEMS) expect(CMMN_XML).toContain(`id="${item}"`);
|
|
/* 审核与放行触达的是人 —— 完成与否不该由数据形状推出来 */
|
|
for (const item of ['S3_review', 'T_release']) {
|
|
expect(new RegExp(`id="${item}"[^>]*manualActivation="true"`).test(CMMN_XML)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('**触达世界的两步不给 doneWhen** —— 给了等于给引擎一把替人签字的钥匙', () => {
|
|
expect(stepOf('S1_inputs')?.doneWhen).toBeTruthy();
|
|
expect(stepOf('S2_assess')?.doneWhen).toBeTruthy();
|
|
expect(stepOf('S3_review')?.doneWhen).toBeUndefined();
|
|
expect(stepOf('T_release')?.doneWhen).toBeUndefined();
|
|
});
|
|
|
|
it('**判据不许编一个数不出来的数**:doneWhen 里不出现「本类型有几个字段」这类', () => {
|
|
/* `parts_type_param_rows` 数的是本单草稿写下的字段行(人新建的那几个),不是整本字典 —— 拿它当分母是错的 */
|
|
for (const step of ASSESSMENT_STEPS) {
|
|
expect(step.doneWhen ?? '').not.toContain('parts_type_param_rows');
|
|
}
|
|
});
|
|
|
|
it('S1 收步:两颗都在库、都归了类型、且是同一个类型、主数据与参数都不缺', () => {
|
|
const done = stepOf('S1_inputs')?.doneWhen ?? '';
|
|
expect(done).toContain('sub_eval_part_exists_status_found = 2');
|
|
expect(done).toContain('sub_eval_part_missing = 0');
|
|
expect(done).toContain('sub_eval_part_distinct_chip_type_id = 1');
|
|
expect(done).toContain('parts_profile_missing = 0');
|
|
expect(done).toContain('parts_parameter_missing = 0');
|
|
});
|
|
|
|
it('S2 收步:明细写了且不缺、关系一行、验证记录一行', () => {
|
|
const done = stepOf('S2_assess')?.doneWhen ?? '';
|
|
expect(done).toContain('substitution_difference_item_rows > 0');
|
|
expect(done).toContain('substitution_difference_item_missing = 0');
|
|
expect(done).toContain('substitution_relation_rows = 1');
|
|
expect(done).toContain('validation_record_rows = 1');
|
|
});
|
|
|
|
it('两张卡各带人工验收清单 —— 一条一句人眼核得动的动作', () => {
|
|
for (const item of ['S3_review', 'T_release']) {
|
|
const list = stepOf(item)?.acceptanceChecklist ?? [];
|
|
expect(list.length).toBeGreaterThanOrEqual(4);
|
|
for (const line of list) expect(line.length).toBeGreaterThan(6);
|
|
}
|
|
expect(stepOf('S3_review')?.cardVariant).toBe('review');
|
|
});
|
|
|
|
it('阶段:除放行外每一项恰好属于一个阶段', () => {
|
|
const covered = ASSESSMENT_PHASES.flatMap((p) => p.items);
|
|
expect(covered.sort()).toEqual(['S1_inputs', 'S2_assess', 'S3_review'].sort());
|
|
expect(new Set(covered).size).toBe(covered.length);
|
|
});
|
|
|
|
it('说明书只讲整单的事 —— 逐步做法归配方,而配方不是给模型看的', () => {
|
|
expect(NARRATIVE.length).toBeLessThan(2000);
|
|
/* **示例不引具体料号与参数名** —— 模型会把示例当成这一单的值(2026-08-27 复现三次) */
|
|
for (const forbidden of ['LM2596S', 'XL2596S', 'AMS1117', '使能阈值']) {
|
|
expect(NARRATIVE).not.toContain(forbidden);
|
|
}
|
|
});
|
|
});
|
|
|
|
/*
|
|
* 恢复自 Task 2 遗留的两条 `it.skip`(原住在这份文件里,注释指名「Task 2 → Task 3 补回」)——
|
|
* 旧版断言的是旧九步剧本引旧台账地址 / 旧两枚工具,那两个前提随四步重写一起撤了,
|
|
* 这里按新的四步剧本与九张新台账重新写实(逻辑不变:读写面必须落在清单声明里、
|
|
* 工具必须落在声明过的工具里),不是新造的断言。
|
|
*/
|
|
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);
|
|
/* reads 里混着入参名(originalInternalPn 等)与 case: 前缀伪读数,只有「域.对象」形状的才对声明表 */
|
|
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 全部是声明过的工具 —— 且四步全是空数组(零工具是这次重写的立场,不是疏漏)', () => {
|
|
for (const step of ASSESSMENT_STEPS) {
|
|
for (const tool of step.tools) expect(declaredTools.has(tool), `${step.item} 要调 ${tool}`).toBe(true);
|
|
expect(step.tools, step.item).toEqual([]);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('S1 配方', () => {
|
|
const recipe = stepOf('S1_inputs')?.auto;
|
|
const acts = recipe?.do ?? [];
|
|
const kinds = acts.map((a) => a.kind);
|
|
|
|
it('六笔,次序是:两颗料 → 案卷 → 主数据骨架 → 参数骨架 → 卡 → 交代', () => {
|
|
expect(kinds).toEqual(['project', 'project', 'project', 'project', 'card', 'say']);
|
|
expect(acts.map((a) => (a.kind === 'project' ? a.into : undefined)).filter(Boolean)).toEqual([
|
|
'sub_eval.part', 'sub_eval.case', 'parts.profile', 'parts.parameter',
|
|
]);
|
|
});
|
|
|
|
it('案号由 let 铸,只从本单开案时刻取材', () => {
|
|
expect(recipe?.let?.['evalRef']).toEqual({ cat: ['W05-', { fmt: { in: 'case:openedAt' }, as: 'digits' }] });
|
|
});
|
|
|
|
it('两颗料由字面两行立起来 —— 这张表是后面每一笔遍历的唯一来源', () => {
|
|
const part = acts[0];
|
|
expect(part?.kind === 'project' && 'rows' in part.from && part.from.rows).toHaveLength(2);
|
|
/* 在库与否是一次 lookup 的结果,不是一句断言:先取依据,再按依据判 */
|
|
const fields = part?.kind === 'project' ? part.fields : undefined;
|
|
expect(fields?.['found_pn_ref']).toMatchObject({ lookup: 'parts.profile', take: 'internal_pn' });
|
|
expect(fields?.['exists_status']).toEqual({ when: 'found_pn_ref != ""', then: 'found', else: 'absent' });
|
|
});
|
|
|
|
it('参数骨架:两颗料都铺、值回填现值 —— 「在库但当年参数没录全」也要被拦住', () => {
|
|
const params = acts[3];
|
|
expect(params?.kind === 'project' && params.prune).toBe(true);
|
|
const from = params?.kind === 'project' ? params.from : undefined;
|
|
/* 外层是本单两颗料,里层是该类型的字段清单 —— 人没挑类型时 where 一行不命中,什么也不铺 */
|
|
expect(from).toMatchObject({ each: 'mine:sub_eval.part', then: { each: 'parts.type_param' } });
|
|
const fields = params?.kind === 'project' ? params.fields : undefined;
|
|
expect(fields?.['param_value']).toMatchObject({ lookup: 'parts.parameter', take: 'param_value' });
|
|
});
|
|
|
|
it('**发卡的条件全是基座读数** —— 不许拿 `gate_reason != ""` 当条件', () => {
|
|
const card = acts[4];
|
|
const when = card?.kind === 'card' ? (card.when ?? '') : '';
|
|
/*
|
|
* S-FEEL 里**未绑定的名字 `!= ""` 求值为 true**,而 `gate_reason` 为空串时那一列压根不写、
|
|
* 于是未绑定 —— 拿它当条件,恰恰在一切正常时把卡发出去。条件只用恒有值的基座读数。
|
|
*/
|
|
expect(when).not.toContain('gate_reason');
|
|
expect(when).toContain('sub_eval_part_exists_status_found < 2');
|
|
expect(when).toContain('parts_parameter_missing > 0');
|
|
});
|
|
|
|
it('卡:数据工单、四张表都在闸上、且有 basis(基座拒发 basis 为空的工单)', () => {
|
|
const card = acts[4];
|
|
expect(card?.kind === 'card' && card.as).toBe('execution');
|
|
expect(card?.kind === 'card' && card.variant).toBe('data');
|
|
expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.chip_type', 'parts.type_param', 'parts.profile', 'parts.parameter']);
|
|
expect((card?.kind === 'card' ? card.basis ?? [] : []).length).toBeGreaterThan(0);
|
|
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it('发卡条件与 doneWhen 是同一件事的正反面', () => {
|
|
const when = acts[4]?.kind === 'card' ? (acts[4].when ?? '') : '';
|
|
for (const reading of [
|
|
'sub_eval_part_exists_status_found', 'sub_eval_part_missing',
|
|
'sub_eval_part_distinct_chip_type_id', 'parts_profile_missing', 'parts_parameter_missing',
|
|
]) {
|
|
expect(when).toContain(reading);
|
|
expect(stepOf('S1_inputs')?.doneWhen).toContain(reading);
|
|
}
|
|
});
|
|
});
|