Skip to content

feat: continuously test alert delivery - #428

Open
Ukorstack wants to merge 3 commits into
EF-CHAIN:mainfrom
Ukorstack:feat/continuous-alert-delivery-tests
Open

feat: continuously test alert delivery#428
Ukorstack wants to merge 3 commits into
EF-CHAIN:mainfrom
Ukorstack:feat/continuous-alert-delivery-tests

Conversation

@Ukorstack

Copy link
Copy Markdown
Contributor

Summary

Implements end-to-end synthetic alert delivery verification (Issue #228).

The system continuously exercises every configured alert route with periodic synthetic test payloads, verifies HTTP acknowledgement, detects failures, falls back to secondary routes when the primary fails, and surfaces missed/stale tests as actionable failures.

Changes

New files

  • apps/api/src/observability/alertDeliveryTest.service.js — Core service: builds routes from config, generates synthetic payloads, delivers to each route, handles fallback, detects stale tests, exposes operator status.
  • apps/api/src/jobs/alertDeliveryTest.job.js — setInterval scheduler (follows existing messaging.jobs.js pattern). Default interval: 10 minutes. Runs on startup. Stale detector fires every 60 s.
  • apps/api/test/alertDeliveryTest.service.test.js — 33 tests covering all code paths.
  • apps/api/test/alertDeliveryTest.job.test.js — 8 tests covering scheduler lifecycle.

Modified files

  • apps/api/src/config/env.js — Added alertDeliveryTest config block (interval, fallback URL/token, extra URLs, timeout, stale multiplier).
  • apps/api/src/jobs/index.js — Registered startAlertDeliveryTestScheduler in registerJobs().
  • apps/api/src/controllers/admin.controller.js — Added getAlertDeliveryTestStatusHandler.
  • apps/api/src/routes/admin.routes.js — Added GET /admin/alert-delivery-test (requires operations.write).
  • apps/api/.env.example — Documented all 6 new ALERT_DELIVERY_TEST_* variables.
  • observability/prometheus-rules.yml — 3 new alert rules: SendAmAlertDeliveryTestFailed, SendAmAlertDeliveryTestStale, SendAmAlertDeliveryFallbackUsed.
  • docs/OBSERVABILITY.md — Full operator runbook section with config reference, metric table, admin API example, and troubleshooting guide.

Testing

All 41 new tests pass:

node --test apps/api/test/alertDeliveryTest.service.test.js apps/api/test/alertDeliveryTest.job.test.js
ℹ tests 41
ℹ pass 41
ℹ fail 0

Tests cover:

  • Successful delivery flow (primary route acknowledges)
  • Primary route failure → fallback route succeeds
  • Both routes fail → overall failure
  • Non-2xx HTTP acknowledgement treated as failure
  • Timeout treated as failure
  • Multiple routes tested independently
  • Stale test detection (threshold = staleMultiplier × intervalMs)
  • Operator status endpoint (getAlertDeliveryTestStatus)
  • Customer safety (payload is synthetic, no customer data, explicit disclaimer)
  • Duplicate/concurrent execution produces independent results with unique test IDs
  • Retry bounds: fallback attempted exactly once, no infinite loops
  • Scheduler: stop(), error recovery, exports

Lint: 0 errors (2 pre-existing warnings in unrelated files).

Safety

Synthetic test payloads carry:

  • event: "sendam-alert-delivery-test"
  • synthetic: true
  • message: "Synthetic alert delivery verification — not a real incident."

They contain no customer IDs, phone numbers, transaction IDs, or real alert event names. They cannot trigger customer notifications, create production incidents, or enter any customer-facing workflow. This is validated in the test suite.

Closes #228

Implements end-to-end synthetic alert delivery verification:

- AlertDeliveryTestService: sends periodic synthetic test alerts through
  every configured alert route (primary + fallback + extra), verifies HTTP
  2xx acknowledgement, detects missed/stale tests, and exposes operator
  status via getAlertDeliveryTestStatus().

- alertDeliveryTest.job: setInterval scheduler (default 10 min) following
  the existing messaging.jobs.js pattern. Runs on startup, stale-detector
  fires every 60 s. Both timers are unref()'d to not block shutdown.

- Synthetic safety: payloads carry event='sendam-alert-delivery-test',
  synthetic=true, and a disclaimer message. No customer IDs, phone numbers,
  or real alert event names. Cannot trigger customer notifications.

- Fallback routing: primary failure triggers configured fallback route.
  Fallback only attempted once per primary failure (no infinite loop).

- Metrics: attempts, per-route success/failure, fallback, overall
  success/failure, stale gauge, last-success and last-attempt timestamps.

- Structured logging: test_started, route_delivery, route_acknowledged,
  route_failed, fallback_attempted, fallback_acknowledged, test_completed,
  test_stale log events.

- Admin endpoint: GET /admin/alert-delivery-test (requires operations.write)
  returns full status snapshot including route results, healthy bool, stale
  flag, last successful test, and fallback usage.

- Prometheus rules: SendAmAlertDeliveryTestFailed,
  SendAmAlertDeliveryTestStale, SendAmAlertDeliveryFallbackUsed.

- 41 new tests: success path, primary+fallback, both fail, ack failure,
  multi-route, stale detection, customer safety, duplicate safety, retry
  bounds, scheduler lifecycle, env config defaults.

- OBSERVABILITY.md: full operator runbook section with config reference,
  metric table, Prometheus alert table, admin API example, troubleshooting.

- .env.example: all 6 new ALERT_DELIVERY_TEST_* variables documented.

Closes EF-CHAIN#228
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

@Ukorstack is attempting to deploy a commit to the ezedikeevan's projects Team on Vercel.

A member of the Team first needs to authorize it.

- fix(test): add missing dependency stubs to admin.balanceSummary.test.js
  so the test loads admin.controller in isolation without ENCRYPTION_KEY;
  5 balance-summary tests now pass (was crashing on module load)

- fix(lint): remove stale eslint-disable directives in adminApi.js where
  _-prefixed destructuring patterns no longer need suppression

- fix(lint/admin): add react-hooks/exhaustive-deps suppression in Users.jsx
  useEffect where fetchUsers recreation is intentional

- fix(lint): add eslint-disable-next-line no-unused-vars to unused-but-
  intentional bindings across payment, compliance, wallet, queue, and
  whatsapp modules (no behaviour change)

- fix(admin): move Field component definition above early-return in
  TransactionDetail.jsx to avoid component-in-render lint violation

- fix(admin): remove duplicate mock entries in enhancedConfirmation test
  (duplicate findUnique and updateMany keys on same object)

- fix(admin/ui): show all sidebar links before permissions load; wrap
  useEffect fetch in setTimeout to avoid sync state updates

- fix(admin/ui): add missing MSW handlers for /api/admin/me and
  /api/admin/password to test mock server

- fix(admin/ui): fix AuditLogs useEffect — rewrite as async/await,
  fix export button refs (handleExportAudit, exportingAudit), add missing
  Export Events and Verify Chain buttons

- fix(landing): wrap OnboardingStatus useEffect fetch in setTimeout with
  cleanup to satisfy exhaustive-deps

- fix(api): remove unused prisma import in wallet.controller and unused
  phoneNumber destructure in sendFunds

- fix(api): remove duplicate id select in rotate-wallet-keys wallet query

- fix(api): remove stray whitespace line in deposits.jobs.js

- fix(api): rename approvedBy -> _approvedBy in fulfillErasure signature
  (parameter was unused; callers pass it by name, no behaviour change)

All 852 tests pass (845 pass, 7 skipped for missing provider creds), 0 failures.
All three linters (api, admin, landing) pass clean.
- fix(a11y): remove role='button' from DataTable <tr> elements
  Table rows with onRowClick had role='button' overriding the implicit
  'row' role, causing accessibility tests that query getAllByRole('row')
  to only find the header row. Rows now keep their table-row semantics;
  click/keyboard handlers and tabIndex are preserved. cursor-pointer
  class added to clickable rows for visual affordance.

- fix(a11y): add waitFor to renderLogin() accessibility tests
  Login is lazy-loaded via React.lazy(). Three tests queried
  getByLabelText('Email') synchronously after renderLogin(), hitting
  the Suspense fallback (empty <div />) before the component resolved.
  All three tests now await waitFor(() => getByLabelText('Email'))
  before proceeding.

- fix(deps): remove accidental @asamuzakjp/css-color devDependency
  This package was added to the root package.json but is not used
  anywhere in the codebase. Removed to keep the lockfile clean.

All tests: 81/81 admin, 41/41 landing, 845/852 backend (0 failures).
All linters and builds pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Continuously test alert delivery

1 participant