Phase 9: Faster Write-Back (CAL-15) — event-driven outbox drain #14

Merged
luckberg merged 26 commits from gsd/phase-09-faster-write-back into main 2026-06-12 21:19:17 -04:00
2 changed files with 36 additions and 15 deletions
Showing only changes of commit b724b3e932 - Show all commits
+12
View File
@@ -873,6 +873,18 @@ export function stopOutboxTrigger(): void {
unsubscribeDrain = null;
}
/**
* IN-03: test-only reset for the module-level concurrency flags.
* isDraining/drainRequested are the entire concurrency contract for the
* single-process deployment and are NOT touched by vi.resetAllMocks(). Tests call
* this in beforeEach so each test starts from a known-quiescent state instead of
* relying on the previous test having drained cleanly. Not for production use.
*/
export function __resetDrainState(): void {
isDraining = false;
drainRequested = false;
}
/**
* Starts the 15-second background outbox drain schedule (polling fallback, D-08).
* Call once at API startup (wired in index.ts beside startBrokerPoller).
+24 -15
View File
@@ -1,5 +1,5 @@
/**
* RED test scaffold: broker/outboxWorker.ts — outbox state machine (D-04, D-07, D-08)
* broker/outboxWorker.ts — outbox state machine (D-04, D-07, D-08)
*
* Behaviors under test:
* 1. runOutboxDrain transitions pending→done on mock 204 response
@@ -8,17 +8,19 @@
* on mock 500 (transient error)
* 4. runOutboxDrain transitions pending→dead when attemptCount reaches MAX_ATTEMPTS
* 5. Edit-as-move (D-04): create row processed BEFORE the linked delete row (groupId)
*
* These tests FAIL (RED) because broker/outboxWorker.ts does not exist yet.
* They will turn GREEN in Plan 03-03 when the implementation is added.
* 6. scheduleOutboxDrain trigger wiring (D-09): signal → prompt drain, trailing re-drain collapse
*/
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from 'vitest';
// This import fails (RED) — broker/outboxWorker.ts does not exist yet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore intentional RED import
import { runOutboxDrain, assembleRruleString, scheduleOutboxDrain, initOutboxTrigger } from '../../src/broker/outboxWorker.js';
import {
runOutboxDrain,
assembleRruleString,
scheduleOutboxDrain,
initOutboxTrigger,
stopOutboxTrigger,
__resetDrainState,
} from '../../src/broker/outboxWorker.js';
import { signalOutboxDrain } from '../../src/lib/outboxTrigger.js';
// ── Drizzle DB mock ────────────────────────────────────────────────────────
@@ -667,8 +669,7 @@ describe('runOutboxDrain — fail closed on bad credentials (CR-03) + backoff in
});
// ─── D-06: assembleRruleString unit tests ─────────────────────────────────────
// These tests import the NOT-YET-EXPORTED `assembleRruleString` helper.
// RED: will fail because assembleRruleString is not exported yet.
// Exercise the exported `assembleRruleString` helper.
describe('assembleRruleString (D-06)', () => {
it('returns base preset unchanged when no bound given', () => {
@@ -701,8 +702,8 @@ describe('assembleRruleString (D-06)', () => {
});
// ─── D-07: FREQ-persistence regression lock ────────────────────────────────────
// RED: will fail because the outbox worker does not yet wire recurrenceUntil/recurrenceCount
// and the FREQ-persistence assertion catches the D-07 regression scenario.
// The FREQ-persistence assertion catches the D-07 regression scenario
// (recurrenceUntil/recurrenceCount wiring must not drop the FREQ).
describe('FREQ persistence (D-07 regression)', () => {
beforeEach(() => {
@@ -741,8 +742,6 @@ describe('FREQ persistence (D-07 regression)', () => {
// SC-1: signalOutboxDrain() → drain fires promptly (no 15s wait)
// D-05: mid-drain signal collapses to exactly one trailing re-drain
// D-07: concurrent scheduleOutboxDrain() calls dispatch each row exactly once
//
// RED: these tests fail because scheduleOutboxDrain is not yet exported.
describe('scheduleOutboxDrain — trigger wiring (D-09)', () => {
// Wire the EventEmitter signal → scheduleOutboxDrain once for this describe block.
@@ -752,8 +751,18 @@ describe('scheduleOutboxDrain — trigger wiring (D-09)', () => {
initOutboxTrigger();
});
// WR-03: remove the leaked 'drain' listener so it cannot fire against the mocked
// DB in subsequent describe blocks (fileParallelism:false shares one module instance).
afterAll(() => {
stopOutboxTrigger();
});
beforeEach(() => {
vi.resetAllMocks();
// WR-03 / IN-03: module-level isDraining/drainRequested are not reset by
// vi.resetAllMocks(); reset them explicitly so each test starts quiescent
// instead of relying on the previous test having drained cleanly.
__resetDrainState();
mockPendingRows = [];
wireMockChain();
});