/* * **구동은 선언된 능력이다** — 그리고 원본을 지난다 (ADR-0029 §8, 2026-08-19). * * ── 무엇이 뚫려 있었나 ────────────────────────────────────────────────────── * `controlTwinScenario` 가 트윈의 커널 런타임(`runtime.scenario`)을 **직접** 잡았다. 시뮬만 할 수 있는 일 * (자극·배속·정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. ADR-0029 의 * 판정 문장(*"실물이 같은 계약으로 할 수 있는가"*)을 그대로 대면 그 통제면은 영원히 「규약의 결손」이 된다. * * 뒤집는다: 통제는 **선언된 어댑터 능력**이다. 선언하지 않은 원본에는 문이 열리지 않고, 그 사실이 결손이 * 아니라 사실이다(실 WMS 는 정지되지 않는다). * * 이 시험이 지키는 것: * ① 선언과 구현이 **함께** 있어야 능력이다 — 선언만 있으면 화면이 거짓말을 하고, 구현만 있으면 닫아 둔 * 문을 여는 것이다(선언이 권위다). * ② 능력 없는 원본은 **사유 코드로** 거절한다(들어온 것이 없는 실패 금지). * ③ 없는 기능을 있는 것처럼 답하지 않는다(내장 시뮬은 아직 재씨앗을 못 한다). */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { capabilitiesOf, type ReferenceAdapter } from '../server/service/reference/reference-adapter.ts' const base: ReferenceAdapter = { type: 'probe', async testConnection() { return { ok: true } }, async discoverSites() { return [] }, async fetchMaster() { return { source: 'p', system: 'wms', space: { spaceId: 'p-site' }, locations: [] } as any } } test('선언도 구현도 없으면 아무 능력이 없다 — 구조만 주는 원본이 정상이다', () => { assert.deepEqual(capabilitiesOf(base), []) }) test('선언만 있으면 능력이 아니다 — 화면이 「할 수 있다」고 말한 뒤 아무 일도 없게 두지 않는다', () => { assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['control'] }), []) assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['live'] }), []) }) test('구현만 있으면 능력이 아니다 — 선언이 권위다(닫아 둔 문을 열지 않는다)', () => { assert.deepEqual(capabilitiesOf({ ...base, control: async () => ({ ok: true }) }), []) assert.deepEqual(capabilitiesOf({ ...base, openLiveFeed: () => () => {} }), []) }) test('선언과 구현이 함께 있으면 그 능력이다', () => { assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['control'], control: async () => ({ ok: true }) }), ['control']) assert.deepEqual( capabilitiesOf({ ...base, capabilities: ['live', 'control'], openLiveFeed: () => () => {}, control: async () => ({ ok: true }) }), ['live', 'control'] ) }) test('어댑터가 없으면 빈손이다 — 지어내지 않는다', () => { assert.deepEqual(capabilitiesOf(undefined), []) }) /* * ── 길이 하나뿐인지 (소스로 지킨다) ──────────────────────────────────────── * 지름길이 남아 있으면 이 결정은 문서상의 것이 된다. 그래서 ① 구동 문이 라우터를 지나고 ② 라우터가 능력을 * 보고 ③ 트윈 런타임을 직접 잡는 옛 경로가 사라졌는지 본다. */ test('구동 문이 원본을 지난다 — 트윈 런타임을 직접 잡지 않는다', () => { const src = readFileSync(fileURLToPath(new URL('../server/service/twin-control/twin-control-mutation.ts', import.meta.url)), 'utf-8') const code = src .split('\n') .filter(line => !/^\s*(\*|\/\/|\/\*)/.test(line)) .join('\n') assert.match(code, /routeControl\(domainId, instanceId/, '구동은 라우터를 지난다') assert.equal(/runtime\.scenario\./.test(code), false, '트윈의 시나리오 엔진을 직접 잡는 지름길이 없다') assert.equal(/controlTwinScenario/.test(code), false, '옛 문이 남아 있지 않다') assert.match(code, /twinSourceCapability/, '화면이 먼저 물을 자리가 있다') }) test('라우터가 선언을 본다 — 없으면 사유 코드로 거절한다', () => { const src = readFileSync(fileURLToPath(new URL('../server/service/reference/control-routing.ts', import.meta.url)), 'utf-8') assert.match(src, /capabilitiesOf\(adapter\)\.includes\('control'\)/) assert.match(src, /'not-declared'/) assert.match(src, /'no-source'/, '원본을 모르는 트윈은 구동 대상이 아니다') }) /* * The assertion that read `operato-twin/server/board/virtual-adapter.ts` moved to that * package's own tests when the applications became their own repository — a guard reading a * consumer's source has to live where that source lives, or it scans nothing and says green. * See operato-application/packages/operato-twin/test/virtual-adapter-capability.test.ts. */