/* * 트윈의 고정 유입 규칙이 **표준 헤더 필드를 세 갈래 모두에서** 통과시키나. * * ── 왜 필요한가 (2026-09-07) ──────────────────────────────────────────────── * 같은 결함이 이 저장소에서 네 번 났고, 네 번 다 **층이 달랐다.** * * ``` * 1 EpcisHeaderOptions 에 칸이 없다 objectEvent 만 자기 인자로 갖고 있었다 * 2 매핑 스펙에 칸이 없다 커넥터가 선언할 자리가 없었다 * 3 이 파일의 규칙 셋에 칸이 없다 선언해도 여기서 사라졌다 * 4 받는 쪽 — 이건 처음부터 서 있었다 * ``` * * 세 층이 각각 새는 자리였고, **아래 두 층을 고친 뒤에도 위층에서 계속 사라졌다.** 계약을 열고 * 「됐다」고 한 것이 아니라 인티그레이션 레인이 이 규칙 그대로 `mapRecord` 를 돌려 봐서 찾았다. * * 그래서 이 시험은 **계약이 아니라 이 파일의 규칙**을 돌린다. 계약 쪽 시험이 초록이어도 여기서 * 사라질 수 있다는 것이 오늘 실제로 일어난 일이다. */ import test from 'node:test' import assert from 'node:assert/strict' import { mapRecordChecked } from '@operato/ops-contract' import { canonicalRulesForTest } from '../server/engine/canonical-ingest.js' const AT = '2026-09-07T02:00:00.000Z' /** 세 갈래가 각자 성립하는 최소 레코드 — 헤더 값은 같은 것을 싣는다. */ const HEADER = { bizStep: 'urn:epcglobal:cbv:bizstep:storing', disposition: 'urn:epcglobal:cbv:disp:in_progress', readPoint: 'urn:epc:id:sgln:0614141.00001.0', bizLocation: 'urn:epc:id:sgln:0614141.00002.0', bizTransactionList: [{ type: 'urn:epcglobal:cbv:btt:po', bizTransaction: 'urn:epcglobal:cbv:bt:0614141:order-33' }], errorDeclaration: { declarationTime: AT, reason: 'urn:epcglobal:cbv:er:incorrect_data' } } const RECORDS: { sourceType: string; record: Record }[] = [ { sourceType: 'twin', record: { ...HEADER, action: 'OBSERVE', epc: 'urn:epc:id:sgtin:0614141.107346.2018' } }, { sourceType: 'twin-transformation', record: { ...HEADER, inputEPCList: ['urn:epc:id:sgtin:0614141.107346.1000'], outputEPCList: ['urn:epc:id:sgtin:0614141.107346.2000'], transformationID: 'urn:uuid:11111111-2222-3333-4444-555555555555' } }, { sourceType: 'twin-aggregation', record: { ...HEADER, action: 'ADD', parentID: 'urn:epc:id:sscc:0614141.1234567890', childEPCs: ['urn:epc:id:sgtin:0614141.107346.2018'] } } ] function mapWith(sourceType: string, record: Record): any { const rule = canonicalRulesForTest().find(r => r.sourceType === sourceType) assert.ok(rule, `${sourceType} 규칙이 없다 — 갈래가 사라졌다`) return mapRecordChecked(record as any, rule!.mapping as any, AT) } test('규칙이 셋이다 — 갈래가 줄면 그 사실이 조용히 사라진다', () => { assert.deepEqual( canonicalRulesForTest().map(r => r.sourceType).sort(), ['twin', 'twin-aggregation', 'twin-transformation'] ) }) for (const { sourceType, record } of RECORDS) { test(`${sourceType} 이 오더 참조를 커널까지 넘긴다 — 이 파일에서 사라지던 자리다`, () => { const { event, errors } = mapWith(sourceType, record) assert.deepEqual(errors, [], `${sourceType}: 성립하는 레코드인데 오류가 났다`) assert.deepEqual( event.bizTransactionList, HEADER.bizTransactionList, `${sourceType}: 오더 참조가 사라졌다 — 커넥터가 실어도 저널의 biz_transaction 이 빈다` ) }) test(`${sourceType} 이 나머지 헤더 필드도 함께 넘긴다 — 하나만 보면 다음 것이 또 샌다`, () => { const { event } = mapWith(sourceType, record) assert.equal(event.bizStep, HEADER.bizStep) assert.equal(event.disposition, HEADER.disposition) assert.equal(event.readPoint?.id, HEADER.readPoint) assert.equal(event.bizLocation?.id, HEADER.bizLocation) assert.equal(event.errorDeclaration?.declarationTime, AT) }) } test('오더 참조를 안 실으면 그 칸을 만들지 않는다 — 빈 배열은 「참조가 없다」와 다르다', () => { const { bizTransactionList: _drop, ...bare } = RECORDS[0].record const { event, errors } = mapWith('twin', bare) assert.deepEqual(errors, []) assert.equal('bizTransactionList' in event, false) })