/** * ACT-WORK-05 差异评估的包内自测 —— **只准 import @sdk / @contracts / @sfeel**。 * 案例活动没有 `run`,自测不跑回合;测的是清单本身与授权面对齐,结构照 E04 / I01。 * * 本文件目前只写三组(清单本身合规 / 台账声明 / 活动入口); * 「案例模型与清单的对齐」「报告模版:编辑面与只读面各就各位」两组留给 Task 4/5 追加 —— * `src/case-model.ts` 与 `src/views/report.ts` 眼下还是占位,写了那两组只会红。 */ import { describe, expect, it } from 'vitest'; import { activityManifestSchema } from '@contracts'; import { manifest, D, RULE_VERSION } from '../src/manifest.ts'; import activity from '../src/index.ts'; const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm']; const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object); describe('清单本身合规', () => { it('过 @contracts 的 schema', () => { expect(() => activityManifestSchema.parse(manifest)).not.toThrow(); }); it('没有静态监听(等外部回音一律落在执行工单上,listen 在本仓不可用)', () => { expect(manifest.declarations.listeners).toEqual([]); }); it('L1/L2 判据都给了谓词;L4 三条不给(裁量项);L5 恰一条', () => { for (const c of manifest.contract.acceptanceCriteria) { if (c.level === 'L1' || c.level === 'L2') expect(c.predicate, c.key).toBeDefined(); if (c.level === 'L4') expect(c.predicate, c.key).toBeUndefined(); } expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L4')).toHaveLength(3); expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L5')).toHaveLength(1); }); it('机检谓词只读基座读数与记账旗:不出现活动自报的汇总列名', () => { const forbidden = ['relation_total', 'candidate_total', 'recommended_total', 'not_recommended_total']; for (const c of manifest.contract.acceptanceCriteria) { for (const name of forbidden) expect(c.predicate ?? '', `${c.key} 读了自报数 ${name}`).not.toContain(name); } }); it('三件必交付物都有台账地址;人环预算严格大于「基线 8 + 两轮审核循环 4」', () => { expect(manifest.contract.requiredDeliverables).toHaveLength(3); for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined(); expect(manifest.contract.authorization.humanCallBudget).toBeGreaterThan(12); expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThan(12); }); it('起单只必填 4 项(客户 / 需求 / 原 PN / 候选数组);datasheet 是选填文件', () => { expect(manifest.contract.input.data['required']).toEqual(['customerCode', 'requestDescription', 'originalInternalPn', 'candidateInternalPns']); const properties = manifest.contract.input.data['properties'] as Record; expect(properties['candidateInternalPns']?.type).toBe('array'); expect(properties['datasheets']?.format).toBe('file'); }); it('五条样例:第一条满载走数据工单那条路,名字 ≤14 字,形状各不相同', () => { expect(manifest.samples).toHaveLength(5); for (const s of manifest.samples) expect(s.name.length, s.name).toBeLessThanOrEqual(14); expect(new Set(manifest.samples.map((s) => JSON.stringify(s.input))).size).toBe(5); const first = manifest.samples[0]?.input as { candidateInternalPns?: string[] }; expect(first.candidateInternalPns).toContain('XL2596S-5.0'); const newCustomer = manifest.samples.find((s) => (s.input as { customerName?: string }).customerName !== undefined); expect(newCustomer).toBeDefined(); }); it('五张视图模版都声明了,剧本每一步引用的 view 都在其中', () => { expect(new Set(manifest.assets.viewTemplates.map((v) => v.name))).toEqual(new Set(VIEWS)); for (const step of manifest.assets.businessModel.steps as ReadonlyArray<{ view?: string }>) { if (step.view !== undefined) expect(VIEWS, step.view).toContain(step.view); } }); }); describe('台账声明:授权面与《统一方案》口径', () => { it('P-08 一张表:主键 internal_pn,参数集是它的一个必填字段,没有 parts.parameter 子表', () => { const p08 = byAddress('parts', 'profile'); expect(p08?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['internal_pn']); expect(p08?.fields.find((f) => f.name === 'normalized_parameter_set')?.required).toBe(true); expect(byAddress('parts', 'parameter')).toBeUndefined(); }); it('P-11 主记录的原 PN / 候选 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门)', () => { const rel = byAddress('substitution', 'relation'); for (const name of ['original_internal_pn', 'candidate_internal_pn']) { expect(rel?.fields.find((f) => f.name === name)?.refersTo, name).toBe('parts.profile.internal_pn'); } }); it('客户批准状态**不在声明里**:本活动没有它的写权(只读边界)', () => { const rel = byAddress('substitution', 'relation'); expect(rel?.fields.map((f) => f.name)).not.toContain('customer_approval_status'); }); it('UNKNOWN 必有去向:unknown_disposition 钉在 difference_status = unknown 的行上;四态是 ASCII tally', () => { const item = byAddress('substitution', 'difference_item'); expect(item?.fields.find((f) => f.name === 'unknown_disposition')?.requiredWhen).toEqual({ column: 'difference_status', value: 'unknown' }); expect(item?.fields.find((f) => f.name === 'difference_status')?.tally).toEqual(['match', 'mismatch', 'unknown', 'conditional']); expect(item?.fields.find((f) => f.name === 'substitution_relation_id')?.distinct).toBe(true); }); it('P-01 回写五项钉在 current_stage = gate03 那一刻(放行前不拦,放行门 L2 数得出来)', () => { const p01 = byAddress('demand', 'request'); for (const name of ['risk_level', 'verification_requirement', 'output_ref', 'next_action', 'current_activity']) { expect(p01?.fields.find((f) => f.name === name)?.requiredWhen, name).toEqual({ column: 'current_stage', value: 'gate03' }); } }); it('自持单据都带 caseKey eval_ref 且在主键里;放弃候选必须写原因', () => { for (const object of ['pn_check', 'input_gap', 'verdict']) { const t = byAddress(D, object); expect(t?.kind, object).toBe('record'); expect(t?.caseKey, object).toBe('eval_ref'); expect(t?.fields.find((f) => f.name === 'eval_ref')?.primaryKey, object).toBe(true); } expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' }); }); it('规则版本常量随包冻结', () => { expect(RULE_VERSION).toBe('R-2026.07'); expect(manifest.assets.businessModel.dmn.tables.map((t) => t.id)).toEqual(['d01-difference-status', 'd02-recommendation']); }); }); describe('活动入口', () => { it('没有 run —— 行为全在清单里', () => { expect(activity.run).toBeUndefined(); }); });