Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ A reusable pattern for capping in-memory data structures + UI work-budgets. Two

**Lib unit tests — `srs.ts` SM-2 algorithm + `pronunciation.ts` `identifyWeakSounds` + `cache.ts` core API + `use-dictation.ts` `compareSentences`:** post-Epic-15.1, the FIRST Epic 15 (Test Coverage & QA Infrastructure) story closes the largest pure-function-test gap surfaced by the Story 12-10 audit refresh footnote. Per the coverage inventory at story-creation time, `activity.ts` was already fully tested (Story 9-2 + 12-3); `memory.ts` + `error-tracker.ts` had substantial partial coverage (sanitize via Story 9-4 prompt-injection.test.ts + dedup via Story 11-6 error-tracker-dedupe.test.ts + e2e clustering via 11-6); `pronunciation.ts` had history-cap coverage (Story 12-12) but NOT `identifyWeakSounds`. Story 15-1 lands **GAP-only scope** across **4 pure-function surfaces** (no source-module modifications — test-only): **(1) NEW [`src/lib/__tests__/srs.test.ts`](src/lib/__tests__/srs.test.ts)** (14 cases) pins the full SM-2 algorithm contract — 3 incorrect-response cases (quality 0/1/2 ease-factor deltas −0.80/−0.54/−0.32 + reset to `repetitions=0`, `intervalDays=1`); 3 correct-response cases (quality 3/4/5 ease-factor deltas −0.14/0/+0.10 + interval progression 1 → 6 → round(prev × ef)); 3 ease-factor 1.3-floor clamping cases (boundary + already-at-floor + just-above-floor); 3-consecutive-correct chain verification; 365-day interval cap; midnight-snap `nextReview` math via Date.now() before/after delta check; no-mutation invariant via input-snapshot equality + determinism via twice-call equivalence. **(2) NEW [`src/lib/__tests__/pronunciation.test.ts`](src/lib/__tests__/pronunciation.test.ts)** (10 cases) pins `identifyWeakSounds` contract — empty input + no-weak happy path + below-threshold-but-count<3 NOT flagged + single-phoneme-aggregated-across-results + same-phoneme-aggregated-across-words-in-one-result + threshold boundaries (`avgScore < 70` strict + `count >= 3` boundary) + multi-phoneme ranking ASCENDING by avgScore (worst first) + no-mutation deep-equal. **(3) NEW [`src/lib/__tests__/cache.test.ts`](src/lib/__tests__/cache.test.ts)** (20 cases) covers the AsyncStorage path (existing `cache-flush.test.ts` covers write-queue idempotency; existing `cache-secure-routing.test.ts` covers the Story 12-7 SecureStore fork) — `getCache` empty/fresh/TTL-fresh-boundary/TTL-expired-boundary/corrupted-JSON/throw paths each with `captureError` routing pinned; `setCache` envelope shape `{data, timestamp, ttlMs}` + DEFAULT_TTL_MS (1 hour) fallback + namespacing `@companion_cache:<userId>:<key>` (different userIds never collide); `setCache` → `getCache` round-trip with nested object structure; `invalidateCache` happy + error paths; `cacheWithFallback` 4-path matrix (fetcher-succeeds-writes-cache, fetcher-throws-stale-cache-hit-returns-fromCache:true, fetcher-throws-no-cache-rethrows-original, fetcher-throws-cache-also-throws-captures-fallback-read-error-rethrows-original); constant pins for `CACHE_KEYS` (5 keys) + `CACHE_TTL` (6 durations) + `SECURE_CACHE_KEYS` size===1 with PROFILE in / SKILLS+VOCABULARY out. **(4) NEW [`src/hooks/__tests__/use-dictation-compare.test.ts`](src/hooks/__tests__/use-dictation-compare.test.ts)** (15 cases) pins the pure word-by-word `compareSentences` helper from [`src/hooks/use-dictation.ts:88`](src/hooks/use-dictation.ts#L88) (despite living in a hook file, the function is pure — no React, no async, no side effects, so it fits 15-1 scope). Test mocks `@/src/hooks/use-audio-player` (transitive `expo-audio` native crash in Jest) + `@/src/lib/openai` (transitive supabase chain) at module load to keep the import boundary clean. Cases cover: perfect-match → 100% / one-wrong-word with `typed` populated → round(2/3 × 100)=67% / missing-word semantics (user shorter → past-user-length positions are "missing", overlap positions compare normally) / extra-word (user longer → extra tokens IGNORED; only original-length iterated) / empty-userInput → all "missing" / empty-original → empty wordResults + accuracy=0 + isFullyCorrect=true vacuously / both-empty same / case-insensitive ("Bonjour" vs "bonjour") with display preserving original capitalization / accent-insensitive ("café" vs "cafe") with display preserving accent / trailing punctuation stripped / internal comma stripped / multi-space collapses / leading+trailing whitespace trimmed / apostrophe stripped ("l'eau" matches "leau") / fresh wordResults array per call (no shared module-level cache). **3 operator decisions resolved per Recommended** (Q1 defer `assessPronunciation` Edge wrapper to 15-1-followup; Q2 defer `memory.ts` async/DB-touching paths to 15-2; Q3 defer `error-tracker.ts` non-dedup paths to 15-2) — keeps 15-1 pure-function-only per the Epic 14 retro lesson that test-writing stories are at scope-drift risk. **Explicitly out of scope** (filed for follow-up if motivated): `memory.ts` `extractFacts`/`persistMemories`/`retrieveMemories`; `error-tracker.ts` `getTopErrors`/`extractErrorsFromCorrections`/`persistErrorPatterns`/`getRecentResolvedError`; `pronunciation.ts` `assessPronunciation` Edge Function wrapper; `cache.ts` `enqueueWrite`/`flushWriteQueue` (already in `cache-flush.test.ts`)/`clearUserCache`/`clearAllCache` multi-key sweeps; `use-dictation.ts` `analyzeErrorPatterns`. **Cross-story invariants preserved by construction:** zero source-module modifications (Story 15-1 is test-only); Story 9-3 Sentry allowlist zero-diff (cache tests verify EXISTING `cache-get`/`cache-set`/`cache-invalidate`/`cache-fallback-read` feature tags fire; no new tags); Story 12-7 SecureStore fork unchanged (cache.test.ts uses non-secure key `"skills"` to exercise AsyncStorage path); all Epic 14 invariants orthogonal. **+59 net Jest cases** (2099 → 2158; spec target +50-65 — squarely in range). 4 new test files; 0 modified source files. All 5 design-system gates green (type-check 0 errors / lint 0 warnings / prettier clean / check:tokens clean / jest 113 suites / 2158 tests). Verified 2026-05-16, story 15-1.

**Hook integration tests — `usePronunciation` mocked-API coverage:** post-Epic-15.2 closes the largest hook-test gap surfaced by the 15-2 coverage inventory. Pre-15-2 `usePronunciation` had zero direct test coverage; the other 3 hooks in the spec inventory (`use-auth`, `use-exercise`, `use-realtime-voice`) already had substantial shape/binding tests (Story 12-2 + 10-8 + 12-1 P8). Story 15-2 lands NEW [`src/hooks/__tests__/use-pronunciation.test.tsx`](src/hooks/__tests__/use-pronunciation.test.tsx) (13 cases via `react-test-renderer` + `act` + `HookHost` consumer pattern from Story 12-1 P8 / 12-9 / 14-X / 15-1 precedent). Mocks `use-audio-recorder` (stubbed recorder), `expo-file-system/legacy` (stubbed `readAsStringAsync`), `@/src/lib/pronunciation` (mock `assessPronunciation` only — passes through real `identifyWeakSounds` aggregator already pinned by Story 15-1 `pronunciation.test.ts`), `@/src/lib/sentry`, `@/src/lib/error-messages`. Cases cover the full state machine: initial-state pin; `startAssessment` clears prior result + delegates to `recorder.startRecording`; `finishAssessment` 4 paths (happy-with-state-update + recorder-returns-null + assess-throws + return-value-callable-contract); `assessFromUri` 2 paths (happy + error); `clearResult` resets result/error but PRESERVES history (Story 12-12 FIFO continuity invariant); `getWeakPhonemes` null + populated; history accumulation 3 sequential calls = length 3 + 51 sequential calls = capped at 50 per Story 12-12 `MAX_PRONUNCIATION_HISTORY` (oldest evicted via real `appendCappedHistory` integration); `isRecording` mirrors recorder. **3 operator decisions resolved per Recommended:** Q1 react-test-renderer continuity (vs RTL `renderHook`), Q2 defer use-auth/use-exercise/use-realtime-voice full-flow integration extensions to `15-2-followup-existing-hook-flow-integration` (Epic 14 14-4 R1-22-patch lesson — broad scope balloons), Q3 mock at `assessPronunciation` lib boundary (NOT `supabase.functions.invoke` one level deeper). **Cross-story invariants preserved:** Story 9-3 Sentry allowlist zero-diff (test verifies existing `pronunciation-assessment` feature tag fires); Story 12-12 `appendCappedHistory` + `MAX_PRONUNCIATION_HISTORY = 50` unchanged (verified via 51-call eviction); Story 15-1 `identifyWeakSounds` unchanged (test uses real helper via `jest.requireActual`). **+13 net Jest cases** (2159 → 2172; spec target +12-15 squarely in range). 1 new test file; 0 source-module modifications. All 5 design-system gates green. Verified 2026-05-17, story 15-2.

### Routing (`app/`)

Expo Router file-based routing with three route groups:
Expand Down
Loading
Loading