vi.mock factories with inline vi.fn() only — no outer variable references to avoid TDZ hoisting
created
modified
backend/src/services/alertService.ts
backend/src/__tests__/unit/alertService.test.ts
Transporter created inside sendAlertEmail() on each call — not at module level — avoids Firebase Secret not-yet-available error (PITFALL A)
Suppressed alerts skip BOTH AlertEventModel.create() AND sendMail — prevents duplicate DB rows in addition to duplicate emails
Email failure caught in try/catch and logged via logger.error — never re-thrown so probe pipeline continues
Alert deduplication pattern: check findRecentByService before creating row or sending email
Non-throwing side effects: email, analytics, and similar fire-and-forget paths must never throw
ALRT-01
ALRT-02
ALRT-04
12min
2026-02-24
Phase 2 Plan 03: Alert Service Summary
Nodemailer SMTP alert service with cooldown deduplication via AlertEventModel, config-based recipient, and lazy transporter pattern for Firebase Secret compatibility
Performance
Duration: 12 min
Started: 2026-02-24T19:27:42Z
Completed: 2026-02-24T19:39:30Z
Tasks: 2
Files modified: 2
Accomplishments
alertService.evaluateAndAlert() evaluates ProbeResults and sends email alerts for degraded/down services
Deduplication via AlertEventModel.findRecentByService() with configurable ALERT_COOLDOWN_MINUTES env var
Email recipient read from process.env.EMAIL_WEEKLY_RECIPIENT — never hardcoded (ALRT-04)
Lazy transporter pattern: nodemailer.createTransport() called inside sendAlertEmail() function (Firebase Secret timing fix)
8 unit tests cover all alert scenarios: healthy skip, down/degraded alerts, deduplication, recipient config, missing recipient, email failure, and multi-probe processing
Task Commits
Each task was committed atomically:
Task 1: Create alertService with deduplication and email - 91f609c (feat)
Task 2: Create alertService unit tests - 4b5afe2 (test)
Plan metadata:0acacd1 (docs: complete alertService plan)
Files Created/Modified
backend/src/services/alertService.ts - Alert evaluation, deduplication, and email delivery
backend/src/__tests__/unit/alertService.test.ts - 8 unit tests, all passing
Decisions Made
Lazy transporter:nodemailer.createTransport() called inside sendAlertEmail() on each call, not cached at module level. This is required because Firebase Secrets (EMAIL_PASS) are not injected into process.env at module load time — only when the function is invoked.
Suppress both row and email: When findRecentByService() returns a non-null alert, both AlertEventModel.create() and sendMail are skipped. This prevents duplicate DB rows in the alert_events table in addition to preventing duplicate emails.
Non-throwing email path: Email send failures are caught in try/catch and logged via logger.error. The function never re-throws, so email outages cannot break the health probe pipeline.
Issue: Test file declared const mockSendMail = vi.fn() outside the vi.mock() factory and referenced it inside. Because vi.mock() is hoisted to the top of the file, mockSendMail was accessed before initialization, causing ReferenceError: Cannot access 'mockSendMail' before initialization
Fix: Removed the outer mockSendMail variable. The nodemailer mock factory uses only inline vi.fn() calls. Tests access the mock's sendMail via vi.mocked(nodemailer.createTransport).mock.results[0].value through a getMockSendMail() helper. This is consistent with the project decision: "vi.mock() factories must use only inline vi.fn() to avoid Vitest hoisting TDZ errors" (established in 01-02)
Total deviations: 1 auto-fixed (1 blocking — Vitest TDZ hoisting)
Impact on plan: Required fix for tests to run. No scope creep. Consistent with established project pattern from 01-02.
Issues Encountered
None beyond the auto-fixed TDZ hoisting issue above.
User Setup Required
None - no external service configuration required beyond the existing email env vars (EMAIL_HOST, EMAIL_PORT, EMAIL_SECURE, EMAIL_USER, EMAIL_PASS, EMAIL_WEEKLY_RECIPIENT, ALERT_COOLDOWN_MINUTES) documented in prior research.
Next Phase Readiness
alertService.evaluateAndAlert() ready to be called from the health probe scheduler (Plan 02-04)
All 3 alert requirements satisfied: ALRT-01 (email on degraded/down), ALRT-02 (cooldown deduplication), ALRT-04 (recipient from config)