/* * 첫 봉투가 몇 번부터 시작했는지 남기는가. * * ── 무엇이 있었나 (2026-09-06 실측, 인티그레이션 레인이 찾음) ──────────────── * plant 아웃박스의 1~20 이 400 으로 거절돼 그 연결에 커서가 없었다. 다음에 온 21번이 첫 봉투로 * 앉으며 커서가 21 로 뛰었다. * * 커서가 0 이었다면 같은 21이 구멍이었다. 둘의 차이가 그 결함의 전부다. 스무 건이 구멍으로도 안 * 세어지고, 경고도 없고, backfill 대상도 아니었다 — backfill 은 커서부터 묻는다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { firstSeqOf, seqOf, takeInSequence, withSeq } from '../server/service/reference/hook-contract.js' const item = (seq: number) => ({ seq, record: { at: '2026-09-06T00:00:00.000Z' } }) test('★ 커서가 없는 연결에 21번이 오면 그 앞 20건을 못 봤다고 말한다', () => { const got = takeInSequence({ items: [item(21)], scope: 'EVENTS', lastSeq: undefined }) assert.equal(got.firstUnseenBefore, 20, '못 본 개수를 안 말하면 그 사실이 아무 데도 안 남는다') assert.equal(got.advancedSeq, 21) assert.equal(got.gap, undefined, '첫 봉투를 구멍으로 만들지 않는다 — 거절할 근거가 없다') }) test('★ 커서가 0 이면 같은 21번이 구멍이다 — 둘의 차이가 이 결함의 전부다', () => { const got = takeInSequence({ items: [item(21)], scope: 'EVENTS', lastSeq: 0 }) assert.equal(got.gap?.expectedSeq, 1) assert.equal(got.firstUnseenBefore, undefined, '구멍은 첫 봉투가 아니다') }) test('1번부터 시작한 연결은 못 본 것이 없다 — 그 칸을 내지 않는다', () => { const got = takeInSequence({ items: [item(1)], scope: 'EVENTS', lastSeq: undefined }) assert.equal(got.firstUnseenBefore, undefined, '0 을 내면 화면이 말할 것 없는 줄을 그린다') assert.equal(got.advancedSeq, 1) }) test('늦게 붙은 연결도 수만 말한다 — 손실이라고 하지 않는다', () => { /* * 원본이 오래 돌고 있는데 트윈을 나중에 붙이면 seq 4,712 부터 받는 것이 정상이고, 그 아래 * 4,711 개는 애초에 안 받기로 한 것이다. 계약은 둘을 구별할 수 없으므로 수만 낸다. */ const got = takeInSequence({ items: [item(4712)], scope: 'EVENTS', lastSeq: undefined }) assert.equal(got.firstUnseenBefore, 4711) assert.equal(got.advancedSeq, 4712, '늦게 붙은 것을 거절하지 않는다') }) test('이어받는 봉투에는 그 칸이 없다', () => { const got = takeInSequence({ items: [item(49)], scope: 'EVENTS', lastSeq: 48 }) assert.equal(got.firstUnseenBefore, undefined) assert.equal(got.advancedSeq, 49) }) test('★ 이 테스트가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => { const first = takeInSequence({ items: [item(21)], scope: 'EVENTS', lastSeq: undefined }) const cont = takeInSequence({ items: [item(21)], scope: 'EVENTS', lastSeq: 20 }) assert.notEqual(first.firstUnseenBefore, cont.firstUnseenBefore, '첫 봉투와 이어받은 봉투가 같아졌다') }) /* ── 몇 번부터 받기 시작했나를 커서에 적는다 ────────────────────────────────── */ test('★ 첫 봉투의 번호를 커서에 적는다 — 「앞이 비었나」를 나중에 답하려면 이 값이 있어야 한다', () => { const got = takeInSequence({ items: [item(21)], scope: 'EVENTS', lastSeq: undefined }) assert.equal(got.firstSeq, 21) const cursor = withSeq({}, 'mes-line-a::EVENTS', got.advancedSeq!, got.firstSeq) assert.equal(firstSeqOf(cursor, 'mes-line-a::EVENTS'), 21) assert.equal(seqOf(cursor, 'mes-line-a::EVENTS'), 21) }) test('★ 한 번 적힌 firstSeq 는 안 바뀐다 — 덮으면 「앞이 비었나」를 다시 못 묻는다', () => { /* * 커서가 나아간 뒤에 이 값을 만들면 그 번호는 첫 번호가 아니라 그때 마침 온 번호다. */ let cursor = withSeq({}, 'S', 21, 21) cursor = withSeq(cursor, 'S', 22, 22) cursor = withSeq(cursor, 'S', 48, 48) assert.equal(firstSeqOf(cursor, 'S'), 21, 'firstSeq 가 덮였다') assert.equal(seqOf(cursor, 'S'), 48) }) test('★ 적힌 적이 없으면 모른다고 말한다 — 0 으로 답하면 1번부터 다 받은 것으로 읽힌다', () => { const old = withSeq({}, 'S', 48) assert.equal(firstSeqOf(old, 'S'), undefined) assert.equal(seqOf(old, 'S'), 48, '옛 커서의 번호는 그대로 읽힌다') }) test('이어받는 봉투는 firstSeq 를 안 낸다 — 그 값을 만들 근거가 없다', () => { const got = takeInSequence({ items: [item(49)], scope: 'EVENTS', lastSeq: 48 }) assert.equal(got.firstSeq, undefined) }) test('1번부터 시작한 연결도 firstSeq 를 적는다 — 못 본 것이 없다는 사실도 사실이다', () => { const got = takeInSequence({ items: [item(1)], scope: 'EVENTS', lastSeq: undefined }) assert.equal(got.firstSeq, 1) assert.equal(got.firstUnseenBefore, undefined, '못 본 것이 0이면 그 칸은 안 낸다') }) test('★ 밑에서부터 다시 받으면 그 번호가 firstSeq 가 된다 — from: 0 은 first 판정이 아니다', () => { /* * 실제로 났다(2026-09-06). `fillTwinScope(from: 0)` 으로 잃은 구간을 되찾았는데 커서에 * `firstSeq` 가 안 적혔다. 커서가 0 이라 seq 1 이 `next` 판정이었고 `first` 가 아니었기 때문이다. * * 그런데 우리는 실제로 1번부터 갖게 됐다. `lowestSeq` 가 그 사실을 낸다. */ const got = takeInSequence({ items: [item(1), item(2), item(3)], scope: 'EVENTS', lastSeq: 0 }) assert.equal(got.firstSeq, undefined, '커서가 있었으므로 첫 봉투가 아니다') assert.equal(got.lowestSeq, 1, '받아들인 것 중 가장 낮은 번호') assert.equal(got.advancedSeq, 3) }) test('가장 낮은 번호는 하나도 못 받으면 없다', () => { const got = takeInSequence({ items: [item(9)], scope: 'EVENTS', lastSeq: 4 }) assert.equal(got.lowestSeq, undefined, '구멍이라 아무것도 안 받았다') assert.equal(got.gap?.expectedSeq, 5) }) /* ── 봉투의 시각 ────────────────────────────────────────────────────────────── */ test('★ 시각을 안 싣는 레코드에 봉투의 시각을 얹는다 — 안 얹으면 다시 받을 때마다 새 사실이 된다', () => { /* * [측정함 2026-09-06] `fillTwinScope(from: 0)` 을 두 번 돌렸더니 mes-line-a 저널이 40 → 69건이 * 되고 중복이 27가지 생겼다. 시각을 안 싣는 레코드는 ingest 시각으로 떨어지는데 그 값이 실행할 * 때마다 달라서 fact identity 가 매번 바뀐다. 시각을 말하는 12건만 되풀이로 걸러졌다. * * plant 아웃박스는 그 시각을 봉투에 실어 보내고 있었다. 갈 자리가 없어서 버려지던 값이다. */ const got = takeInSequence({ items: [{ seq: 1, record: { subjectId: 'L', decision: 'scrap' }, at: '2026-09-01T00:00:00.000Z' }], scope: 'EVENTS', lastSeq: 0 }) assert.equal((got.records[0] as any).at, '2026-09-01T00:00:00.000Z') }) test('★ 레코드가 자기 시각을 말하면 그것이 이긴다 — 봉투는 「언제 보냈나」에 가깝다', () => { const got = takeInSequence({ items: [{ seq: 1, record: { subjectId: 'L', decision: 'scrap', at: '2026-08-30T00:00:00.000Z' }, at: '2026-09-01T00:00:00.000Z' }], scope: 'EVENTS', lastSeq: 0 }) assert.equal((got.records[0] as any).at, '2026-08-30T00:00:00.000Z') }) test('봉투가 시각을 안 말하면 레코드를 그대로 둔다 — 없는 값을 만들지 않는다', () => { const rec = { subjectId: 'L', decision: 'scrap' } const got = takeInSequence({ items: [{ seq: 1, record: rec }], scope: 'EVENTS', lastSeq: 0 }) assert.equal(got.records[0], rec, '새 객체를 만들 이유가 없다') })