/* * ★ **한 번 넘겨 봤나** — 설정이 맞는 것과 저쪽이 받는 것은 다르다 (ADR-0046 ②). * * 주소도 비밀값도 다 적혀 있는데 저쪽 프로세스가 그 값을 잃은 일이 있었다(§`actuationReadiness`). * 그것은 넘겨 봐야만 알고, 그래서 「아직 안 해 봤다」와 「해 봤고 됐다」와 「해 봤고 안 됐다」가 * 서로 다른 사실이다. 이 시험은 그 셋이 안 뭉개지는지 본다. * * ── 저장되는 것은 코드다 ──────────────────────────────────────────────────── * `lastError` 가 사람 문장을 저장해 두고 화면이 그것을 그대로 뿌렸다. 번역도 집계도 안 되고, * 커넥터가 문장을 고치는 날 옛 행과 새 행이 다른 말을 한다. 그래서 사유는 코드로만 앉는다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { actuationAddressed, actuationOutcomePatch, actuationResetPatch } from '../server/service/reference/actuation-verification.js' const AT = '2026-09-16T04:05:06.000Z' test('받아들여지면 verified 로 서고 그 시각이 남는다', () => { const patch = actuationOutcomePatch({ ok: true, ref: 'JO-1' }, AT) assert.equal(patch.actuationState, 'verified') assert.equal(patch.actuationVerifiedAt, AT) }) test('★ 받아들여지면 지난 거절을 지운다 — 「됐는데 실패 사유가 있다」를 그리지 않게', () => { assert.equal(actuationOutcomePatch({ ok: true }, AT).actuationFailure, null) }) test('★ 거절되면 사유가 코드로 앉는다 — 문장이 아니라', () => { const patch = actuationOutcomePatch( { ok: false, error: '저쪽이 503 을 답했습니다', errorCode: 'peer-unavailable', errorParams: { status: 503 } }, AT ) assert.equal(patch.actuationState, 'failed') assert.deepEqual(patch.actuationFailure, { code: 'peer-unavailable', at: AT, params: { status: 503 } }) /* 사람 문장은 어디에도 안 실린다 — 그것이 이 칸을 만든 이유다. */ assert.ok(!JSON.stringify(patch).includes('503 을 답했습니다')) }) test('★ 코드가 없으면 사유를 비운다 — 자리를 채우려고 문장을 옮기지 않는다', () => { const patch = actuationOutcomePatch({ ok: false, error: '무언가 잘못됐습니다' }, AT) assert.equal(patch.actuationState, 'failed', '코드가 없어도 실패는 실패다') assert.equal(patch.actuationFailure, null, '모르는 것을 적었다') }) test('★ 거절은 성공 시각을 지우지 않는다 — 「한 번이라도 닿은 적 있나」가 남아야 한다', () => { const patch = actuationOutcomePatch({ ok: false, errorCode: 'peer-unavailable' }, AT) assert.ok(!('actuationVerifiedAt' in patch), '실패가 지난 성공의 시각을 덮었다') }) test('★ params 가 없으면 그 칸을 만들지 않는다 — 빈 객체와 「없다」는 다르다', () => { const patch = actuationOutcomePatch({ ok: false, errorCode: 'not-declared' }, AT) assert.deepEqual(patch.actuationFailure, { code: 'not-declared', at: AT }) }) /* * 위 시험들은 **판단만** 본다. 판단이 옳아도 아무도 안 부르면 레퍼런스에는 아무것도 안 남는다 — * 순수 시험은 부르는 사람을 못 본다. * * 값으로 재려면 DB 가 붙어야 해서, 여기서는 원문을 읽는다. 다만 **어느 함수 안에서 부르는지까지** * 본다. 파일 전체에서 이름을 세면 **선언이 자기를 센다** — 실제로 호출을 지우고 돌려 봤는데 이 * 시험이 초록이었다. 세는 것은 주장이지 그 이웃이 아니다. */ const strip = (x: string) => x.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '') const src = strip(readFileSync(new URL('../server/service/reference/actuation-routing.ts', import.meta.url), 'utf8')) const bodyOf = (name: string) => { const at = src.indexOf(`function ${name}(`) assert.ok(at > 0, `${name} 이 없다`) const next = src.slice(at + 1).search(/\nasync function |\nexport (async )?function /) return next < 0 ? src.slice(at) : src.slice(at, at + 1 + next) } test('★ routeActuation 이 실제로 그것을 부른다 — 판단만 맞고 아무도 안 부르면 남는 것이 없다', () => { const route = bodyOf('routeActuation') assert.match(route, /recordActuationOutcome\(/, '조치 경로에서 그 기록이 불리지 않는다') /* 기록이 본업을 막지 않는다 — 실패해도 조치 결과는 그대로 돌아간다. */ assert.match(route, /\.catch\(/, '기록이 실패하면 조치까지 함께 넘어진다') assert.match(route, /return result/, '조치 결과를 돌려주지 않는다') }) test('★ 기록하는 자리가 판단을 쓰고 그 값을 저장한다 — 자기 판단을 다시 짓지 않는다', () => { const record = bodyOf('recordActuationOutcome') assert.match(record, /actuationOutcomePatch\(/, '판단을 부르지 않는다 — 두 벌이 된다') assert.match(record, /\.update\(/, '부른 결과를 저장하지 않는다') }) test('★ 주소를 바꾸는 저장은 검증을 되돌린다 — 지난 왕복은 지난 주소의 이야기다', () => { /* * 2026-09-18 실측(ADR-0046 ② 를 코드에 대어 본 결과): 칸 셋도 왕복의 결과를 적는 자리도 섰는데, * `verified` 인 연결의 주소를 바꿔도 그대로 `verified` 였다. 화면은 다른 공장을 가리키는 연결을 * 「확인됨」으로 그린다. */ const before = { intakeEndpoint: 'https://a.example/intake', intakeSiteId: 'line-a', intakeSecret: 's1' } const moved = actuationResetPatch(before, { ...before, intakeEndpoint: 'https://b.example/intake' }) assert.deepEqual(moved, { actuationState: 'unverified', actuationVerifiedAt: null, actuationFailure: null }) assert.ok(actuationResetPatch(before, { ...before, intakeSiteId: 'line-b' }), '받는 사이트가 바뀌었다') assert.ok(actuationResetPatch(before, { ...before, intakeSecret: 's2' }), '저쪽이 우리를 못 알아보면 그 왕복은 다시 성립하지 않는다') assert.ok(actuationResetPatch(undefined, before), '처음 적는 주소도 아직 왕복이 없다') }) test('주소가 그대로면 검증도 그대로 — 이름만 고쳤다고 되돌리지 않는다', () => { const cfg = { intakeEndpoint: 'https://a.example/intake', intakeSiteId: 'line-a', intakeSecret: 's1' } assert.equal(actuationResetPatch(cfg, { ...cfg, label: '군산 1라인' }), null) /* 끝의 빗금은 같은 주소다 — 그것으로 되돌리면 저장할 때마다 초록이 사라진다. */ assert.equal(actuationResetPatch(cfg, { ...cfg, intakeEndpoint: 'https://a.example/intake/' }), null) /* 가려진 비밀값은 저장 쪽이 병합해 옛 값 그대로 온다(그래서 여기까지 같은 값이 온다). */ assert.equal(actuationResetPatch(cfg, { ...cfg }), null) }) test('★ 보낼 주소가 적힌 연결에만 검증 상태를 말한다 — 읽기만 하는 연결에 할 일을 만들지 않는다', () => { assert.equal(actuationAddressed({ intakeEndpoint: 'https://a.example/intake', intakeSiteId: 'line-a' }), true) assert.equal(actuationAddressed({ intakeEndpoint: 'https://a.example/intake', siteId: 'line-a' }), true, 'siteId 가 그 자리를 대신한다') assert.equal(actuationAddressed({ intakeSiteId: 'line-a' }), false, '주소가 없으면 아무 데도 안 간다') assert.equal(actuationAddressed({ intakeEndpoint: 'https://a.example/intake' }), false, '받는 사이트를 모르면 못 보낸다') assert.equal(actuationAddressed(undefined), false) }) test('★ 읽기 문이 그 상태를 싣는다 — 저장돼 있어도 나르지 않으면 화면이 볼 길이 없다', () => { /* * 2026-09-18 실측: 칸 셋은 2026-09 부터 서 있었고 왕복의 결과도 적히는데, 목록도 상세도 그 값을 * 싣지 않았다. 저장된 사실이 아무에게도 닿지 않는 상태였다. */ const src = readFileSync(new URL('../server/service/reference/reference-resolver.ts', import.meta.url).pathname, 'utf-8') for (const door of ['async twinReferences(', 'async twinReference(']) { const at = src.indexOf(door) assert.ok(at > 0, `${door} 를 못 찾았다 — 읽는 쪽이 깨졌다`) const body = src.slice(at, at + 3000) assert.ok(/actuationState/.test(body), `${door} 가 검증 상태를 싣지 않는다`) } })