/* * ★ **다시 넘길 것인가, 언제인가** — 일꾼의 판단 전부. * * ── 왜 이 판단이 필요한가 (2026-09-04) ───────────────────────────────────── * 승인 직후에 한 번 보내는 것만 있었다. 그래서 **커밋과 발신 사이에 프로세스가 죽으면** 그 * 조치는 `approved` 로 남고 스스로 나가지 않았다. 그리고 실패한 것을 다시 보내는 것은 사람이 * 시켜야 했다. * * ── 실패가 다 같지 않다 (2026-09-02 실측) ────────────────────────────────── * 저쪽 인증 정보가 없어 503 이었던 것(고치면 됨)과 `ITEM-1` 이 400 이었던 것(고쳐도 이 조치로는 * 안 됨)이 같은 모양이었다. 다 다시 하면 없는 자재를 끝없이 두드리고, 다 포기하면 저쪽을 고쳐 * 놓고도 안 나간다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { AFTER_FIX_PERIOD_MS, AGAIN_BACKOFF_MS, retryDecisionOf } from '../server/service/actuation/retry-plan.js' const NOW = 1_800_000_000_000 const at = (over: Record = {}) => retryDecisionOf({ state: 'failed', dispatchable: true, nowMs: NOW, ...over } as any) /* ── 첫 시도 — 승인 직후 발신이 놓친 것을 줍는다 ─────────────────────────── */ test('★ 승인됐는데 한 번도 안 나간 것은 바로 집는다 — 그 창이 이 일꾼의 존재 이유다', () => { const d = at({ state: 'approved', attempts: 0 }) assert.equal(d.take, true) assert.equal(d.attempt, 1) }) test('★ 첫 시도는 멱등을 묻지 않는다 — 한 번뿐이라 두 건이 설 수 없다', () => { assert.equal(at({ state: 'approved', attempts: 0, idempotent: false }).take, true) }) test('★ 첫 시도는 어댑터의 답을 묻지 않는다 — 아직 아무 답도 하지 않았다', () => { assert.equal(at({ state: 'approved' }).take, true) }) /* ── 재시도 — 모르면 넘기지 않는다 ──────────────────────────────────────── */ test('★ 어댑터가 다시 될 일인지 말하지 않았으면 집지 않는다 — 「never」로 접지도 않는다', () => { /* * 모르는 것을 `never` 로 접으면 고칠 수 있는 것을 사람이 포기한다. 그래서 집지 않고 목록에 * 「모름」으로 남긴다 — 사람이 보고 정한다. */ const d = at({ attempts: 1, idempotent: true }) assert.equal(d.take, false) assert.equal(d.skip, 'outlook-unknown') }) test('★ 멱등을 선언하지 않은 커넥터는 재시도하지 않는다 — 현장에 지시가 두 건 서면 안 된다', () => { const d = at({ attempts: 1, retry: 'again' }) assert.equal(d.take, false) assert.equal(d.skip, 'not-idempotent') }) test('★ 영원히 안 될 것은 집지 않는다 — 사람이 새 조치를 내야 한다', () => { const d = at({ attempts: 1, retry: 'never', idempotent: true }) assert.equal(d.take, false) assert.equal(d.skip, 'never', '우리 쪽에 고칠 것이 없다는 뜻이다') }) /* ── `again` — 짧게 세 번, 그다음 접는다 ────────────────────────────────── */ test('★ 물러서면서 세 번 한다', () => { for (const [i, expected] of AGAIN_BACKOFF_MS.entries()) { const d = at({ attempts: i, retry: 'again', idempotent: true, state: i === 0 ? 'approved' : 'failed' }) assert.equal(d.take, true, `${i + 1}번째를 안 했다`) assert.equal(d.attempt, i + 1) assert.equal(Date.parse(d.nextAttemptAt!) - NOW, expected, `${i + 1}번째 물러섬이 다르다`) } }) test('★ 세 번을 다 쓰면 접는다 — 끝없이 두드리는 것은 소음이다', () => { const d = at({ attempts: AGAIN_BACKOFF_MS.length, retry: 'again', idempotent: true }) assert.equal(d.take, false) assert.equal(d.skip, 'attempts-spent') }) /* ── `after-fix` — 접지 않는다. 이 파일에서 가장 중요한 것 ──────────────── */ test('★ 설정 문제는 횟수로 접지 않는다 — 고쳐 놓고도 손으로 눌러야 하면 안 된다', () => { /* * 2026-09-02 사고가 그 모양이었다: 저쪽이 고쳐진 뒤 사람이 손으로 다시 넘겨서 나갔다. * `after-fix` 는 재시도가 아니라 **기다리는 것**이다. */ for (const attempts of [1, 3, 10, 100]) { const d = at({ attempts, retry: 'after-fix', idempotent: true }) assert.equal(d.take, true, `${attempts}번째에 접었다`) assert.equal(Date.parse(d.nextAttemptAt!) - NOW, AFTER_FIX_PERIOD_MS, '간격이 늘어났다') } }) /* ── 물러섬을 표에서 읽는다 ─────────────────────────────────────────────── */ test('★ 아직 이르면 기다린다', () => { const d = at({ attempts: 1, retry: 'again', idempotent: true, nextAttemptAt: new Date(NOW + 10_000).toISOString() }) assert.equal(d.take, false) assert.equal(d.skip, 'backing-off') }) test('★ 때가 되면 집는다', () => { const d = at({ attempts: 1, retry: 'again', idempotent: true, nextAttemptAt: new Date(NOW - 1).toISOString() }) assert.equal(d.take, true) }) test('★ 읽을 수 없는 시각으로 막지 않는다 — 막으면 그 조치가 표에 갇힌다', () => { const d = at({ attempts: 1, retry: 'again', idempotent: true, nextAttemptAt: '언젠가' }) assert.equal(d.take, true) }) /* ── 넘길 수 없는 상태 ──────────────────────────────────────────────────── */ test('★ 종착이면 아무것도 하지 않는다', () => { const d = at({ state: 'acked', dispatchable: false, attempts: 1, retry: 'again', idempotent: true }) assert.equal(d.take, false) assert.equal(d.skip, 'not-dispatchable') }) test('넘길 수 없으면 다른 어떤 값도 그것을 뒤집지 않는다', () => { for (const retry of ['again', 'after-fix', 'never', undefined]) { assert.equal(at({ dispatchable: false, attempts: 0, retry, idempotent: true }).skip, 'not-dispatchable') } })