/* * 다시 읽기가 **자기 사이트**를 찾는가 — 사이트가 여럿인 참조에서 온 트윈의 재동기. * * ── 무엇이 났나 (2026-08-18 실측) ─────────────────────────────────────────── * `resyncTwin` 이 「reference "ems-gw-live" no longer reports this site」로 거절했다. 사이트는 멀쩡히 * 있었다 — 코드가 **다른 것을 견주고** 있었다: `origin.source` 는 레퍼런스 등록 이름(그 참조의 사이트 * 전부에 같다)이고 `master.source` 는 사이트별 인스턴스 id 다. 그래서 사이트가 여럿인 참조에서 온 * 트윈은 전부 재동기가 불가했고, 오류 문장은 원인을 참조 쪽으로 잘못 가리켰다(내가 그 문장을 읽고 * 커넥터를 의심했다). * * 고친 뒤의 규율: ① 가져올 때 사이트 정체를 남긴다 ② 다시 읽기는 그 사이트로 곧장 간다 ③ 기억이 없는 * 옛 트윈은 마스터의 정체(=인스턴스 id)로 찾는다 ④ 어느 쪽도 못 찾으면 **첫 사이트로 때우지 않는다**. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { pickOriginSite } from '../server/service/reference/reference-master.ts' const sites = [{ siteId: 'plant-1' }, { siteId: 'plant-2' }, { siteId: 'plant-3' }] test('기억해 둔 사이트로 곧장 간다', () => { const { site, reason } = pickOriginSite(sites, { siteId: 'plant-2' }) assert.equal(site?.siteId, 'plant-2') assert.equal(reason, undefined) }) test('기억이 없으면 그렇게 말한다 — 첫 사이트로 때우지 않는다', () => { const { site, reason } = pickOriginSite(sites, {}) assert.equal(site, undefined) assert.equal(reason, 'no-remembered-site') }) test('기억한 사이트가 사라졌으면 다른 사이트로 덮지 않는다', () => { const { site, reason } = pickOriginSite(sites, { siteId: 'plant-9' }) assert.equal(site, undefined) assert.equal(reason, 'site-gone', '다른 현장의 구조를 이 트윈에 덮는 것은 오류 없이 틀린 공장이 된다') }) test('빈 문자열은 기억이 아니다', () => { assert.equal(pickOriginSite(sites, { siteId: ' ' }).reason, 'no-remembered-site') }) /* 배선이 사라지면 알리지 않고 예전으로 돌아간다 — 소스 모양으로 못 박는다(리졸버는 DB·어댑터를 물고 있다). */ test('다시 읽기는 인스턴스 정체로 견주고, 가져오기는 사이트 정체를 남긴다', () => { const src = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-resolver.ts', import.meta.url)), 'utf-8') /* 옛 비교식은 **설명문에 남아 있어야 한다**(왜 고쳤는지가 사라지면 되돌아간다) — 그래서 코드 줄만 본다. */ const code = src .split('\n') .filter(line => !/^\s*(\*|\/\*|\/\/)/.test(line)) .join('\n') assert.match(code, /pickOriginSite\(sites, origin\)/, '기억한 사이트로 곧장 가는 길이 있어야 한다') assert.match(code, /master\.source === instanceId/, '옛 트윈은 마스터의 정체로 찾는다(참조 이름이 아니다)') assert.equal(/master\.source === origin\.source/.test(code), false, '참조 이름과 인스턴스 id 를 견주지 않는다') /* 사이트 정체는 남아 있어야 한다 — 현장 기억(spaceId)이 함께 붙어도 그 사실은 변하지 않는다. */ assert.match(code, /origin: \{ kind: 'reference', source, siteId: site\.siteId/, '가져올 때 사이트 정체를 남긴다') }) /* * ── 다시 읽기가 **현장 소속**을 지킨다 (2026-08-18 실측) ───────────────────── * 한 현장에 합쳐 둔 EMS 트윈 여덟을 재동기했더니 각자 공간으로 흩어졌다 — 템플릿이 낸 기본 공간으로 * 돌아갔기 때문이다. 들어온 것이 없다: 오류도 경고도 없고, 트윈은 멀쩡히 돌아간다. 그런데 그 순간 **그 현장의 * 값(요금·통화)을 함께 잃는다** — 현장 겹은 소속으로 얹히기 때문이다. * * 원인은 origin 이 「어느 현장에 합쳤나」를 기억하지 않은 것이다(구조만 기억했다). */ test('프로비저닝이 고른 현장을 origin 에 남기고, 다시 읽기가 그것을 따른다', () => { const src = readFileSync(fileURLToPath(new URL('../server/service/reference/reference-resolver.ts', import.meta.url)), 'utf-8') const code = src .split('\n') .filter(line => !/^\s*(\*|\/\*|\/\/)/.test(line)) .join('\n') assert.match(code, /kind: 'template', templateId, params: effective, \.\.\.\(spaceId \? \{ spaceId \} : \{\}\)/, '템플릿 트윈이 현장을 기억한다') assert.match(code, /const keepSpace = origin\.spaceId \|\| inst\.spaceId/, '기억이 없으면 지금 있는 현장을 지킨다') /* 인자 사이에 재기동 정책이 끼었다(ADR-0029 §2) — 현장을 넘기는 인자는 그대로여야 한다. */ assert.match(code, /keepSpace \? \{ spaceId: keepSpace \} : undefined/, '다시 읽기가 그 현장으로 넣는다') assert.match( code, /readRestartPolicy\(inst\.restartPolicy, `resync of twin/, '다시 읽기는 그 트윈의 정책을 그대로 지킨다(구조만 새로 그린다)' ) })