/* * **라이브 문이 운영 사실을 안다** — 미러가 시뮬보다 가난하지 않게 (ADR-0029 어휘 넓히기, 2026-08-19). * * ── 무엇이 막혀 있었나 ────────────────────────────────────────────────────── * 이 문(`ingestCanonicalRecords`)은 어휘를 셋만 알았다: EPCIS 품목 · 에너지 계량 · 설비 에너지. 그리고 * **모르는 것은 EPCIS 로 떨어졌다** — 작업 사실을 넣으면 `epc` 가 없어 거부됐고, 원본은 「이 작업이 * 끝났다」를 말할 길이 없었다. 시뮬 커널만 그것을 알고 미러는 영원히 몰랐다. * * ── 이 시험이 지키는 것 ───────────────────────────────────────────────────── * ① 네 어휘가 **한 문**에서 갈린다(판정은 커널의 것이고 여기서 다시 짐작하지 않는다). * ② 운영 사실이 자기 봉투로 나온다 — EPCIS 로 감싸지지 않는다. * ③ 섞여 와도 갈린다(실 피드는 한 배치에 여러 어휘가 섞인다). * ④ 거부는 이유와 함께 남는다 — 피드 결선이 그 이유를 사람에게 말한다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { ingestCanonicalRecords } from '../server/engine/canonical-ingest.ts' const AT = '2026-08-19T09:00:00.000Z' test('운영 사실이 자기 봉투로 나온다 — EPCIS 로 감싸지지 않는다', () => { const { accepted, rejected } = ingestCanonicalRecords( [{ taskId: 'tk-1', kind: 'putaway', status: 'completed' } as any], 'domain-1', AT ) assert.deepEqual(rejected, []) assert.equal(accepted.length, 1) assert.equal(accepted[0].eventType, 'task.status') assert.equal(accepted[0].eventTime, AT, '시각을 주지 않으면 이 문이 정한 기본 시각을 쓴다') assert.equal(accepted[0].tenantId, 'domain-1') }) test('네 어휘가 한 배치에서 갈린다 — 실 피드는 섞여 온다', () => { const { accepted, rejected } = ingestCanonicalRecords( [ { epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' }, { meterId: 'm-1', kW: 120 }, { equipmentId: 'pv-1', generatedKW: 40 }, { moverId: 'fk-1', kind: 'forklift', status: 'busy' }, // vocabulary-guard: allow 저널 와이어 필드 { orderId: 'or-1', kind: 'outbound', status: 'picking', requested: 10, fulfilled: 3 } ] as any, 'domain-1', AT ) assert.deepEqual(rejected, [], '어느 어휘도 남의 길로 떨어지지 않는다') assert.deepEqual(accepted.map(e => e.eventType).sort(), [ 'energy.equipment', 'energy.measured', 'epcis.ObjectEvent', 'equipment.status', 'order.status' ].sort()) }) test('거부는 이유와 함께 남는다 — 유효한 것은 함께 통과한다', () => { const { accepted, rejected } = ingestCanonicalRecords( [ { taskId: 'tk-1', kind: 'pick', status: 'completed' }, { taskId: 'tk-2', kind: 'pick', status: 'FINISHED' } // 커널이 다룰 수 없는 낱말 ] as any, 'domain-1', AT ) assert.equal(accepted.length, 1) assert.equal(rejected.length, 1) assert.ok(rejected[0].errors.join(' ').length > 0) }) test('피드 결선이 거부를 알리지 않고 버리지 않는다 — 이유를 사람에게 말한다', () => { const src = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-live.ts', import.meta.url)), 'utf-8') assert.match(src, /record\(s\) rejected/, '거부 수와 이유를 남긴다') }) /* * ── 담김도 이 문을 지난다 (2026-08-19) ────────────────────────────────────── * 실 앱에서 한 바퀴 돌리자 「옮기지 못했다」 로그가 `epcis.AggregationEvent` 를 짚었다 — 관측 리듀서는 그 * 사실로 자식 물품에 `parent` 를 붙이는데(3D 가 「이 상자가 어느 팔레트에 실렸나」를 묻는 자리) 문에 그 * 어휘가 없었다. */ test('담김 레코드가 자기 봉투로 나온다 — 개체 관측과 한 배치에 섞여 와도 갈린다', () => { const { accepted, rejected } = ingestCanonicalRecords( [ { epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' }, { parentID: 'urn:epc:id:sscc:0614141.0000000001', childEPCs: ['urn:epc:id:sgtin:9521321.012345.1'], action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:packing', readPoint: 'packing' } ] as any, 'domain-1', AT ) assert.deepEqual(rejected, []) assert.deepEqual(accepted.map(e => e.eventType), ['epcis.ObjectEvent', 'epcis.AggregationEvent']) assert.equal((accepted[1].data as any).parentID, 'urn:epc:id:sscc:0614141.0000000001') }) test('자리를 구조째 실으면 거부한다 — 예전에는 "[object Object]" 로 굳어 저널에 쌓였다', () => { const { accepted, rejected } = ingestCanonicalRecords( [{ epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: { id: 'dock-1' } }] as any, 'domain-1', AT ) assert.equal(accepted.length, 0) assert.match(rejected[0].errors.join(' '), /readPoint/) }) test('이 문이 어휘가 넷임을 스스로 말한다 — 다음 사람이 세 어휘로 읽지 않게', () => { const src = readFileSync(fileURLToPath(new URL('../server/engine/canonical-ingest.ts', import.meta.url)), 'utf-8') assert.match(src, /어휘가 넷이다/) assert.match(src, /isOperationalRecord/, '판정은 커널의 것을 부른다(여기서 다시 짐작하지 않는다)') })