diff --git a/docs/superpowers/specs/2026-03-11-robustness-plan-design.md b/docs/superpowers/specs/2026-03-11-robustness-plan-design.md index 0824f5f..c9b8762 100644 --- a/docs/superpowers/specs/2026-03-11-robustness-plan-design.md +++ b/docs/superpowers/specs/2026-03-11-robustness-plan-design.md @@ -10,7 +10,7 @@ ### 1a. Retry Utility -**File:** `src/shared/retry.ts` +**File:** `src/shared/retry.ts` (isomorphic — used by both server API clients and client feed assembly) Generic `withRetry(fn, opts)`: - Exponential backoff, configurable max attempts (default 3), base delay (default 500ms) @@ -18,6 +18,8 @@ Generic `withRetry(fn, opts)`: - Only retries on transient failures: network errors, 5xx, 429 - Does not retry 4xx or Zod validation errors +Placed in `src/shared/` because both server (2a, 2c) and client (3a feed fetches) consume it. + **Tests:** Unit tests for retry logic, backoff timing, abort behavior, non-retryable error passthrough. ### 1b. Error Boundary Component @@ -48,11 +50,14 @@ Generic `withRetry(fn, opts)`: **Files:** `src/server/shared/jobs/poll-checker.ts`, `src/server/shared/jobs/legislation-syncer.ts` -- Replace `Promise.all` with `Promise.allSettled` for batch vote/vorgänge fetching -- Log failures, continue with successful results (partial success) -- Structured log format: `{ job, action, id, error }` for parseable journalctl output +Both jobs use sequential `for` loops with individual `await` calls. The legislation-syncer already has per-item try-catch; the poll-checker is missing try-catch around several inner awaits (e.g., `resolveMandateId`, vote fetching inside the per-device notification loop), which can break the entire iteration on a single failure. -**Tests:** Mock partial API failures, verify job completes with available data and logs errors. +Fixes: +- Poll-checker: wrap every `await` inside the per-poll and per-device loops in try-catch — log the error with structured format and continue to the next item +- Legislation-syncer: verify existing try-catch coverage is complete, add structured logging +- Structured log format for both: `{ job, action, id, error }` for parseable journalctl output + +**Tests:** Mock individual API call failures mid-iteration, verify job processes remaining items and logs errors. ### 2c. Push Notification Error Handling @@ -87,7 +92,7 @@ Fixes: **Files:** `src/client/features/feed/lib/assemble-feed.ts`, `src/client/features/feed/hooks/use-feed.ts` -- Replace `Promise.all` with `Promise.allSettled` for poll/vote fetching +- Replace `Promise.all` with `Promise.allSettled` for poll/vote fetching in `assemble-feed.ts` (the single assembly file that handles all feed types) - Surface partial failures as a non-blocking warning banner ("some data couldn't be loaded") - Await `saveFeedCache()` + wrap in try-catch (currently fire-and-forget) - Catch `loadFeedCache()` DB errors, fall back to empty cache @@ -101,11 +106,13 @@ Fixes: - Debounce follows sync: 300ms debounce when follows change rapidly - In-flight guard: if sync already running, queue the next one instead of concurrent requests -**Tests:** Simulate rapid follow/unfollow, verify only one sync request in-flight, final state correct. +**Tests:** Use `vi.useFakeTimers()` to simulate rapid follow/unfollow within the debounce window. Mock `syncFollowsToBackend` to track calls. Verify only one sync request in-flight, final state includes all changes. ### 3c. IndexedDB Error Handling -**Files:** All modules in `src/client/shared/db/` — `follows.ts`, `feed-cache-db.ts`, `geo-cache-db.ts`, `push-state-db.ts`, `device.ts` +**Files:** `src/client/shared/db/follows.ts`, `feed-cache-db.ts`, `geo-cache-db.ts`, `push-state-db.ts`, `device.ts` + +Excluded: `client.ts` (PGlite singleton — init errors are already fatal by design) and `migrate-from-localstorage.ts` (one-time migration with its own error handling). - Wrap all PGlite query calls with try-catch - On failure: log error, return sensible defaults (empty arrays, null) @@ -139,11 +146,11 @@ Fixes: ### 4a. Route Error Boundaries Wrap each major route with error boundary from 1b: -- `bundestag`, `landtag` — "couldn't load votes" -- `legislation.$legislationId` — "couldn't load legislation" -- `politician.$politicianId` — "couldn't load politician profile" -- `representatives` — "couldn't load representatives" -- Root layout keeps last-resort catch-all +- `src/client/routes/app/bundestag/index.tsx`, `src/client/routes/app/landtag/index.tsx` — "couldn't load votes" +- `src/client/routes/app/legislation.$legislationId.tsx` — "couldn't load legislation" +- `src/client/routes/app/politician.$politicianId.tsx` — "couldn't load politician profile" +- `src/client/routes/app/representatives.tsx` — "couldn't load representatives" +- `src/client/routes/__root.tsx` keeps last-resort catch-all ### 4b. Loading/Empty States @@ -158,7 +165,7 @@ No additional tests beyond 1b's error boundary tests. This section is primarily ### 5a. Server Integration Tests -- **Legislation router:** Full request→response for GET /upcoming, GET /:id, POST /:id/vote, GET /:id/results/:deviceId, including 400/404 paths +- **Legislation router:** Full request→response for GET /upcoming, GET /:id, POST /:id/vote, GET /:id/results/:deviceId, GET /dip-proxy/vorgaenge, GET /dip-proxy/vorgaenge/:id, including 400/404 paths - **Politician router:** Cache hit vs miss, invalid ID, AW API failure → 500 - **Push router:** Subscribe → sync → test → unsubscribe lifecycle, invalid payloads