/** * 六个活动槽位 —— 契约就绪、实现留白。守:岗位/出口与 MVP 一致; * 声明只有名(工具按名引用、域对象按名取)—— 活动与工具的解耦就靠这一条。 * A5 定价审批是**内置审批点**,零开发、不入库 —— 不在槽位之列。 */ import { describe, expect, it } from 'vitest'; import { MVP_SLOTS, SLOT_ARTIFACT } from '../seed-mvp-slots.ts'; describe('MVP_SLOTS', () => { it('六槽位(A5 审批点内置,不入库)', () => { const ids = MVP_SLOTS.map((m) => m.identity.activityId as string); expect(MVP_SLOTS).toHaveLength(6); expect(ids).toEqual( expect.arrayContaining(['rfq-intake', 'alt-selection', 'sourcing-inquiry', 'cost-pricing', 'quote-negotiation', 'intel-archive']), ); expect(ids).not.toContain('pricing-approval'); }); /* * 占位骨架的形状(2026-07-29 范式统一后):**案例模型**—— * SOP 每件事一个人工任务 + 末尾一张收口验收。两条都是踩过坑才立的: * · 触达世界的项必须人工激活 —— 占位尤其不许替人自动做事 * · 没有收口验收 → 活动永远不 release →「等人却没东西给人答」 */ it('占位骨架:案例模型双表示齐备,项全人工激活,末尾一张收口验收', () => { for (const m of MVP_SLOTS) { const model = m.assets.businessModel; const id = m.identity.activityId; expect(model.cmmnXml, `${id} 缺 CMMN`).toContain('/g)) { expect(task[0], `${id} 的 ${task[1]} 应人工激活`).toContain('manualActivationRule'); } // 占位不做业务判断,所以不带决策表 expect(model.dmn.tables, `${id} 占位期不该有 DMN`).toHaveLength(0); } }); it('A1 出口带两枚去向旗;A2 透传 needSourcing —— 网关条件的数据来源', () => { const a1 = MVP_SLOTS.find((m) => m.identity.activityId === 'rfq-intake')!; const p1 = (a1.contract.output.mandatory[0]!.payload as { properties?: Record }).properties ?? {}; expect(Object.keys(p1)).toEqual(expect.arrayContaining(['needAlternative', 'needSourcing'])); const a2 = MVP_SLOTS.find((m) => m.identity.activityId === 'alt-selection')!; const p2 = (a2.contract.output.mandatory[0]!.payload as { properties?: Record }).properties ?? {}; expect(Object.keys(p2)).toContain('needSourcing'); }); /* * 三个去向是**选择组**,不是必交口里的一个字段:人在放行时恰选一口, * 那一口就是这次运行的 outcome(引擎按选口注入),G4 判的正是它。 * 曾写成 formal 口的必填 outcome + selective: [] —— 引擎只在选择组恰发一口时 * 才注入 outcome,没有选择组就结构上永远没有判据,G4 三条边全不成立(实测)。 */ it('A6 三个去向是选择组 —— 放行恰选一口,选口即 G4 的判据', () => { const a6 = MVP_SLOTS.find((m) => m.identity.activityId === 'quote-negotiation')!; const groups = a6.contract.output.selective; expect(groups).toHaveLength(1); expect(groups[0]!.group).toBe('议价结果'); expect(groups[0]!.ports.map((p) => p.name)).toEqual(['won', 'revise', 'lost']); // outcome 不再是必交口的必填字段 —— 它由选口给出,不靠活动自己写 const formal = a6.contract.output.mandatory[0]!.payload as { required?: string[] }; expect(formal.required ?? []).not.toContain('outcome'); }); it('槽位不携实现:入口是抛「未实现」的占位,工具声明只有名', () => { expect(SLOT_ARTIFACT).toContain('未实现'); for (const m of MVP_SLOTS) { for (const t of m.declarations.tools) expect(typeof t.name).toBe('string'); } }); });