/* * **나간 지시와 돌아온 관측을 잇는다** — 그 판정. * * `dispatchRef` 는 쓰는 곳만 있고 읽는 곳이 없었다. 그래서 조치를 넘긴 뒤 저쪽에서 그것이 어떻게 * 됐는지 트윈이 몰랐고, 화면은 「넘겼다」까지만 보여 주고 끝난 것처럼 읽혔다. * * 여기서 지키는 것은 셋이다. * * 이어지나 우리가 낸 지시의 식별자로 돌아온 보고를 찾는가 * 해석하지 않나 커널이 「released 는 나간 것」 같은 방언을 알지 않는가 * 버리지 않나 못 이은 보고의 이유가 남는가 */ import { test } from 'node:test' import assert from 'node:assert/strict' import { matchEffects, refsToLookUp, type DispatchedCommand, type OrderObservation } from '../server/service/actuation/effect-match.js' const AT = '2026-09-02T09:00:00.000Z' const LATER = '2026-09-02T09:05:00.000Z' const ob = (over: Partial = {}): OrderObservation => ({ orderId: 'WO-20260902-002', status: 'released', at: AT, ...over }) function byRef(...commands: DispatchedCommand[]) { return new Map(commands.map(c => [c.dispatchRef, c])) } const cmd = (over: Partial = {}): DispatchedCommand => ({ id: 'cmd-1', dispatchRef: 'WO-20260902-002', ...over }) /* ── 이어진다 ───────────────────────────────────────────────────────────────── */ test('★ 우리가 낸 지시의 식별자로 돌아온 보고를 잇는다 — 이것이 폐루프의 마지막 칸이다', () => { const out = matchEffects({ observations: [ob()], byRef: byRef(cmd()) }) assert.deepEqual(out.updates, [{ commandId: 'cmd-1', status: 'released', at: AT, seen: 1 }]) assert.deepEqual(out.skips, []) }) test('★ 상태를 해석하지 않는다 — 커널은 어느 값이 「현장에 나갔다」인지 모른다', () => { /* * 오더 상태 어휘는 도메인 소유다(`OrderStatusDelta.status` 는 열린 문자열이고 코어가 강제하지 * 않는다). 커널이 `released` 를 특별히 알면 그 방언을 쓰지 않는 현장에서 폐루프가 조용히 안 닫힌다. * * 그래서 무엇이 와도 그대로 적는다 — 뜻은 그 어휘를 아는 쪽이 읽는다. */ for (const status of ['released', 'in-progress', '작업중', 'WIP', 'cancelled']) { const out = matchEffects({ observations: [ob({ status })], byRef: byRef(cmd()) }) assert.equal(out.updates[0]?.status, status, `${status} 를 그대로 적지 않았다`) } }) test('취소 보고도 효과다 — 저쪽에서 무엇이 되었나이지 잘 되었나가 아니다', () => { const out = matchEffects({ observations: [ob({ status: 'cancelled' })], byRef: byRef(cmd()) }) assert.equal(out.updates.length, 1, '나쁜 소식을 걸러 내면 화면이 여전히 「넘겼다」로 끝난다') assert.equal(out.updates[0]!.status, 'cancelled') }) /* ── 몇 번 바뀌었나를 남긴다 ────────────────────────────────────────────────── */ test('★ 상태가 여러 번 바뀌면 마지막이 남고 센 수가 는다', () => { const out = matchEffects({ observations: [ob({ status: 'released', at: AT }), ob({ status: 'in-progress', at: LATER })], byRef: byRef(cmd()) }) assert.equal(out.updates.length, 1, '같은 조치에 두 줄을 냈다') assert.deepEqual(out.updates[0], { commandId: 'cmd-1', status: 'in-progress', at: LATER, seen: 2 }) }) test('★ 이미 적힌 효과에서 이어 센다 — 다음 묶음이 1로 되돌리지 않는다', () => { const out = matchEffects({ observations: [ob({ status: 'in-progress', at: LATER })], byRef: byRef(cmd({ effect: { status: 'released', at: AT, seen: 3 } })) }) assert.equal(out.updates[0]!.seen, 4) }) /* ── 못 이은 것은 이유가 남는다 ─────────────────────────────────────────────── */ test('★ 우리가 내지 않은 오더는 이유로 남는다 — 대개 이것이고, 결함이 아니다', () => { /* * 저쪽 시스템은 자기 오더를 다 보고하고 그중 우리가 낸 것은 일부다. 이 줄이 쌓이는 것을 결함으로 * 읽으면 안 된다 — 그래서 이유 문장이 그렇게 말한다. */ const out = matchEffects({ observations: [ob({ orderId: 'WO-남의것' })], byRef: byRef(cmd()) }) assert.deepEqual(out.updates, []) assert.equal(out.skips.length, 1) assert.match(out.skips[0]!.reason, /우리가 낸 지시가 아니다/) assert.equal(out.skips[0]!.orderId, 'WO-남의것', '어느 오더였는지 말하지 않으면 되짚을 수 없다') }) test('★ 시각이 없는 보고는 「지금」으로 채우지 않는다 — 처리 시각이 관측 시각이 된다', () => { const out = matchEffects({ observations: [ob({ at: undefined })], byRef: byRef(cmd()) }) assert.deepEqual(out.updates, []) assert.match(out.skips[0]!.reason, /지금으로 채우지 않는다/) }) test('★ 상태가 빈 보고는 효과로 적지 않는다 — 「아무 말도 안 했다」가 「효과 없음」이 된다', () => { for (const status of ['', ' ']) { const out = matchEffects({ observations: [ob({ status })], byRef: byRef(cmd()) }) assert.deepEqual(out.updates, [], `${JSON.stringify(status)} 를 효과로 적었다`) assert.match(out.skips[0]!.reason, /빈 값을 효과로 적지 않는다/) } }) test('오더 식별자가 없는 보고도 이유로 남는다', () => { const out = matchEffects({ observations: [ob({ orderId: ' ' })], byRef: byRef(cmd()) }) assert.deepEqual(out.updates, []) assert.match(out.skips[0]!.reason, /식별자가 없는/) }) test('보고가 없으면 아무것도 만들지 않는다 — 빈 묶음에 이유를 만들지 않는다', () => { const out = matchEffects({ observations: [], byRef: byRef(cmd()) }) assert.deepEqual(out.updates, []) assert.deepEqual(out.skips, []) }) /* ── 여러 조치 ──────────────────────────────────────────────────────────────── */ test('★ 한 묶음이 여러 조치에 닿으면 각각 적는다 — 하나만 잇고 끝내지 않는다', () => { const out = matchEffects({ observations: [ob({ orderId: 'WO-1' }), ob({ orderId: 'WO-2', status: 'in-progress' }), ob({ orderId: 'WO-남의것' })], byRef: byRef(cmd({ id: 'cmd-1', dispatchRef: 'WO-1' }), cmd({ id: 'cmd-2', dispatchRef: 'WO-2' })) }) assert.equal(out.updates.length, 2) assert.deepEqual( out.updates.map(u => [u.commandId, u.status]).sort(), [ ['cmd-1', 'released'], ['cmd-2', 'in-progress'] ] ) assert.equal(out.skips.length, 1) }) /* ── 저장소에 물을 것을 줄인다 ─────────────────────────────────────────────── */ test('★ 찾아볼 식별자만 골라 낸다 — 유입은 초당 수천 건이 지나는 자리다', () => { const observations = [ob({ orderId: 'WO-1' }), ob({ orderId: 'WO-1' }), ob({ orderId: 'WO-2' }), ob({ orderId: ' ' })] assert.deepEqual(refsToLookUp(observations).sort(), ['WO-1', 'WO-2'], '중복이나 빈 값을 저장소에 물었다') })