/* * 「계측이 들어오고 있나」 — **도는 것과 받는 것을 가른다.** * * ── 무엇이 보이지 않았나 (2026-08-14) ─────────────────────────────────────── * 미러 트윈은 커널이 돌기만 하면 「도는 중」이었다. 피드가 떨어져 있으면 그 트윈은 아무것도 받지 * 않으면서 도는 중인데, 화면에서 두 상태가 똑같이 보였다 — 재기동 뒤 실제로 그랬고 사람은 * 「값이 안 변하네」로 뒤늦게 알았다. * * 이 모듈은 DB 를 물지 않는다 — 그래서 배선의 모양이 아니라 **거동**을 시험한다. */ import { test } from 'node:test' import assert from 'node:assert/strict' /* * **빌드 산출물을 부른다** — 이 모듈은 로그 문(`engine/log.js`)을 값으로 참조하고, 타입만 지우고 소스를 * 실행하는 방식은 그 `.js` 경로를 `.ts` 로 바꿔 주지 않는다. 소스를 부르면 「모듈을 찾을 수 없다」로 선다. * **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌린다.** */ import { createRequire } from 'node:module' const req = createRequire(import.meta.url) const { registerLiveFeedProbe, liveFeedAttached, liveFeedStateOf } = req('../dist-server/engine/live-feed-registry.js') /* 두 계층의 장부를 흉내낸다 — 커넥터 기반과 프리셋 기반. */ const connectorFeeds = new Set() const presetFeeds = new Set() registerLiveFeedProbe(() => [...connectorFeeds]) registerLiveFeedProbe(() => [...presetFeeds]) test('어느 장부든 붙어 있다고 하면 붙어 있다 — 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다', () => { connectorFeeds.add('ems-plant-1') presetFeeds.add('sap-wh-1') assert.equal(liveFeedAttached('ems-plant-1'), true, '커넥터 기반 피드') assert.equal(liveFeedAttached('sap-wh-1'), true, '프리셋 기반 피드 — 이걸 놓치면 거짓 경보가 된다') assert.equal(liveFeedAttached('nobody'), false) }) test('답하지 못하는 장부가 다른 계층의 사실을 죽이지 않는다', () => { registerLiveFeedProbe(() => { throw new Error('registry down') }) /* 터진 장부 뒤에 등록된 장부의 사실도 그대로 읽혀야 한다. */ const late = new Set(['late-twin']) registerLiveFeedProbe(() => [...late]) assert.equal(liveFeedAttached('late-twin'), true) assert.equal(liveFeedAttached('ems-plant-1'), true) }) test('「없음」과 「해당 없음」을 섞지 않는다', () => { /* 도는 미러 — 이 둘이 우리가 보이려던 구별이다. */ assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: true, instanceId: 'ems-plant-1' }), 'attached') assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: true, instanceId: 'ems-plant-2' }), 'detached') /* 시뮬 트윈에는 나를 피드가 **없어야 한다** — 결함이 아니라 그 트윈의 성질이다. */ assert.equal(liveFeedStateOf({ restartPolicy: 'resume', running: true, instanceId: 'hatio-us' }), 'not-applicable') assert.equal(liveFeedStateOf({ restartPolicy: 'reset', running: true, instanceId: 'trailer-new' }), 'not-applicable') /* 사람이 멈춰 둔 트윈에게 피드를 묻는 것은 뜻이 없다. */ assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: false, instanceId: 'ems-plant-1' }), 'not-applicable') /* 선언이 없으면 미러라고 짐작하지 않는다(짐작으로 경보를 울리지 않는다). */ assert.equal(liveFeedStateOf({ running: true, instanceId: 'ems-plant-1' }), 'not-applicable') }) test('목록이 그 사실을 함께 낸다 — 화면이 따로 물어야 하면 언젠가 묻지 않는다', async () => { const src = await import('node:fs').then(fs => fs.readFileSync(new URL('../server/engine/twin-engine.ts', import.meta.url), 'utf-8') ) const at = src.indexOf('static async list(') assert.ok(at >= 0, 'list() 가 없다 — 이름이 바뀌었으면 이 검사도 함께 고친다') const body = src.slice(at, src.indexOf('\n /** 운영 공간 목록', at)) assert.match(body, /liveFeed: liveFeedStateOf\(/, '관리 목록 행에 피드 사실이 실려야 한다') assert.match(body, /restartPolicy: r\.restartPolicy/, '선언된 모드를 넘겨야 한다(짐작하지 않는다)') })