getHouseholdTimezone(db) — the accessor imported at all three rewired sites
reminderScheduler.ts all-day branch reads stored household_timezone via getHouseholdTimezone(db)
outboxWorker.ts UPDATE all-day branch reads stored household_timezone via getHouseholdTimezone(db)
outboxWorker.ts CREATE all-day branch reads stored household_timezone via getHouseholdTimezone(db)
18-04-PLAN (PWA settings UI — all three rewired sites now respect the timezone set via Plan 18-02 admin API)
added
patterns
getHouseholdTimezone(db) imported and awaited at three all-day broker sites (replacing bare process.env.TZ ?? Intl)
Mock chain extension
makeAppConfigSelectMock for where().limit() chain (different from makeSelectMock which handles innerJoin chains)
mockTwoQueries updated to mock the new third db.select() call, keeping existing tests green via D-06 fallback
wireMockChain() extended in outboxWorker.test.ts to handle app_config table with where().limit() returning []
created
modified
apps/api/src/broker/reminderScheduler.ts
apps/api/src/broker/outboxWorker.ts
apps/api/tests/broker/reminderScheduler.test.ts
apps/api/tests/broker/outboxWorker.test.ts
D-05 wired: three all-day sites replaced bare process.env.TZ ?? Intl with await getHouseholdTimezone(db)
D-06 preserved: getHouseholdTimezone falls back to process.env.TZ → Intl when no app_config row exists; existing tests pin process.env.TZ and pass unchanged
D-07 enforced: eventDateTime.ts and hydrateEvents.ts not modified (verified via git diff)
mockTwoQueries extended to emit a third mockReturnValueOnce for the new app_config SELECT (no-row → D-06 fallback)
mockReset() added in mockTwoQueries/mockThreeQueries to clear unconsumed once-queue entries across vi.resetModules() cycles (Vitest caches mock factory instances)
Three all-day "9 AM local" reminder call sites in reminderScheduler.ts and outboxWorker.ts now route through getHouseholdTimezone(db) instead of the bare process.env.TZ ?? Intl expression, making the stored household_timezone the source of truth for all-day reminder fire times (D-05/D-06/D-07)
Performance
Duration: 28 min
Started: 2026-06-15T02:04:00Z
Completed: 2026-06-15T02:32:01Z
Tasks: 2 (TDD RED + GREEN)
Files modified: 4
Accomplishments
Rewired reminderScheduler.ts line 247: const serverTz = await getHouseholdTimezone(db);
Added 4 new RED tests (2 scheduler + 2 outbox) verifying stored 'America/Chicago' drives the 9 AM alert instant; all correctly FAIL before the rewire
Extended test mock infrastructure: makeAppConfigSelectMock, mockThreeQueries, updated mockTwoQueries to add third call for D-06 fallback path, updated wireMockChain() in outboxWorker.test.ts for app_config table
All 76 broker tests pass after GREEN commit; TypeScript typecheck (tsc --noEmit) clean
D-07 boundary confirmed: eventDateTime.ts and hydrateEvents.ts not in changed-files list
Task 2: GREEN — rewire all three all-day TZ call sites - c80845c (feat)
Files Created/Modified
apps/api/src/broker/reminderScheduler.ts — import added + line 247 rewired to await getHouseholdTimezone(db)
apps/api/src/broker/outboxWorker.ts — import added + lines ~501 and ~607 rewired to await getHouseholdTimezone(db)
apps/api/tests/broker/reminderScheduler.test.ts — makeAppConfigSelectMock, mockThreeQueries helpers added; mockTwoQueries extended to mock the new third db.select(); Plan 18-03 describe block with 2 stored-TZ tests
apps/api/tests/broker/outboxWorker.test.ts — wireMockChain() extended for app_config table; wireMockChainWithTz() helper for D-05 stored-TZ tests; Plan 18-03 describe block with 2 stored-TZ tests (create + update branch)
Decisions Made
mockTwoQueries was extended (not renamed) to avoid updating 20+ call sites. The third mock call returns an empty app_config row (no stored TZ → D-06 fallback), which is transparent to all existing tests that pin process.env.TZ.
mockReset() added inside mockTwoQueries and mockThreeQueries to flush any unconsumed mockReturnValueOnce entries. Vitest caches mock factory instances across vi.resetModules() cycles, so the pending third entry from one test bleeds into the next test's queue without an explicit reset.
The outbox UPDATE branch test uses mockWhereCalEvents.mockResolvedValue([{etag: 'W/"abc"'}]) after wireMockChainWithTz to drive the freshEtagRows path into the all-day VALARM branch (no rawVevent → falls through to the explicit-reminder/allDay condition).
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] vi.fn() queue contamination across tests with vi.resetModules()
Found during: Task 1 (RED) — second scheduler test crashed with "innerJoin is not a function"
Issue:vi.clearAllMocks() does not flush mockReturnValueOnce queues. Vitest's mock factory for vi.mock() returns the SAME vi.fn() instance across module resets, so an unconsumed third entry from the first test bled into the first call slot of the second test — causing makeAppConfigSelectMock to be returned where makeSelectMock was expected.
Fix: Added db.select.mockReset() at the start of both mockTwoQueries and mockThreeQueries to explicitly clear the queue before configuring new once-values.
Found during: Task 2 (GREEN) — the existing CAL-13 all-day test would have broken after the rewire because app_config fell through to { where: mockWherePending }, and mockWherePending(...) returns a Promise; calling .limit(1) on a Promise throws "limit is not a function"
Fix: Extended wireMockChain() to handle app_config table with a { where: fn → {limit: fn} } chain returning empty rows (D-06 fallback), matching the getHouseholdTimezone SELECT chain
3. [Rule 2 - Missing critical functionality] mockTwoQueries needed a third mock for the new db.select() call
Found during: Task 2 (GREEN) — all 20 existing scheduler tests that call mockTwoQueries started failing because runReminderCheck now issues a third db.select() for getHouseholdTimezone; the undefined return caused crashes
Fix: Updated mockTwoQueries to add a third mockReturnValueOnce(makeAppConfigSelectMock(null)) returning no row, keeping the D-06 fallback path for all existing tests
None — all three rewired sites now read from the actual DB through getHouseholdTimezone(db). No placeholder values.
Threat Flags
None — no new network endpoints, auth paths, file access patterns, or schema changes. The three rewired sites are read-only DB lookups within the trusted server process (T-18-08 disposition: mitigate → validated values only reach computeAlertInstantUtc via the Plan 01 accessor that enforces the D-06 fallback chain).
TDD Gate Compliance
RED gate: 94daca3 — test(18-03): add failing stored-TZ all-day tests for scheduler + outbox
GREEN gate: c80845c — feat(18-03): route all-day reminder TZ through stored household_timezone
REFACTOR gate: N/A (implementation was clean on first pass; deviations were auto-fixed inline)
Self-Check: PASSED
apps/api/src/broker/reminderScheduler.ts — FOUND
apps/api/src/broker/outboxWorker.ts — FOUND
apps/api/tests/broker/reminderScheduler.test.ts — FOUND
apps/api/tests/broker/outboxWorker.test.ts — FOUND