Commit Graph
140 Commits
Author SHA1 Message Date
Lucas Berger 03e953158a fix(13-02): eliminate all ESLint violations — pnpm lint exits 0
- eslint.config.js: disable React Compiler rules (v7 flat.recommended enables
  them; codebase does not use the Compiler); add e2e/ to disableTypeChecked
  block; promote exhaustive-deps to error
- API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler,
  expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument
  disables with justifying comments inside try blocks
- API routes/sse.ts: fix no-misused-promises on async writeSSE callback with
  void+IIFE+catch pattern
- API routes/lists.ts: let → const for updateValues
- API tests: remove unused imports (beforeEach, eq, vi); rename unused vars
  with _ prefix; remove unused lastActiveId assignment
- PWA components: void navigate() and void queryClient.invalidateQueries() on
  all fire-and-forget call sites; fix CalendarShell explicit-type-casts;
  Couldn't → HTML entity
- PWA test files: as unknown as Response for partial mock objects; string | null
  type annotation on mockLastSyncedUid; remove async from test callbacks without
  await; act(() => {}) not await act(async () => {}) for sync ops
- sw.ts: restructure Notification.data?.url access as let+if so disable
  comments land on the exact violation lines; void self.skipWaiting()
2026-06-11 20:23:38 -04:00
Lucas Berger df62d333d0 chore(13-01): install ESLint+Prettier, author flat config, wire lint/format scripts
- Install eslint@9.39.4 @eslint/js@9.39.4 typescript-eslint@8.61.0
  eslint-plugin-react@7.37.5 eslint-plugin-react-hooks@7.1.1
  eslint-config-prettier@10.1.8 prettier@3.8.4 as root devDependencies
- Author eslint.config.js: ignores → base recommendedTypeChecked (projectService:true)
  → pwa-react block (apps/pwa/** only) → disableTypeChecked override (config files +
  apps/api/tests) → prettier-last (eslint-config-prettier/flat)
- Add .prettierrc (singleQuote:true, printWidth:100) and .prettierignore
- Add "type":"module" to root package.json; add format/format:check scripts
- Add lint script (--max-warnings 0) to apps/api and apps/pwa
2026-06-11 19:44:57 -04:00
Lucas Berger c0f892cae5 fix(db): squash migrations to single baseline (cold-migrate was broken)
0000_easy_slipstream already created lists/list_shares/list_items and the
calendars unique constraint, but 0001_lists_schema re-created those tables and
0001_calendars_user_url_unique was an orphan (not in _journal) — so a cold
`drizzle-kit migrate` against an empty DB failed with ERROR 1050 'Table lists
already exists'. Dev only survived because its DB was built incrementally; CI
is the first cold migrate and exposed it.

Regenerated a single 0000_baseline.sql from schema.ts. Verified on a fresh
mariadb:11: migrate succeeds, schema is structurally identical to the running
dev DB, `drizzle-kit generate` reports no drift, and all 238 API tests pass.
Local dev DBs must be rebuilt (drop + db:migrate); no prod exists.
2026-06-11 14:28:08 -04:00
Lucas Berger a9c3304c4e docs: generate project documentation 2026-06-10 18:17:51 -04:00
Lucas Berger 7ac4c29ea9 fix(06): IN-06 fold long DESCRIPTION line per RFC 5545 in weekly-count3 fixture 2026-06-10 16:56:06 -04:00
Lucas Berger 0511a23886 fix(06): WR-06 bound post-write targeted resync with 10s timeout so a hang cannot wedge drain 2026-06-10 16:53:13 -04:00
Lucas Berger eb00ec7dfb fix(06): WR-01 gate all-day SQL branch on hasRrule=0 to stop duplicate occurrences 2026-06-10 16:51:19 -04:00
Lucas Berger d101aa899d fix(06): CR-01 validate recurrenceUntil as YYYY-MM-DD to close RRULE injection 2026-06-10 16:51:06 -04:00
Lucas Berger 8343faddce feat(260610-k1z-01): wire persistSessionCookie into index.ts + add unit tests
- Mount persistSessionCookie() immediately after oidcAuthMiddleware() inside !devBypassActive block
- Test A: truthy oidcAuthJwt produces Set-Cookie with Max-Age, SameSite=Lax, HttpOnly, Secure
- Test B: falsy/absent oidcAuthJwt emits no oidc-auth cookie (no-resurrection guard)
2026-06-10 14:32:42 -04:00
Lucas Berger aabcb5d043 feat(260610-k1z-01): add persistSessionCookie() middleware (AUTH-02)
- Re-issues oidc-auth cookie with maxAge so PWA sessions survive close/reopen
- Guards on c.get('oidcAuthJwt'): only runs when @hono/oidc-auth set a valid session
- Falsy oidcAuthJwt falls straight through — no resurrection of deleted/absent cookies
- Cookie attrs mirror the library: httpOnly, secure, sameSite=Lax, conditional domain
- maxAge reads OIDC_AUTH_EXPIRES (default 86400s)
2026-06-10 14:30:59 -04:00
Lucas Berger d9efbc1060 fix(quick-260610-i4x-01): replace node-cron schedule() with setInterval in three broker workers
- poller.ts: setInterval(cb, 5 * 60 * 1000) replaces schedule('*/5 * * * *', cb)
- outboxWorker.ts: setInterval(cb, 15 * 1000) replaces schedule('*/15 * * * * *', cb)
- reminderScheduler.ts: setInterval(cb, 60 * 1000) replaces schedule('* * * * *', cb)
- Remove 'import { schedule } from node-cron' from all three files
- Update doc comments to reflect setInterval and document why (node-cron 4.2.1 silent skip)
- Callback bodies and .catch wrappers unchanged; typecheck clean; 91/91 broker tests pass
2026-06-10 13:06:56 -04:00
Lucas Berger 19d92c671b test(260610-hbu): drop unused 'now' param in rowForNow helper 2026-06-10 12:38:37 -04:00
Lucas Berger 93bb2c1c68 test(260610-hbu-02): update reminderScheduler tests for catch-up + per-uid dedup
- Add SINGLE-FIRE: 3 consecutive ticks, exactly 1 dispatch total
- Add MISSED-TICK-RECOVERY: fires at 8-min lead when ideal 15-min tick skipped
- Add ALREADY-STARTED: dtstart<=now returns no rows, 0 dispatches
- Add CR-01 pruning: started-event entry pruned after dtstart passes
- Add D-16: empty subscriptions, zero sends, no crash
- Add T-05-19: per-sub error isolation, both subs attempted when first throws
- Add fan-out: 2 subs -> 2 dispatches for one event
- Rewrite WR-01 test to per-uid dedup language; remove minuteBucket tests
- Update file docblock for catch-up (now, now+16min] window and per-uid dedup
2026-06-10 12:36:23 -04:00
Lucas Berger 3fdb242f7e fix(260610-hbu-01): catch-up window + per-uid dedup in reminderScheduler
- Replace [now+14min, now+16min] window with (now, now+16min] catch-up
- Replace minuteBucket-keyed Set with uid-keyed Map for exactly-once dedup
- Lead-accurate body: 'Starts in N min' (Math.max(1, round(lead/60000)))
- CR-01 pruning: drop entries whose dtstart <= now (event started)
- WR-01 preserved: mark uid sent after all dispatches complete
- Drop gte import; add gt import from drizzle-orm
2026-06-10 12:32:43 -04:00
Lucas Berger 44d336c01b feat(06-03): expose hasRrule on expanded occurrences
- Add hasRrule: boolean to CalendarOccurrence interface
- Capture isRecurring = event.isRecurring() once before the branch
- Set hasRrule: isRecurring in non-recurring push (always false)
- Set hasRrule: isRecurring in recurring push (always true)
- All 10 expand.test.ts tests pass (RED→GREEN)
2026-06-10 11:17:13 -04:00
Lucas Berger 593302ee41 test(06-03): add failing tests for hasRrule + bounded expansion
- Add hasRrule===true assertion for recurring events (weekly-dst.ics)
- Add hasRrule===false assertion for non-recurring events (single-duration.ics)
- Add weekly-count3.ics fixture (FREQ=WEEKLY;COUNT=3, 1-hour events)
- Add bounded RRULE test: expects exactly 3 occurrences in wide window
- Add per-occurrence duration test: each occurrence is 1 hour (not recurrence span)
- Tests are RED: hasRrule field absent from CalendarOccurrence interface
2026-06-10 11:11:08 -04:00
Lucas Berger d2abb91bd2 feat(06-02): serialize RRULE UNTIL/COUNT and lock FREQ persistence
- Add assembleRruleString() helper (exported) to outboxWorker.ts (D-06)
- Wire UNTIL/COUNT bound assembly into create + update dispatch branches
- Add recurrenceUntil (max 10) + recurrenceCount (int min 1) to outboxPayloadSchema
- Add recurrenceUntil + recurrenceCount to eventFieldsSchema in events.ts
- Series-edit bound change strips existing UNTIL/COUNT via regex before re-apply (Pitfall 3)
- All 39 broker tests pass (RED->GREEN); existing CR-01 none-wins test preserved
2026-06-10 11:00:24 -04:00
Lucas Berger a59455a727 test(06-02): add failing tests for RRULE UNTIL/COUNT + FREQ persistence
- vevent.test.ts: add COUNT, UNTIL-DATE, UNTIL-DATETIME serialization assertions
- outboxWorker.test.ts: add assembleRruleString (D-06) describe block (not yet exported)
- outboxWorker.test.ts: add FREQ persistence (D-07 regression) describe block
- RED: assembleRruleString not yet exported; FREQ-persistence cases fail on missing helper
2026-06-10 10:56:22 -04:00
Lucas Berger f452400517 chore(quick-260610-cr8-01): remove db:push script from apps/api/package.json
- Delete the 'db:push': 'drizzle-kit push' entry from scripts
- Retain db:generate and db:migrate as the canonical workflow
- JSON remains valid; no other changes
2026-06-10 09:15:30 -04:00
Lucas Berger 17756fc523 fix(05-review): NEW-WR-01 emit delete changes when server returns zero events (whole-cache clear)
Pre-capture all currently-cached rows into pendingDeleteRows before the whole-cache
db.delete() when seenUids.length === 0. The existing >0 branch behavior is unchanged.
Adds a regression test verifying onChanges receives one delete change per cached row
on a full-calendar clear.
2026-06-09 22:39:11 -04:00
Lucas Berger 44fbb2bb3a fix(05-review): commit 0004 migration meta (journal + snapshot) 2026-06-09 22:33:09 -04:00
Lucas Berger 1044de57ae test(05-review): add CR-01/WR-01 reminder pruning tests and IN-01 actor-name tests 2026-06-09 22:30:03 -04:00
Lucas Berger 50da9b3bca fix(05-review): IN-01 resolve actor display name in eventChangeDispatcher for D-02/D-03 2026-06-09 22:25:32 -04:00
Lucas Berger 8cecbab7ab fix(05-review): CR-02 change endpoint/p256dh to varchar to prevent InnoDB prefix-index truncation 2026-06-09 22:24:23 -04:00
Lucas Berger 7702f7e19a fix(05-review): CR-01+WR-01 prune sentReminders after each scan and mark-sent after dispatch 2026-06-09 22:22:04 -04:00
Lucas Berger f058aefb88 fix(05-review): WR-04 collect delete changes after db.delete to avoid race in sync fan-out 2026-06-09 22:21:37 -04:00
Lucas Berger c69bd30aaa fix(05-review): WR-03 log error.message not raw error in push route catch blocks 2026-06-09 22:20:43 -04:00
Lucas Berger b666b1d114 fix(05-review): WR-02 remove unused 'and' import from listChangeDispatcher 2026-06-09 22:20:30 -04:00
Lucas Berger 30e9de13f9 feat(05-07): implement eventChangeDispatcher + syncCalendar diff/title/onChanges
- Create eventChangeDispatcher.ts: dispatchEventChange + isMeaningfulChange
- D-04: description-only edits are silent; meaningful fields = title/dtstartUtc/dtstartDate/allDay/location
- D-03: actor excluded via ne() + application-level filter; all subs filtered by userId != actorUserId
- D-13: reads only push_subscriptions from MariaDB — no tsdav/Fastmail I/O
- syncCalendar: add optional onChanges callback; populate title from VEVENT SUMMARY on every upsert
- syncCalendar: pre-upsert SELECT to detect add vs update; track changedFields; prune emits deletes
- poller: pass onChanges with actor=cred.userId (external changes from other member)
- outboxWorker.triggerTargetedResync: pass onChanges with actor=userId (this-member writes)
- All 4 eventChangeDispatcher tests + 14 sync tests GREEN
2026-06-09 22:03:52 -04:00
Lucas Berger b95f671485 feat(05-06): implement reminderScheduler — shared timed 15-min reminder scan
- Create apps/api/src/broker/reminderScheduler.ts:
  - runReminderCheck(now): queries isShared=true + allDay=false events
    with dtstartUtc in [now+14min, now+16min] via calendarEvents→calendars→
    pushSubscriptions cross-join (2 innerJoins; fans out to all subscribers)
  - In-memory sentReminders Set keyed uid:minuteBucket prevents double-fire
    at window boundary (D-06, T-05-18)
  - Per-event and per-subscription try/catch for error isolation (T-05-19)
  - Null title fallback (event.title ?? uid) — handles rows before Plan 05-07
  - Empty shared-calendar set produces zero sends and no crash (D-16)
  - startReminderScheduler(): node-cron 1-min schedule wrapping runReminderCheck
- Wire startReminderScheduler() into index.ts isMainModule() guard after
  startOutboxWorker() and VAPID setVapidDetails (NOTIF-01)
- Tests: 3/3 GREEN (all-day excluded, non-shared excluded, dedup)
2026-06-09 21:38:32 -04:00
Lucas Berger d2ce4e08c7 feat(05-05): hook notifyListChange into list/item mutations (reorder excluded)
- POST /:id/items (item added) → notifyListChange
- PATCH /list-items/:itemId checked/text → notifyListChange; position-only → silent (D-01)
- DELETE /list-items/:itemId → notifyListChange
- PATCH /:id (list rename/share toggle) → notifyListChange
- DELETE /:id (list delete) → notifyListChange
- POST / (list create) → no notification (empty list, D-01 spirit)
- lists.test.ts: 2 new tests prove reorder-silent (position) and check-notifies (NOTIF-02)
- All 59 lists.test.ts assertions GREEN
2026-06-09 21:26:29 -04:00
Lucas Berger 69231043e4 feat(05-05): implement listChangeDispatcher — access-scoped, self-suppressed, coalesced push (NOTIF-02)
- notifyListChange(listId, actorId, windowMs?) wraps coalesceListPush with a
  dispatch closure that resolves actor name + list name from DB, builds
  audience as owner ∪ list_shares MINUS actorId (D-03), and calls
  dispatchPush per accessible subscriber subscription
- D-02 generic copy: '{Actor} made {N} changes to {ListName}' — no item text
- D-03 self-suppression: actorId filtered from audience before subscription load
- T-05-14: audience strictly scoped to list access (owner + list_shares only)
- T-05-15: no item text in notification body
- Empty audience and missing subscriptions are silent no-ops
- Tests: 5/5 GREEN (burst→1 push, self-suppress, access scope, empty audience)
2026-06-09 21:24:48 -04:00
Lucas Berger 97f7026095 test(05-05): add failing tests for listChangeDispatcher — RED gate
- burst coalescing: N calls → 1 dispatchPush to non-actor with count=N
- D-03 self-suppression: actor's own subscription never dispatched
- T-05-14 access scoping: unrelated user (no owner/share) excluded
- empty audience (no other members): no dispatch, no crash
- empty audience (other member has no subscription): no dispatch, no crash
2026-06-09 21:20:34 -04:00
Lucas Berger d816f79271 fix(05-04): wrap setVapidDetails in try/catch to prevent startup crash on bad VAPID key
- Bad/truncated VAPID_PRIVATE_KEY logs a warning instead of crashing the server
- API still starts and serves all other routes; push dispatch fails with a log message
2026-06-09 21:13:58 -04:00
Lucas Berger f6f1374904 feat(05-04): push subscription API + VAPID startup wiring
- Create apps/api/src/routes/push.ts: GET /vapid-public-key, POST /subscription (upsert), DELETE /subscription (user-scoped)
- Wire pushRouter at /api/push in index.ts
- Call webpush.setVapidDetails() in isMainModule() guard before serve()
- Fix broken vi.getMockImplementation scaffold bug in push.test.ts (Rule 1)
- push.test.ts: all 4 tests GREEN
2026-06-09 21:04:16 -04:00
Lucas Berger c1758de05e feat(05-03): implement pushCoalescer — per-(list,actor) sliding debounce (D-01/D-03)
- module-level Map<string, {count, timer}> keyed by ${listId}:${actorId}
- sliding window: each call within window resets timer and increments count
- fires dispatch(listId, actorId, count) once on timer expiry; map entry self-deletes
- actorId passed as second arg so caller can apply excludeUserId=actorId (D-03)
- default windowMs=45000; injected dispatch keeps module pure and testable
2026-06-09 20:58:42 -04:00
Lucas Berger 7af827a9b9 test(05-03): add actorId assertion in burst test — fix unused var lint warning 2026-06-09 20:58:01 -04:00
Lucas Berger e4170b3823 feat(05-02): implement pushDispatcher — VAPID send + 410/404 prune
- buildPushBody: dual-format payload (iOS 18.4+ declarative + legacy)
- dispatchPush: calls webpush.sendNotification with TTL=300, urgency=normal
- Prunes push_subscriptions row on 410/404 from push service (D-11)
- Logs transient errors with [pushDispatcher] prefix; never throws to caller
- Default import for web-push (CommonJS — Pitfall 7)
2026-06-09 20:54:47 -04:00
Lucas Berger ef558b65be test(05-01): add Wave-0 RED scaffolds + VAPID fixture + setup truncation
- tests/fixtures/vapid.ts: static TEST_VAPID keypair for offline unit tests
- tests/lib/pushDispatcher.test.ts: RED — 410/404 prune + 201/5xx no-delete
- tests/lib/pushCoalescer.test.ts: RED — burst coalesce fires once with count=N; excludeUserId
- tests/broker/reminderScheduler.test.ts: RED — shared+timed filter; dedup by (uid,minuteBucket)
- tests/lib/eventChangeDispatcher.test.ts: RED — create/meaningful-update fires; description-only silent; actor excluded
- tests/routes/push.test.ts: RED — POST 201/401; DELETE removes rows; GET vapid-public-key
- test/setup.ts: import pushSubscriptions + add db.delete(pushSubscriptions) in afterEach
- all 5 RED files fail on missing-module (correct; implementations in Plans 05-02..05-06)
2026-06-09 20:50:35 -04:00
Lucas Berger 73fcdaf075 feat(05-01): add push_subscriptions table + calendar_events.title column; VAPID env wiring
- schema.ts: new pushSubscriptions mysqlTable (user_id FK cascade, endpoint unique, p256dh, auth)
- schema.ts: add nullable title varchar(500) to calendarEvents after rawVevent (D-02/NOTIF-01)
- 0003_same_xavin.sql: CREATE TABLE push_subscriptions + ALTER calendar_events ADD title
- migration applied to dev DB via db:generate + db:migrate (NOT db:push per anti-pattern)
- docker-compose.yml: inject VAPID_PUBLIC_KEY/PRIVATE_KEY/SUBJECT into api environment block
- .env.example: document all three VAPID vars with placeholders + generation instructions
2026-06-09 20:47:44 -04:00
Lucas Berger 80bbdc1735 chore(05-01): install web-push and workbox push dependencies
- apps/api: add web-push@^3.6.7 (prod) + @types/web-push@^3.6.4 (dev)
- apps/pwa: add workbox-core, workbox-precaching, workbox-routing@^7.4.1 (dev)
- pnpm-lock.yaml updated for all three workspace packages
2026-06-09 20:47:35 -04:00
Lucas Berger ae115c65ef chore(04-07): commit drizzle migration metadata for 0002 collation migration
The 0002_yielding_mattie_franklin.sql migration was committed but its
drizzle-kit journal entry and snapshot were left untracked. Without these,
drizzle-kit cannot track the migration as applied.
2026-06-09 14:26:52 -04:00
Lucas Berger c0bd6d732d fix(04-07): add owner-only guard for isShared on PATCH /api/lists/:id (T-04-08)
- Immediately after access check, return 403 if patch.isShared !== undefined
  and !access.isOwner — blocks sharees from mutating list_shares
- Guard message: 'Only the list owner can change sharing settings'
- Sharees may still PATCH { name } (rename test stays green)
- Update stale comment: 'Reconcile list_shares on visibility change (owner only)'
- Closes T-04-08 (elevation of privilege) and T-04-05 (shared root cause)
2026-06-09 14:22:48 -04:00
Lucas Berger 931f767922 test(04-07): add failing sharee-403 tests for T-04-08 owner-only isShared guard
- T-04-08 test 1: sharee PATCH { isShared: false } must get 403 and
  list_shares row unchanged (currently 200 + shares wiped — bug)
- T-04-08 test 2: sharee PATCH { isShared: true } must get 403 and
  no new shares inserted (currently 200 + shares fan-out — bug)
- Both tests fail now; GREEN once owner-only guard added to lists.ts
2026-06-09 14:22:21 -04:00
Lucas Berger 9b860617c5 feat(04-07): migrate list_items.rank to COLLATE utf8mb4_bin (LIST-03)
- Add varcharBin customType helper emitting varchar(255) COLLATE utf8mb4_bin
- Replace listItems.rank varchar with varcharBin to carry explicit binary collation
- Generate migration 0002_yielding_mattie_franklin.sql: ALTER TABLE list_items
  MODIFY COLUMN rank varchar(255) COLLATE utf8mb4_bin NOT NULL (additive, no DROP)
- Apply migration via db:migrate (never db:push, per project memory constraint)
- utf8mb4_bin ensures uppercase-prefixed ranks (e.g. Zz) sort before lowercase
  ranks (e.g. a0) in DB ORDER BY, matching JS string order — closes LIST-03 gap
2026-06-09 14:21:42 -04:00
Lucas Berger ece663d1df test(04-07): add failing collation regression test (LIST-03)
- Seed items with ranks 'a0' and 'a1', drag second to top via rank 'Zz'
- Assert 'Zz' < 'a0' is true in JS (documents uppercase-before-lowercase intent)
- GET /api/lists/:id/items must return Zz-ranked item at index 0
- Fails now because MariaDB utf8mb4_uca1400_ai_ci sorts 'Zz' after 'a0'
- Will pass once rank column gets COLLATE utf8mb4_bin via migration
2026-06-09 14:19:27 -04:00
Lucas Berger 690f0b95c0 refactor(04): move rank.test.ts into tests/ mirror dir (convention + dist hygiene)
src/lib/rank.test.ts was the last co-located API test. The API tsconfig excludes
tests/ from the build, so all test files belong there; a test in src/ gets compiled
into dist/ and vitest then runs the stale compiled copy (the source of phantom
'22 todo' and dist sourcemap warnings). Moving it to tests/lib/ matches convention
and leaves zero test files in src/, so the production build no longer emits test
artifacts. Updated rank.test.ts path refs in phase-04 plan docs.
2026-06-09 13:40:11 -04:00
Lucas Berger 1652a68c51 feat(04-06): implement live-sync SSE vertical slice (LIST-04, D-04/D-10/D-11/D-12)
- Wire publishListEvent fan-out in lists.ts after every write mutation (item:added/updated/deleted, list:updated/deleted)
- Add GET /api/sse/lists scoped endpoint in sse.ts: resolveUserId → 401 on null; getAccessibleListIds → subscribe only to accessible channels; 30s heartbeat; cleanup on disconnect (D-04/T-04-01/T-04-02)
- Create useListSSE.ts: bounded-backoff EventSource wrapper (250ms→500ms→1s→2s→4s→cap 8s); MAX_ATTEMPTS=6; withCredentials:true; close-before-retry prevents reconnect storm (Pitfall 3); invalidates ['list', listId] on open (D-10) and on each event; onStateChange('disconnected') after exhaustion (D-11)
- Create LiveSyncIndicator.tsx: connected=green dot; reconnecting=pulsing muted dot + label; disconnected=red dot + 'Updates paused' (role=alert); correct ARIA per UI-SPEC
- Wire useListSSE + LiveSyncIndicator into ListDetail header; retain refetchInterval:30000 polling fallback (D-12)
- All 8 useListSSE tests pass; all 54 API tests pass; both typechecks pass
- playwright-cli: live update confirmed (eggs item added via API appeared in browser without manual refresh)
2026-06-09 13:33:04 -04:00
Lucas Berger 5a8d1efe1c test(04-06): add failing RED tests for LIST-04 SSE fan-out + bounded backoff
- API: 5 failing fan-out spy tests (subscribeListEvents receives 0 events since publishListEvent seams commented out in lists.ts)
- API: 4 D-04 scoped subscription tests (green — listAccess primitives from 04-02 already proven)
- PWA: useListSSE.test.ts — all 7 tests fail (module-not-found; hook not yet created)
- Covers: item:added/updated/deleted, list:updated/deleted fan-out + D-11 bounded backoff exhaustion + D-10 reconnect invalidation
2026-06-09 13:25:39 -04:00
Lucas Berger ef4b1157b3 test(04-05): server-side reorder ordering + rank precision tests (LIST-03, D-13)
- rank.test.ts: 100-iteration zipper mid-point insert precision test (Pitfall 2);
  rank-between-neighbors contract test; total 10 tests (was 8)
- lists.test.ts: 5 new LIST-03 ordering tests — PATCH position updates only rank
  and GET returns new ASC order; one-row write asserts other items unchanged;
  LWW (D-15): second PATCH overwrites first; T-04-07 two-field position PATCH → 400
- Note: tests use a0–a5 rank range (avoids uppercase ranks that sort differently
  under MariaDB utf8mb4_unicode_ci vs JS lexicographic order)
2026-06-09 13:17:35 -04:00