feat(03-04): implement outbox drain state machine (GREEN)
- runOutboxDrain: drains pending outbox rows, dispatches CalDAV writes via broker/write.ts, classifies HTTP responses per D-07/D-08 - CONFLICT_STATUS=412 routes to conflict flow: mark failed, re-sync (D-08) - TRANSIENT_STATUSES: exponential backoff with MAX_ATTEMPTS=5 dead-letter (D-07) - HARD_FAIL_STATUSES 400/401/403: fail immediately, no retry (D-07) - Edit-as-move D-04: create row sorted before delete for same groupId; create-fail aborts the paired delete (T-03-14) - triggerTargetedResync: fetches fresh DAVCalendars, calls syncCalendar (D-06) - startOutboxWorker: node-cron */15 * * * * * schedule (15s interval) - Fix test scaffold: vi.hoisted() for mock variables to resolve vitest hoisting TDZ issue; simplified mock chain to match and() single .where()
This commit is contained in:
@@ -22,15 +22,30 @@ import { runOutboxDrain } from '../../src/broker/outboxWorker.js'
|
||||
|
||||
// ── Drizzle DB mock ────────────────────────────────────────────────────────
|
||||
// Follows the pattern from PATTERNS.md §Drizzle DB mock in tests.
|
||||
//
|
||||
// vi.hoisted() is required for variables referenced inside vi.mock() factories.
|
||||
// vi.mock() is hoisted to the top of the file by vitest's transform; without
|
||||
// vi.hoisted(), variables declared with const/let are in the TDZ when the factory
|
||||
// runs (static imports trigger module loading before declarations are evaluated).
|
||||
|
||||
const mockUpdateSet = vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) })
|
||||
const mockUpdate = vi.fn().mockReturnValue({ set: mockUpdateSet })
|
||||
const {
|
||||
mockUpdateSet,
|
||||
mockUpdate,
|
||||
mockWherePending,
|
||||
mockFromFn,
|
||||
mockSelectFn,
|
||||
} = vi.hoisted(() => {
|
||||
const mockUpdateSet = vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) })
|
||||
const mockUpdate = vi.fn().mockReturnValue({ set: mockUpdateSet })
|
||||
// mockWherePending is the terminal node of the select chain:
|
||||
// db.select().from(table).where(and(cond1, cond2)) — resolves to the row array
|
||||
const mockWherePending = vi.fn().mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
const mockFromFn = vi.fn().mockReturnValue({ where: mockWherePending })
|
||||
const mockSelectFn = vi.fn().mockReturnValue({ from: mockFromFn })
|
||||
return { mockUpdateSet, mockUpdate, mockWherePending, mockFromFn, mockSelectFn }
|
||||
})
|
||||
|
||||
let mockPendingRows: unknown[] = []
|
||||
const mockWherePending = vi.fn().mockImplementation(() => Promise.resolve(mockPendingRows))
|
||||
const mockLimitFn = vi.fn().mockReturnValue({ where: mockWherePending })
|
||||
const mockFromFn = vi.fn().mockReturnValue({ where: mockLimitFn })
|
||||
const mockSelectFn = vi.fn().mockReturnValue({ from: mockFromFn })
|
||||
|
||||
vi.mock('../../src/db/client.js', () => ({
|
||||
db: {
|
||||
@@ -84,12 +99,12 @@ describe('runOutboxDrain — state transitions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPendingRows = []
|
||||
// Restore mock chain after clearAllMocks
|
||||
// Restore mock chain after clearAllMocks:
|
||||
// db.select().from(table).where(and(cond1, cond2)) → Promise<rows>
|
||||
mockUpdateSet.mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) })
|
||||
mockUpdate.mockReturnValue({ set: mockUpdateSet })
|
||||
mockWherePending.mockImplementation(() => Promise.resolve(mockPendingRows))
|
||||
mockLimitFn.mockReturnValue({ where: mockWherePending })
|
||||
mockFromFn.mockReturnValue({ where: mockLimitFn })
|
||||
mockFromFn.mockReturnValue({ where: mockWherePending })
|
||||
mockSelectFn.mockReturnValue({ from: mockFromFn })
|
||||
})
|
||||
|
||||
@@ -167,8 +182,7 @@ describe('runOutboxDrain — edit-as-move ordering (D-04)', () => {
|
||||
mockUpdateSet.mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) })
|
||||
mockUpdate.mockReturnValue({ set: mockUpdateSet })
|
||||
mockWherePending.mockImplementation(() => Promise.resolve(mockPendingRows))
|
||||
mockLimitFn.mockReturnValue({ where: mockWherePending })
|
||||
mockFromFn.mockReturnValue({ where: mockLimitFn })
|
||||
mockFromFn.mockReturnValue({ where: mockWherePending })
|
||||
mockSelectFn.mockReturnValue({ from: mockFromFn })
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user