/* * ★ **화면 저장도 받는 사이트 충돌을 거부한다** (ADR-0046 ②-1, 2026-09-16) * * 들여오기에는 충돌 거부가 있는데 화면 저장(`saveTwinReference`)에는 없었다. `intakeEndpoint::intakeSiteId` * 가 같은 연결 둘을 만들면 받는 문은 둘 다 200 으로 받고 경고가 0 이다 — 잘못된 공장이 실제로 만든다. * 이 검사는 저장의 문이 쓰는 판정(`refuseIntakeCollision`)을 값으로 못 박는다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { intakeCollisions, refuseIntakeCollision } from '../server/service/reference/connection-portability.js' const intake = { intakeEndpoint: 'https://plant.example/intake/', intakeSiteId: 'system' } const existing = [ { source: 'mes-line-a', siteName: 'Line A', connectionConfig: { baseUrl: 'https://plant.example', ...intake } } ] test('★ 같은 받는 사이트를 가리키는 둘째 연결은 거부되고, 부딪힌 연결의 이름을 말한다', () => { const r = refuseIntakeCollision('mes-line-b', { baseUrl: 'https://plant.example', ...intake }, existing) assert.ok(r, '베낀 연결이 그대로 저장된다') assert.equal(r.code, 'intake-site-taken') assert.equal(r.params.site, 'system') assert.equal(r.params.connection, 'Line A', '무엇과 부딪혔는지 말하지 않는다') }) test('주소 끝의 / 하나로 비껴가지 않는다', () => { const r = refuseIntakeCollision('mes-line-b', { intakeEndpoint: 'https://plant.example/intake', intakeSiteId: 'system' }, existing) assert.ok(r) }) test('자기 자신을 다시 저장하는 것은 충돌이 아니다', () => { assert.equal(refuseIntakeCollision('mes-line-a', existing[0].connectionConfig, existing), null) }) test('사이트를 고치면 저장된다 — 한 글자 고친 사람은 그 자리에서 지난다', () => { assert.equal(refuseIntakeCollision('mes-line-b', { ...intake, intakeSiteId: 'line-b' }, existing), null) }) test('조치 주소가 없는 연결은 부딪힐 것이 없다', () => { assert.equal(refuseIntakeCollision('wms-x', { baseUrl: 'https://plant.example', siteId: 'system' }, existing), null) }) test('★ 저장의 문이 그 판정을 부르고, 거부하면 쓰기 전에 돌아간다', () => { const src = readFileSync(new URL('../server/service/reference/reference-resolver.ts', import.meta.url), 'utf8') const body = src.slice(src.indexOf('async saveTwinReference('), src.indexOf('async deleteTwinReference(')) const check = body.indexOf('refuseIntakeCollision(') const write = body.indexOf('repo.save(') assert.ok(check > 0, 'saveTwinReference 가 충돌 판정을 부르지 않는다') assert.ok(check < write, '판정이 쓰기 뒤에 있다 — 이미 저장된 뒤의 거부는 거부가 아니다') assert.match(body, /errorCode: taken\.code/) }) /* * ★ **같은 충돌을 두 모양으로 건다** (ADR-0053 ②, 2026-09-16). * * 패키지는 `config`, `TwinReference` 행은 `connectionConfig` 다. 전에는 `config` 만 읽어서 행을 그대로 * 넘기면 `[]` 였다 — 오류도 경고도 없이 저장이 통과했다. 부르는 둘이 마침 옮겨서 부른 덕에 멀쩡했는데, * 그건 계약이 아니라 우연이다(이 함수는 화면이 쓰라고 리졸버에서 다시 내보내진다). * * 옮기기가 다시 갈리는 날 **빨개지는 자리가 여기 하나**다. 그래서 「두 모양이 같은 답을 낸다」를 * 값으로 센다 — 결정의 내용이 그것이므로. */ const pair = [ { source: 'mes-line-a', ...intake }, { source: 'mes-line-b', ...intake } ] test('★ DB 행 모양(connectionConfig)으로 걸어도 같은 충돌이 나온다 — 「없다」가 편한 답인 자리다', () => { const rows = intakeCollisions(pair.map(({ source, ...cfg }) => ({ source, connectionConfig: cfg }))) assert.equal(rows.length, 1, '행 모양을 못 알아보고 조용히 넘어갔다') assert.deepEqual(rows[0].sources, ['mes-line-a', 'mes-line-b']) }) test('★ 두 모양이 같은 답을 낸다 — 옮기기가 갈리면 여기가 빨개진다', () => { const asConfig = intakeCollisions(pair.map(({ source, ...cfg }) => ({ source, config: cfg }))) const asRow = intakeCollisions(pair.map(({ source, ...cfg }) => ({ source, connectionConfig: cfg }))) assert.deepEqual(asRow, asConfig) }) test('섞여 들어와도 한 충돌로 본다 — 들여오기는 실제로 행과 패키지를 함께 넘긴다', () => { const mixed = intakeCollisions([ { source: 'mes-line-a', connectionConfig: { ...intake } }, { source: 'mes-line-b', config: { ...intake } } ]) assert.equal(mixed.length, 1, '섞인 목록에서 한쪽만 세었다') assert.deepEqual(mixed[0].sources, ['mes-line-a', 'mes-line-b']) })