/* * **적은 것도 추이로 남는다** — 「저널이 얼마나 쓰이고 있나」를 나중에도 물을 수 있게 (2026-08-20). * * ── 왜 필요한가 ───────────────────────────────────────────────────────────── * 계기(`journalRate`·`journaledTotal`)는 **지금**만 답한다. 프로세스를 다시 띄우면 0 에서 시작하므로 * 「어제 이 시각에도 이랬나」·「고친 뒤로 정말 줄었나」를 물을 자리가 없었다. 그런데 2026-08-20 에 고친 * 것이 바로 쓰기 경로(델타마다 한 행 → 창당 한 번)다. 세지 않는 개선은 되돌아가도 아무도 모른다. * * ── 왜 새 장부를 만들지 않았나 ────────────────────────────────────────────── * 유입 창은 「닫힌 창만 최근」·「0 과 없음을 가른다」·「밀려난 것을 세어 드러낸다」를 이미 지키고, 닫힐 때 * 행으로 남는다. 같은 규율을 두 벌 만들면 한 벌만 고쳐진다. 그래서 그 창에 칸을 더했다. * * ── 이 시험이 지키는 것 ───────────────────────────────────────────────────── * ① 유입이 한 번도 없어도 쓰기가 창을 넘긴다 — 시뮬 트윈이 추이에서 사라지지 않게. * ② 행과 왕복을 **따로** 센다 — 행당 왕복이 1 로 되돌아가면 그것이 보인다. * ③ 쓰기는 **「마지막 유입」을 갱신하지 않는다** — 섞으면 끊긴 피드가 옛 창으로 초록을 낸다. * ④ 없는 것을 0 건 쓰기로 만들지 않는다. * ⑤ 쓰기만 있는 장부의 시각이 1969년으로 뜨지 않는다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { INGEST_WINDOW_MS, STALE_AFTER_MS, ingestHealth, newIngestLedger, recordIngest, recordJournalWrite } from '../server/engine/ingest-health.ts' const T0 = 1_700_000_000_000 /* ── ① 유입 없이도 추이가 생긴다 ──────────────────────────────────────────── */ test('받는 것 없이 적기만 하는 트윈도 추이에 남는다 — 시뮬이 오류 없이 빠지지 않게', () => { const l = newIngestLedger() const closed: any[] = [] recordJournalWrite(l, 40, T0, w => closed.push(w)) recordJournalWrite(l, 60, T0 + 1000, w => closed.push(w)) /* 창 길이를 넘긴 뒤의 쓰기가 앞 창을 닫는다. */ recordJournalWrite(l, 10, T0 + INGEST_WINDOW_MS + 1, w => closed.push(w)) assert.equal(closed.length, 1, '창이 닫혀 영속 훅까지 불린다') assert.equal(closed[0].rows, 100) assert.equal(closed[0].offered, 0, '유입은 0 이다 — 그것도 사실이다') const view = ingestHealth(l, 'not-applicable', undefined, T0 + INGEST_WINDOW_MS + 2) assert.equal(view.trend.length, 1, '유입 시각이 없다는 이유로 빈 화면을 내지 않는다') assert.equal(view.trend[0].rows, 100) assert.equal(view.total.rows, 110, '누적은 열린 창까지 센다') }) /* ── ② 행과 왕복을 따로 센다 ──────────────────────────────────────────────── */ test('행과 왕복을 따로 센다 — 행당 왕복이 1 로 되돌아가면 그것이 보인다', () => { const batched = newIngestLedger() recordJournalWrite(batched, 500, T0) recordJournalWrite(batched, 500, T0 + 200) assert.equal(batched.rows, 1000) assert.equal(batched.flushes, 2, '모아 보낸 것은 두 번이다') /* 되돌아간 모양 — 같은 1,000행에 왕복 1,000번. 행 수만 세면 두 장부가 똑같이 보인다. */ const perDelta = newIngestLedger() for (let i = 0; i < 1000; i++) recordJournalWrite(perDelta, 1, T0 + i) assert.equal(perDelta.rows, batched.rows, '행 수는 같다 — 그래서 행만으로는 가려낼 수 없다') assert.ok(perDelta.flushes > batched.flushes * 100, '왕복 수가 그 차이를 드러낸다') }) /* ── ③ 축을 섞지 않는다 ───────────────────────────────────────────────────── */ test('쓰기가 「마지막 유입」을 갱신하지 않는다 — 끊긴 피드가 초록으로 보이지 않게', () => { const l = newIngestLedger() /* 건강한 유입이 한동안 있었고, 그 뒤 피드가 끊겼다. */ recordIngest(l, 100, [], T0) recordIngest(l, 100, [], T0 + INGEST_WINDOW_MS + 1) /* 앞 창을 닫아 「최근」을 만든다 */ const muchLater = T0 + INGEST_WINDOW_MS + STALE_AFTER_MS + 60_000 /* 그 사이에도 트윈은 틱이 만든 델타를 계속 적는다 — 라이브에서 실제로 그렇다. */ for (let t = T0 + 60_000; t < muchLater; t += 60_000) recordJournalWrite(l, 5, t) const view = ingestHealth(l, 'attached', undefined, muchLater) /* * 판정이 초록이 아니어야 한다. 어떤 말로 부르는지는 창의 모양에 달렸다(마지막 닫힌 창이 유입 0 이면 * 「유입 없음」, 창이 아직 안 닫혔으면 「끊긴 피드」) — 둘 다 「받고 있지 않다」다. 못 박는 것은 **적고 * 있다는 사실이 「받고 있다」로 번역되지 않는다**는 것이다. */ assert.notEqual(view.verdict, 'healthy', '적고 있다는 사실이 「받고 있다」로 번역되지 않는다') assert.equal(view.lastAt, new Date(T0 + INGEST_WINDOW_MS + 1).toISOString(), '마지막 유입 시각이 쓰기로 밀리지 않는다') assert.equal(view.lastWriteAt, new Date(muchLater - 60_000).toISOString(), '적은 시각은 따로 낸다') }) /* ── ④⑤ 없는 것과 0 ──────────────────────────────────────────────────────── */ test('없는 것을 0 건 쓰기로 만들지 않는다', () => { const l = newIngestLedger() for (const bad of [0, -3, NaN, Infinity]) recordJournalWrite(l, bad as number, T0) assert.equal(l.flushes, 0, '아무 일도 없었으면 왕복도 없다') assert.equal(l.rows, 0) assert.equal(l.lastWriteAtMs, -1, '적지 않았으면 적은 시각도 없다') assert.equal(ingestHealth(l, 'not-applicable', undefined, T0).lastWriteAt, null) }) test('쓰기만 있는 장부의 시각이 1969년으로 뜨지 않는다 — `-1` 을 시각으로 읽지 않는다', () => { const l = newIngestLedger() recordJournalWrite(l, 7, T0) recordJournalWrite(l, 7, T0 + INGEST_WINDOW_MS + 1) const view = ingestHealth(l, 'not-applicable', undefined, T0 + INGEST_WINDOW_MS + 2) assert.equal(view.lastAt, null, '받은 적이 없으면 유입 시각은 **없다**(0 이 아니다)') for (const w of [...view.trend, view.recent!]) { assert.ok(new Date(w.to).getUTCFullYear() >= 2023, `창 끝 시각이 ${w.to} 로 떴다`) } })