Session 21: All adapters through gateway, ntfy alerts, provider registry correction (1486 tests)

This commit is contained in:
Kev
2026-06-12 02:06:22 -04:00
parent 9b10bb4138
commit ea848e327e
14 changed files with 614 additions and 46 deletions
+182 -1
View File
@@ -4,7 +4,188 @@
2026-06-12
## Current Phase
SHIP BUILD v20.0 — Provider intelligence: quota tracker, gateway, key rotation (Session 20)
SHIP BUILD v21.0 — Every external HTTP call tracked + ntfy alerts (Session 21)
## Session 21 (2026-06-12) — SHIPPED
Session 20 built the gateway; Session 21 wires every adapter
through it. Plus: ntfy push at WARN (80%) and BLOCK (95%), an
end-to-end integration test, and an honest correction to the
provider registry that flags a critical misconception in the
original spec.
### PHASE 1 — Provider trace + registry correction (CRITICAL)
The Session 20 registry classified `oddspapi` and `parlayapi` as
live-odds fallback providers for `the-odds-api`. **They are not.**
Tracing the existing adapters revealed:
- **`oddsPapiAdapter`** — Pinnacle CLOSING-line capture at
tip-off. Writes to `closing_lines` table for CLV. One row per
game. NOT a live-props source.
- **`parlayApiAdapter`** — historical archive (1K credits/month).
Used by bulk scripts and trap detection. NOT real-time.
Registry corrected:
- `oddspapi.capabilities = ['closing_lines']`, name = "ODDSPAPI
(Pinnacle close)"
- `parlayapi.capabilities = ['historical_props',
'historical_lines']`, name = "ParlayAPI (historical)"
- Both bumped to `priority: 1` for their actual capability sets
**Consequence:** `getFallbackChain('odds', 'nba', 'odds-api')` now
returns `[]` because no other configured provider serves live
`odds`. The gateway's QuotaExhaustedError path is honest about
this: when the-odds-api hits 95%, there is no fallback to take
over. The fix to the original 503 incident is operational (higher
tier, better caching, or a real new provider), not architectural.
### PHASE 2 — Tank01 NBA + MLB through gateway
Single axios.get call site in each adapter's `fetchWithCache` —
wrapped in `gateway.fetch('tank01', () => axios.get(...), {
capability: 'box_scores', sport })`. Existing cache + stale-while-
revalidate logic untouched.
Tests: existing 51 Tank01 tests all pass unchanged.
### PHASE 3 — API-Football through gateway
Same surgical pattern at the `fetchWithCache` axios.get. The
adapter keeps its own `apifootball:daily_count` Redis counter
(legacy SOFT_LIMIT=90 trigger); the tracker is now ALSO advancing
on every successful call. Two counters, one truth source: tracker
drives WARN/BLOCK; legacy counter drives the local
stale-while-revalidate switch.
Tests: 16/16 unchanged.
### PHASE 4 — Football-Data through gateway
Wrap pattern as above. The adapter's in-process token bucket
(8 req/min) short-circuits BEFORE the gateway — so the gateway
counter only ticks for calls that actually went over the wire.
Order: bucket → gateway → axios.
Tests: 15/15 unchanged.
### PHASE 5 — ODDSPAPI + ParlayAPI through gateway
Wired for their actual purposes:
- `oddsPapiAdapter.fetchPinnacleProp` → gateway with
`capability: 'closing_lines'`
- `parlayApiAdapter.fetchWithGuards` → gateway with
`capability: 'historical_props'`
Test mock update: `parlayApiAdapter.test.js` mocked redis without
`isDegraded`, which made the gateway's `quotaTracker.recordCall`
throw. Added `isDegraded: () => true` so the gateway falls
through in degraded-mode fail-open — preserves the test's
existing axios+cache assertions.
Tests: 13/13 (10 oddsPapi + 3 parlayApi) pass.
### PHASE 6 — ntfy alerts at WARN + BLOCK
`quotaTracker.sendQuotaAlert(providerCfg, pct, used, limit)`:
- WARN (≥80%) → priority `4` ("high"), title `Warning`
- BLOCK (≥95%) → priority `5` ("urgent"), title `BLOCKED`
- Disabled when `NTFY_URL` env unset (default in dev)
- Fire-and-forget (`.catch(() => {})`) so a slow ntfy server
can't add latency to the adapter's HTTP call
- ntfy POST failure → console.warn only; recordCall still
returns the normal status
Two dedupe keys per period:
- `quota_warned:{provider}:{period}` — WARN sentinel
- `quota_warned:{provider}:{period}:block` — BLOCK sentinel
This handles the WARN→BLOCK transition correctly: a provider
that jumps from 79% → 96% in one call fires the BLOCK alert
even though the WARN sentinel was never set. Without the
separate key, the operator wouldn't get the BLOCK notice (the
actionable one).
6 new tests cover: no-post when NTFY_URL unset, priority 4 at
80%, priority 5 at 95%, dedupe (3 calls → 1 alert), WARN→BLOCK
transition fires BOTH alerts, axios.post failure preserves
recordCall return.
### PHASE 7 — End-to-end gateway wiring test
`tests/integration/providerGatewayWiring.test.js` — 4 tests
through the Tank01 NBA adapter (chosen because its
`fetchWithCache` has no token-bucket/circuit-breaker; the
gateway behavior dominates):
1. Successful adapter call → tank01 counter goes 0 → 1
2. Cache hit → no HTTP, counter stays
3. Counter seeded to 95% via `syncFromHeaders` → adapter
returns `null` (cache miss + no stale = degrade to null);
axios.get NEVER called
4. axios throws → gateway rolls back the optimistic increment;
counter restored to pre-call value
### Honest scope flags
- **No new ODDSPAPI/ParlayAPI live-props adapter.** The spec
asked for one; reality is they don't serve live props. Built
documentation in the registry instead.
- **No "provider-aware callback architecture" abstraction
(Phase 2 of the spec).** Each adapter is already provider-aware
(it knows its URL, key, auth) — adding a meta-adapter that
switches between them per-call is premature without a real
fallback chain. Worth revisiting if/when a true live-odds
alternative provider is onboarded.
- The "documentation" phase wasn't applied to a separate
playbook file (none exists at the repo root); the corrections
+ per-provider wiring rationale live in the adapter files and
this BUILD-STATE entry, which is the closest the repo has to
a playbook.
### Battery
- Express suite: **115 passed / 1486 tests** (+10 over baseline
1476). Breakdown of new tests:
- 6 ntfy in quotaTracker.test.js
- 4 in providerGatewayWiring.test.js (new file)
- Web build: **clean**, no TS errors. Admin route still resolves.
### Files changed (Session 21)
**Created:**
- `tests/integration/providerGatewayWiring.test.js`
**Modified:**
- `src/config/providers.js` — capability corrections for
oddspapi + parlayapi
- `src/services/quotaTracker.js` — `sendQuotaAlert` + WARN/BLOCK
dedupe key split
- `src/services/adapters/tank01NbaAdapter.js` — gateway wrap
- `src/services/adapters/tank01MlbAdapter.js` — gateway wrap
- `src/services/adapters/apiFootballAdapter.js` — gateway wrap
- `src/services/adapters/footballDataAdapter.js` — gateway wrap
- `src/services/adapters/oddsPapiAdapter.js` — gateway wrap
- `src/services/adapters/parlayApiAdapter.js` — gateway wrap
- `tests/unit/quotaTracker.test.js` — 6 ntfy tests + axios mock
- `tests/unit/parlayApiAdapter.test.js` — `isDegraded` in mock
### Provider wiring status (after Session 21)
| Provider | Gateway-wired | Capability | Quota visible |
|----------------|---------------|-----------------|---------------|
| the-odds-api | ✅ (Session 20) | odds/props | ✅ |
| Tank01 NBA+MLB | ✅ | box_scores | ✅ |
| API-Football | ✅ | lineups/stats | ✅ |
| Football-Data | ✅ | fixtures/tables | ✅ |
| ODDSPAPI | ✅ | closing_lines | ✅ |
| ParlayAPI | ✅ | historical | ✅ |
Every external HTTP call from the app now flows through
`gateway.fetch()`. The admin dashboard's Provider quotas tile
shows real numbers for every one of them.
---
## Session 20 (2026-06-12) — SHIPPED