/* * ★ 커맨드 라우팅 판정 — 권위(모드) × 성격(주석/액추에이션). * * ── 무엇이 문제였나 ───────────────────────────────────────────────────────── * 리졸버가 `inst.runtime.dispatch(...)` 를 곧바로 불렀다. 그런데 `runtime` 은 **시뮬 경로에서만** * 만들어진다. 실물을 비추는 트윈(mirror)에서는 그 한 줄이 `TypeError` 로 터져 조치 채널이 통째로 * 닿지 않았다 — 사용자에게는 이유 있는 거절이 아니라 그냥 실패였다. * * ── 그냥 열면 안 되는 이유 ────────────────────────────────────────────────── * 미러에서 액추에이션을 허용하면 **새 거짓말이 생긴다**: 트윈은 "보류됨" 이라 말하고 실 시스템은 * 모른다. 그래서 실 프로세스를 바꾸는 커맨드는 아웃바운드 어댑터가 붙기 전까지 이유를 밝혀 거절한다 * (`design/plans/command-routing.md` P0). 확인(ack)은 트윈 쪽 주석이라 미러에서도 통과한다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { routeCommand, isActuation } from '../server/engine/command-routing.ts' const sim = { hasRuntime: true, hasKernelDispatch: true } const mirror = { mode: 'live', hasRuntime: false, hasKernelDispatch: true } test('시뮬 — 커맨드는 커널 런타임으로 간다(현행 경로 그대로)', () => { assert.deepEqual(routeCommand(sim, true, 'order.hold'), { target: 'runtime' }) assert.deepEqual(routeCommand(sim, true, 'attention.ack'), { target: 'runtime' }) }) test('미러 — 확인(ack)은 관측 커널로 간다. 현실을 건드리지 않는 주석이다', () => { assert.deepEqual(routeCommand(mirror, true, 'attention.ack'), { target: 'kernel' }) }) test('미러 — 실 프로세스를 바꾸는 커맨드는 이유를 밝히고 거절한다', () => { for (const type of ['order.hold', 'order.resume', 'order.release', 'resource.down', 'resource.repair', 'resource.add']) { const r = routeCommand(mirror, true, type) assert.equal(r.target, 'reject', `${type} 은 미러에서 통과하면 안 된다`) if (r.target !== 'reject') return assert.equal(r.errorCode, 'actuation-not-wired') assert.equal(r.errorParams?.type, type, '어느 커맨드였는지 함께 알려야 한다') } }) test('없는 인스턴스와 다른 도메인은 같은 사유로 거절한다 — 있음/없음이 구별되면 그것이 정보다', () => { assert.deepEqual(routeCommand(undefined, false, 'attention.ack'), { target: 'reject', errorCode: 'no-instance' }) assert.deepEqual(routeCommand(sim, false, 'attention.ack'), { target: 'reject', errorCode: 'no-instance' }) }) test('런타임이 없는 시뮬 — 오류를 내지 않고 이유를 알린다', () => { assert.deepEqual(routeCommand({ hasRuntime: false, hasKernelDispatch: true }, true, 'order.hold'), { target: 'reject', errorCode: 'no-runtime' }) }) test('커널이 커맨드를 못 받는 미러 — 확인도 이유를 말하고 멈춘다', () => { assert.deepEqual(routeCommand({ mode: 'live', hasRuntime: false, hasKernelDispatch: false }, true, 'attention.ack'), { target: 'reject', errorCode: 'no-runtime' }) }) test('빈 커맨드 종류는 거절한다 — 무엇을 하라는지 모르는 것을 보내지 않는다', () => { assert.deepEqual(routeCommand(sim, true, ''), { target: 'reject', errorCode: 'unknown-command' }) }) test('성격 판정 — 확인만 주석이고 나머지는 액추에이션이다', () => { assert.equal(isActuation('attention.ack'), false) for (const t of ['order.hold', 'resource.add', 'order.release']) assert.equal(isActuation(t), true, t) })