/* * 커널↔보드 capability 매핑 무결성 — 스트림라인의 척추 검증. * 커널이 타입별 capability 를 선언(SSOT)하고, 그 능력의 관측 상태필드가 data(tag) payload 로 흘러 * 보드 컴포넌트에 도달한다. 이 브릿지가 끊기면 모니터링이 성립 안 한다. * * 여기서 검증하는 절반 = 커널→payload(Node 로 완결). 나머지 절반(payload→컴포넌트 렌더)은 클라 하네스. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { DOMAIN_CATALOG, CAPABILITIES, CAPABILITY_KEYS, capabilitiesForType, stateFieldsOf } from '@operato/ops-contract' import { buildEntityDeltas } from '../shared/entity-delta.ts' const SYSTEMS = ['wms', 'yms', 'mes'] as const test('카탈로그 무결성: 모든 타입의 capability 가 정의된 능력집합에 속함 (dangling/오타 0)', () => { for (const sys of SYSTEMS) { for (const t of DOMAIN_CATALOG[sys].types) { assert.ok(Array.isArray(t.capabilities), `${sys}/${t.key} capabilities 배열`) for (const c of t.capabilities) { assert.ok(CAPABILITY_KEYS.includes(c), `${sys}/${t.key}: '${c}' 는 정의된 능력`) } } } }) test('capabilitiesForType: 타입→능력 해소(SSOT)', () => { assert.deepEqual(capabilitiesForType('wms', 'storage'), ['storable'], 'storage=storable') assert.ok(capabilitiesForType('wms', 'forklift').includes('mobile'), 'forklift 는 mobile') assert.ok(capabilitiesForType('wms', 'forklift').includes('operable'), 'forklift 는 operable') assert.deepEqual(capabilitiesForType('wms', '__nope__'), [], '미지 타입 → []') }) test('능력 정의 완전성 + 직교(어느 상태필드도 두 능력에 중복 없음)', () => { const seen = new Map() for (const k of CAPABILITY_KEYS) { assert.ok(CAPABILITIES[k], `${k} 정의 존재`) assert.ok(CAPABILITIES[k].stateFields.length > 0, `${k} stateFields ≥1`) for (const f of CAPABILITIES[k].stateFields) { assert.ok(!seen.has(f), `상태필드 '${f}' 직교 위반: ${seen.get(f)} & ${k}`) seen.set(f, k) } } }) test('능력 소싱 구조: 4 자원/위치 능력은 카탈로그 타입에서, trackable 은 런타임 오더에서', () => { // 매핑 구조: 노드/무버 타입은 카탈로그가 capability 를 선언(정적 소싱). // trackable(오더/아이템 생애)은 런타임 생성 엔티티라 타입 카탈로그에 없고, // board-placement-twin-provider 가 __orders__ 집약에 직접 부여(런타임 소싱). 이 분리가 매핑의 실제 구조. const usedByType = new Set() for (const sys of SYSTEMS) for (const t of DOMAIN_CATALOG[sys].types) for (const c of t.capabilities) usedByType.add(c) // 카탈로그가 쓰는 능력은 모두 정의됨(dangling 0) for (const c of usedByType) assert.ok(CAPABILITY_KEYS.includes(c as any), `카탈로그 사용 능력 '${c}' 정의됨`) // 4 자원/위치 능력은 각기 최소 1 타입에서 사용(죽은 정의 감지) for (const k of ['operable', 'storable', 'mobile', 'processable'] as const) { assert.ok(usedByType.has(k), `'${k}' 는 최소 1 카탈로그 타입에서 사용`) } // trackable 은 타입 카탈로그에 없음 = 런타임(오더) 스코프임을 명시 (고아가 아니라 다른 소싱 경로) assert.ok(!usedByType.has('trackable'), 'trackable 은 타입 카탈로그가 아니라 런타임 오더(__orders__)에서 소싱') }) test('★ 스트림라인 브릿지: 능력의 상태필드가 라이브 payload 에 실림 (커널→보드 도달)', () => { // storable 노드 → occupancy/capacity (+ operable status) const nodeD: any = buildEntityDeltas({ locations: [{ id: 'n', type: 'storage', occupancy: 2, capacity: 9, status: 'idle' }] }, 't1')[0].data for (const f of stateFieldsOf(['storable'])) assert.ok(f in nodeD, `storable payload.${f}`) assert.ok('status' in nodeD, 'operable(노드 status) payload') // mobile+operable 무버 → location/motion/status const equipmentD: any = buildEntityDeltas({ equipment: [{ id: 'm', kind: 'forklift', location: 'n', status: 'busy', motion: { toNode: 'n2', progress: 0.3 } }] }, 't1').find(x => x.tag === 'm')!.data for (const f of stateFieldsOf(['mobile'])) assert.ok(f in equipmentD, `mobile payload.${f}`) for (const f of stateFieldsOf(['operable'])) assert.ok(f in equipmentD, `operable payload.${f}`) // trackable 오더 → progress/held (lifecycle 은 payload 에서 'status' 로 표면화 — 이름-매핑 시임) const orderD: any = buildEntityDeltas({ orders: [{ id: 'o', status: 'picking', progress: 0.5, held: false }] }, 't1').find(x => x.tag === 'o')!.data assert.ok('progress' in orderD && 'held' in orderD, 'trackable progress/held payload') assert.ok('status' in orderD, 'trackable lifecycle → payload.status (이름-매핑)') }) test('시임 문서화 · processable output(양품/불량)은 payload oee.good/scrap 으로 표면화', () => { // 계약 stateField = 'output' 이나, 라이브 payload 는 이를 oee(정수%+good/scrap)로 집약해 표면화. const d: any = buildEntityDeltas({ equipment: [{ id: 'c', kind: 'cutter', status: 'busy', oee: { overall: 0.8, availability: 0.9, performance: 0.9, quality: 0.98, goodCount: 10, nonconformingCount: 1 } }] }, 't1').find(x => x.tag === 'c')!.data assert.ok(d.oee, 'processable → payload.oee 존재') assert.equal(d.oee.good, 10, 'output.good = oee.good') assert.equal(d.oee.scrap, 1, 'output.scrap = oee.scrap') })