From 9fb1e0da8499ff7742f29b8f7f5a185b468eec8f Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 12:24:50 -0400 Subject: [PATCH] docs(04-02): complete scoped fan-out primitives plan SUMMARY.md for 04-02: listEmitter + listAccess TDD plan. RED/GREEN gate confirmed. D-04 isolation proven. ioredis not introduced. --- .../04-02-SUMMARY.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .planning/phases/04-shared-lists-live-sync/04-02-SUMMARY.md diff --git a/.planning/phases/04-shared-lists-live-sync/04-02-SUMMARY.md b/.planning/phases/04-shared-lists-live-sync/04-02-SUMMARY.md new file mode 100644 index 0000000..faf5293 --- /dev/null +++ b/.planning/phases/04-shared-lists-live-sync/04-02-SUMMARY.md @@ -0,0 +1,124 @@ +--- +phase: 04-shared-lists-live-sync +plan: "02" +subsystem: api-lib, test-harness +tags: [listEmitter, listAccess, scoped-fanout, D-04, tdd, eventEmitter, sse-primitive] +dependency_graph: + requires: + - 04-01 (lists/list_shares schema, test harness, vitest.config.ts) + provides: + - publishListEvent(listId, event): scoped in-process fan-out + - subscribeListEvents(listId, handler): per-list subscription returning unsub closure + - ListEvent type union + - getAccessibleListIds(userId): owner OR list_shares access-scope query + - fileParallelism:false vitest config (prevents DB test race conditions) + affects: + - apps/api/tests/lib/listEmitter.test.ts (stubs replaced with real assertions) + - apps/api/vitest.config.ts (fileParallelism:false added) +tech_stack: + added: [] + patterns: + - Module-level EventEmitter singleton; per-list channel key list:${listId} + - subscribeListEvents returns unsub closure (emitter.off) + - Two-query union (owned + shared) with Set dedup for getAccessibleListIds + - randomUUID() suffix in test seed helpers to avoid unique-key collisions + - fileParallelism:false to serialize DB test file execution +key_files: + created: + - apps/api/src/lib/listEmitter.ts + - apps/api/src/lib/listAccess.ts + - apps/api/tests/lib/listAccess.test.ts + modified: + - apps/api/tests/lib/listEmitter.test.ts (it.todo stubs replaced with real assertions) + - apps/api/vitest.config.ts (fileParallelism:false; sequence.concurrent:false) +decisions: + - "D-04: In-memory EventEmitter per-list channel isolation confirmed by Test 2 (cross-list negative)" + - "D-18: ioredis NOT introduced; abstraction boundary in listEmitter.ts makes future Redis swap mechanical" + - "vitest fileParallelism:false: global afterEach in test/setup.ts truncates shared MariaDB state; parallel files caused FK violations mid-test" + - "getAccessibleListIds: two-select + Set approach per RESEARCH Finding 3 (not single OR-join) — simpler, equally correct" + - "listAccess.test.ts in tests/lib/ (not src/lib/) per tdd_note convention matching listEmitter placement" +metrics: + duration: "~15 minutes" + completed: "2026-06-09" + task_count: 3 + file_count: 5 +--- + +# Phase 4 Plan 2: Scoped Fan-out Primitives Summary + +**One-liner:** In-memory per-list EventEmitter singleton (listEmitter.ts) + owner/shares access-scope query (listAccess.ts) with D-04 isolation proven by RED/GREEN TDD gate. + +## TDD Gate Compliance + +| Gate | Commit | Status | +|------|--------|--------| +| RED — failing tests | 2d250af | PASS — module-not-found; 6 tests failed as expected | +| GREEN — implementation | 792efeb | PASS — all 9 tests pass | +| REFACTOR | (skipped) | No refactoring needed — implementation was clean on first pass | + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| RED | Write failing listEmitter + listAccess tests | 2d250af | listEmitter.test.ts (stubs → assertions), listAccess.test.ts (new) | +| GREEN | Implement listEmitter.ts + listAccess.ts | 792efeb | listEmitter.ts, listAccess.ts, listAccess.test.ts (UUID fix), vitest.config.ts | +| FIX | fileParallelism:false to eliminate DB race condition | 9e17853 | vitest.config.ts | + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Test seed helper oidc_sub collisions across runs** +- **Found during:** GREEN phase — running both test files together +- **Issue:** `seedUser('owner-5')` inserted `sub-owner-5` on first run; on the second run (or when running tests without cleanup of the users table), the `uniq_oidc_identity` key fired `ER_DUP_ENTRY`. +- **Fix:** Added `randomUUID()` suffix: `oidcSub: sub-${label}-${randomUUID()}` — unique per invocation regardless of table state. +- **Files modified:** `apps/api/tests/lib/listAccess.test.ts` +- **Commit:** 792efeb + +**2. [Rule 1 - Bug] Concurrent test files race against shared-MariaDB global afterEach** +- **Found during:** GREEN phase — running both test files together (and during full suite run) +- **Issue:** vitest defaults to `fileParallelism: true`. The global `afterEach` in `test/setup.ts` runs in every worker and truncates `lists`/`listShares`. When two DB-backed test files ran concurrently, file A's `afterEach` deleted rows that file B's test was still reading — producing FK violations (`ER_NO_REFERENCED_ROW_2`) and incorrect empty results. +- **Fix:** Added `fileParallelism: false` to `vitest.config.ts`, serializing test file execution. +- **Files modified:** `apps/api/vitest.config.ts` +- **Commit:** 9e17853 + +## Verification Results + +### TDD Tests +- listEmitter suite: 5 passed (Tests 1-4 + D-18 scale check) +- listAccess suite: 4 passed (Tests 5-8) +- **Test 2 (D-04 cross-list negative):** GREEN — handler subscribed to list 1 received 0 events when list 2 published +- **Test 7 (D-04 private-list negative):** GREEN — `getAccessibleListIds(otherUser)` did not return a list owned exclusively by another user + +### Full API Suite +- 15 test files passed | 3 skipped (Wave-0 stubs, expected) | 117 passed | 38 todo +- No regressions from prior plans + +### TypeScript +- `pnpm --filter @familysync/api exec tsc --noEmit` — PASS + +### ioredis Check +- `grep -r "ioredis" apps/api/` — not present (D-18 confirmed) + +## Known Stubs + +None. Both modules are fully implemented and tested. + +## Threat Surface Scan + +| Flag | File | Description | +|------|------|-------------| +| T-04-02 (mitigated) | apps/api/src/lib/listEmitter.ts | Fan-out channel keyed by listId; cross-list isolation proven by Test 2 | +| T-04-03 (mitigated) | apps/api/src/lib/listAccess.ts | Access-scope query restricted to owner_id OR list_shares; over-return proven impossible by Test 7 | +| T-04-04 (accepted) | apps/api/src/lib/listEmitter.ts | setMaxListeners(200) headroom applied; DoS risk accepted for household scale | + +## Self-Check: PASSED + +- `apps/api/src/lib/listEmitter.ts` — FOUND +- `apps/api/src/lib/listAccess.ts` — FOUND +- `apps/api/tests/lib/listEmitter.test.ts` — FOUND (stubs replaced) +- `apps/api/tests/lib/listAccess.test.ts` — FOUND +- `apps/api/vitest.config.ts` — FOUND (fileParallelism:false) +- Commit 2d250af (RED) — FOUND +- Commit 792efeb (GREEN) — FOUND +- Commit 9e17853 (fix) — FOUND