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([]); } }); });