/* * 런타임 레지스트리 색인 가드 — **엔진 밖에서 맵을 직접 만지지 않는다.** * * 계기(2026-08-06): `TwinEngine.instances` 가 `instanceId` 하나로 키를 잡던 시절, 엔진 밖 네 곳이 그 맵을 * **직접 색인**했다(`TwinEngine.instances[instanceId]`, 심지어 `(TwinEngine as any).instances?.[…]`). * 그래서 도메인 확인이 자리마다 제각각이었고 한 곳은 아예 없었다. * * 키를 `(domainId, instanceId)` 로 고쳤지만, **직접 색인이 남아 있으면 같은 부류가 다시 자란다** — 밖에서 * `instances[id]` 를 쓰면 이제는 항상 `undefined` 가 나와 알리지 않고 "안 돌고 있다" 로 읽힌다(더 나쁘다: * 오류가 아니라 거짓 사실이 된다). 그래서 접근자(`owns`·`runtime`·`kernel`·`metrics`·`snapshot`)만 쓴다. * * 이 부류는 이미 한 번 물었다 — `twin-lifecycle-mutation.ts` 주석이 증언한다("과거엔 * `Object.keys(TwinEngine.instances)` 로 전역 반환 → 다른 도메인 트윈이 새던 버그"). 그때는 그 자리만 * 고쳤다. 이번에는 기계가 지킨다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readdirSync, readFileSync, statSync } from 'node:fs' import { join } from 'node:path' const SERVER = new URL('../server', import.meta.url).pathname /** 맵을 소유한 자리 — 여기서는 색인해야 한다. */ const OWNERS = ['engine/twin-engine.ts', 'engine/runtime-key.ts'] function walk(dir: string, out: string[] = []): string[] { for (const entry of readdirSync(dir)) { const p = join(dir, entry) if (statSync(p).isDirectory()) walk(p, out) else if (p.endsWith('.ts') && !p.endsWith('.d.ts')) out.push(p) } return out } /** 주석 줄은 뺀다 — 이 결함을 설명하는 주석이 여러 곳에 있고, 그것을 위반으로 셀 수 없다. */ const isComment = (line: string) => /^\s*(\/\/|\/\*|\*)/.test(line) test('엔진 밖에서 런타임 레지스트리를 직접 색인하지 않는다', () => { const offenders: string[] = [] for (const file of walk(SERVER)) { const rel = file.slice(SERVER.length + 1) if (OWNERS.includes(rel)) continue readFileSync(file, 'utf-8') .split('\n') .forEach((line, i) => { if (isComment(line)) return /* * **엔진 참조가 같은 줄에 있을 때만** 위반이다(`as any` 우회 포함). `.instances[` 만 보면 * 무관한 배열(응답·지역 변수)까지 잡아 정당한 코드를 막는다. */ if (/TwinEngine\s*(as\s+any\s*)?\)?\s*\.\s*(instances|recovered)/.test(line)) { offenders.push(`${rel}:${i + 1} ${line.trim().slice(0, 100)}`) } }) } assert.deepEqual( offenders, [], `레지스트리를 직접 색인하는 곳 ${offenders.length}곳 — 접근자(owns·runtime·kernel·metrics·snapshot)를 쓴다:\n` + offenders.join('\n') ) }) test('엔진이 맵을 만질 때는 항상 키 함수를 거친다', () => { const src = readFileSync(join(SERVER, 'engine/twin-engine.ts'), 'utf-8') const bad: string[] = [] src.split('\n').forEach((line, i) => { if (isComment(line)) return /* 색인하는 줄에는 `runtimeKey(` 또는 미리 만들어 둔 `key` 변수가 있어야 한다. */ if (!/this\.(instances|recovered)\s*\[/.test(line)) return if (/runtimeKey\(/.test(line) || /\[key\]/.test(line)) return bad.push(`engine/twin-engine.ts:${i + 1} ${line.trim().slice(0, 100)}`) }) assert.deepEqual(bad, [], `키 함수를 거치지 않고 색인하는 곳 ${bad.length}곳:\n${bad.join('\n')}`) })