/* * 상태가 빈 축의 이유 — **「아직 없다」와 「왔는데 상태에 없다」를 가른다** (2026-08-22). * * ── 무엇이 있었나 ─────────────────────────────────────────────────────────── * 카드가 「작업 — 아직 하나도 없습니다」라고 말했다. 그 트윈의 저널에는 `task.status` 5,786건이 있었고 * **마지막 상태가 `in-progress`** 였다. 사용자가 「작업 지시 835건인데 작업이 0인 이유」를 물었고, * 원인은 관측 모드 커널이 관측기의 작업을 스냅샷으로 내보내지 않는 것이었다(커널 자리, 그 레인에 넘겼다). * * **화면 몫은 단정하지 않는 것이다.** 「아직 하나도 없습니다」는 「선언은 있고 사실이 아직 일어나지 * 않았다」는 뜻인데 실제로는 「사실이 일어났는데 상태에 오지 못했다」였다. 그 차이를 화면이 말하지 * 않아서 사용자가 직접 조사해야 했다. * * 이 시험이 지키는 것: * ① 저널에 그 축의 사건이 있으면 **그렇게 말한다**. * ② 없으면 예전 그대로 「아직 없다」다. * ③ 판단할 수 없는 축은 **아무 말도 만들지 않는다**(`null`) — 모르는 것을 새 사실로 바꾸지 않는다. * ④ 한 건만 묻는다 — 세는 것도 비용이다(저널 358만 행에서 전량 세기는 5.32초였다). */ import { test } from 'node:test' import assert from 'node:assert/strict' import { axisSeenInJournal } from '../server/service/twin-model/axis-journal-evidence.ts' /** 저장소 흉내 — 어떤 조건으로 몇 번 물었는지 기록한다. */ function repoWith(present: string[]) { const asked: any[] = [] return { asked, async find(opts: any) { asked.push(opts) const t = opts?.where?.eventType const hit = typeof t === 'string' ? present.includes(t) : /* Like(…) 객체 — 접두사로 본다 */ present.some(p => p.startsWith(String(t?._value ?? '').replace('%', ''))) return hit ? [{ id: 'x' }] : [] } } as any } const WHERE = { domain: { id: 'd' }, instanceId: 'twin-1' } test('저널에 그 축의 사건이 있으면 그렇게 말한다 — 작업', async () => { const repo = repoWith(['task.status']) assert.equal(await axisSeenInJournal(repo, WHERE, 'tasks'), true) }) test('없으면 예전 그대로 — 아직 없다', async () => { const repo = repoWith(['order.status']) assert.equal(await axisSeenInJournal(repo, WHERE, 'tasks'), false) }) test('물품은 사건 이름이 여럿이라 접두사로 본다 — 커널이 이름을 늘려도 낡지 않게', async () => { const repo = repoWith(['epcis.AggregationEvent']) assert.equal(await axisSeenInJournal(repo, WHERE, 'items'), true) }) test('판단할 수 없는 축은 아무 말도 만들지 않는다', async () => { const repo = repoWith(['task.status']) assert.equal(await axisSeenInJournal(repo, WHERE, 'locations'), null, '표에 없는 축은 모른다') assert.equal(repo.asked.length, 0, '모르는 축을 두고 저널을 묻지 않는다') }) test('한 건만 묻는다 — 세는 것도 비용이다', async () => { const repo = repoWith(['task.status']) await axisSeenInJournal(repo, WHERE, 'tasks') assert.equal(repo.asked.length, 1) assert.equal(repo.asked[0].take, 1, '한 건만 읽는다') assert.deepEqual(repo.asked[0].select, ['id'], '키 컬럼만 읽는다(행 본문을 만들지 않는다)') })