/* * ★ 연결 적합성 하네스 — **절반만 돌린 스위트가 초록을 주장하지 못하게.** * * 지키는 것: * ① 돌리지 않은 프로브는 **「모른다」**로 처리된다 → 판정이 그 단계를 달성으로 만들지 않는다. * ② **실패한 프로브의 수를 0 으로 읽지 않는다.** 「0건이었다」와 「세지 못했다」는 다른 사실이다. * ③ **쓰기 프로브는 동의 없이 계획에 들어오지 않는다** — 남의 운영 시스템에 명령을 넣는 일이다. * ④ **게이팅 실패 뒤는 돌리지 않는다** — 빨간 칸을 늘리는 것은 원인을 알려 주지 않는다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { PROBE_SEQUENCE, foldProbeResults, plannedProbes, type ProbeResult } from '../server/engine/integration-probes.ts' import { assessStagePath, forbiddenClaims } from '../server/engine/stage-path.ts' const pass = (id: any, count?: number, coverage?: { declared: number; total: number }): ProbeResult => ({ id, ran: true, passed: true, ...(count !== undefined ? { count } : {}), ...(coverage ? { coverage } : {}) }) /** 읽기 프로브 전부 통과 — 반출까지 관측된 상태. */ const readOnlyAllPass = (): ProbeResult[] => [ pass('connect'), pass('discover', 1), pass('master', 12), pass('master-warnings', 3), pass('items', 340), pass('live-ingest', 1200), pass('live-removal', 17), pass('declarations', undefined, { declared: 6, total: 8 }), pass('forecast'), pass('actuate-declared') ] test('시퀀스는 읽기부터 시작하고 쓰기 프로브는 하나뿐이다', () => { assert.equal(PROBE_SEQUENCE[0].id, 'connect') assert.deepEqual(PROBE_SEQUENCE.filter(p => p.writes).map(p => p.id), ['actuate-effect']) }) test('아무것도 돌리지 않았으면 첫 칸부터 「모른다」다 — 실패로 계산하지 않는다', () => { const p = assessStagePath(foldProbeResults([])) assert.equal(p.reached, null) assert.deepEqual(p.next?.stage, 'structure') assert.ok(p.stages[0].blockedBy.includes('no-locations')) assert.ok(p.stages[0].unmeasured.includes('master-warnings'), '세지 않은 것은 모르는 것이다') }) test('읽기만 전부 통과하면 예측까지 도달하고 폐루프는 「검증되지 않음」으로 남는다', () => { const p = assessStagePath(foldProbeResults(readOnlyAllPass())) assert.equal(p.reached, 'forecast') assert.deepEqual(p.stages[3].unmeasured, ['correlated-effect']) assert.deepEqual(p.stages[3].blockedBy, [], '능력은 선언∩구현이므로 막힌 것이 아니라 안 돌린 것이다') assert.deepEqual(forbiddenClaims(p), ['claim.automatic-control']) }) test('쓰기 프로브를 돌려 효과가 관측되면 폐루프가 닫힌다', () => { const p = assessStagePath(foldProbeResults([...readOnlyAllPass(), pass('actuate-effect', 2)])) assert.equal(p.reached, 'closed-loop') assert.deepEqual(forbiddenClaims(p), []) }) test('측정하지 못한 프로브는 수를 들지 않는다 — 0 을 지어내지 않는다', () => { /* * 규칙은 「실패면 모른다」가 아니다 — **센 값은 판정과 무관한 사실**이다(0건도 사실이다). * 측정 자체를 못 한 프로브(타임아웃 등)는 `count` 를 **비운다**. 0 을 채우면 「세어 보니 없었다」로 * 읽히고, 그건 스냅샷 피드를 delta 로 오인하게 만드는 정반대의 결론을 낳는다. */ const rs: ProbeResult[] = [...readOnlyAllPass().filter(r => r.id !== 'live-removal'), { id: 'live-removal', ran: true, passed: false, detail: 'timeout' }] const e = foldProbeResults(rs) assert.equal(e.removalsObserved, null, '측정하지 못했으면 모른다') assert.deepEqual(assessStagePath(e).stages[1].unmeasured, ['removals']) }) test('세어서 0 을 얻은 것은 사실이다 — 모른다로 계산하지 않는다', () => { const rs: ProbeResult[] = [...readOnlyAllPass().filter(r => r.id !== 'live-removal'), { id: 'live-removal', ran: true, passed: false, count: 0 }] const e = foldProbeResults(rs) assert.equal(e.removalsObserved, 0) assert.deepEqual(assessStagePath(e).stages[1].blockedBy, ['no-removal-observed']) }) test('반출을 세었는데 0 이면 라이브가 막힌다 — 스냅샷 되풀어 읽기', () => { const rs: ProbeResult[] = [...readOnlyAllPass().filter(r => r.id !== 'live-removal'), pass('live-removal', 0)] const p = assessStagePath(foldProbeResults(rs)) assert.deepEqual(p.stages[1].blockedBy, ['no-removal-observed']) assert.equal(p.reached, 'structure') assert.ok(forbiddenClaims(p).includes('claim.realtime')) }) test('쓰기 프로브는 동의 없이 계획에 들어오지 않는다', () => { assert.ok(!plannedProbes([]).includes('actuate-effect')) assert.ok(plannedProbes([], { allowWrites: true }).includes('actuate-effect')) }) test('게이팅 프로브가 실패하면 그 뒤는 계획하지 않는다', () => { const planned = plannedProbes([{ id: 'connect', ran: true, passed: false, detail: '401' }]) assert.deepEqual(planned, [], '붙지 못했으면 뒤를 두드릴 이유가 없다') }) test('통과한 프로브는 다시 계획하지 않고, 남은 것만 계획한다', () => { const planned = plannedProbes([pass('connect'), pass('discover', 1)]) assert.equal(planned[0], 'master') assert.ok(!planned.includes('connect')) }) test('게이팅이 아닌 프로브의 실패는 뒤를 멈추지 않는다', () => { const planned = plannedProbes([pass('connect'), pass('discover', 1), pass('master', 12), { id: 'master-warnings', ran: true, passed: false, detail: 'not-counted' }]) assert.ok(planned.includes('items'), '못 옮긴 값을 세지 못한 것이 구조 확인을 멈추지는 않는다') })