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: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ 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.
**AI schema regression test infrastructure — fixture loader + replay harness:** post-Epic-15.5 ships the regression-test layer for `ai-responses.ts` Zod schemas. Pre-15-5 the existing `ai-responses.test.ts` covered schemas at the unit level (synthetic-input boundary tests); real-shape regressions (a prompt change shifting model output, a new field the model started emitting) had no CI gate. Story 15-5 lands **INFRASTRUCTURE-ONLY scope** — fixture loader + replay harness + 3 synthetic seed fixtures + operator runbook for real-fixture capture (deferred to operator action because (a) capturing 10 real outputs per prompt requires Sentry breadcrumb pulls / live OpenAI calls that burn cost-cap budget per Story 11-4 and (b) is inappropriate for an autopilot story). NEW [`src/lib/schemas/__fixtures__/`](src/lib/schemas/__fixtures__/) directory tree with subdirs `writing-evaluation/`, `dictation/`, `mock-test-section/` (matches the spec's "writing eval, mock test, dictation" example list) + 1 synthetic seed fixture per dir. NEW [`src/lib/schemas/__tests__/fixture-replay.test.ts`](src/lib/schemas/__tests__/fixture-replay.test.ts) (8 cases) walks the directory tree at suite setup, maps each schema-dir to its Zod parser via explicit `FIXTURE_SCHEMA_MAP` constant (catches schema rename via TypeScript import; auto-derive would silently fail), strips top-level `_synthetic` + `_note` metadata fields before parse (operator-readable bookkeeping markers not part of the schema contract), and uses `it.each` over discovered files so adding a new fixture is a **pure data add** (no code change needed). Infrastructure cases: `FIXTURE_SCHEMA_MAP` entries pin; no orphan dirs; ≥1 fixture per schema; `stripMetadata` strips top-level + preserves nested + is no-op on non-objects. Per-fixture cases: 3 synthetic seed fixtures parse successfully against their schemas (writing-evaluation B1 formal essay with 13-field shape incl. all 5 score bands + 1 grammar error + connectors used/missing + tcfEstimatedScore + summary; dictation A2 mixed 3-sentence set with easy/medium/hard difficulty enum; mock-test B1 listening 1-passage 2-question shape with 4-options/1-correct/unique-ids per `mcqQuestionSchema` Story 9-7 R1 P9/P10 invariant). NEW operator runbook [`_bmad-output/planning-artifacts/runbooks/ai-fixture-capture.md`](_bmad-output/planning-artifacts/runbooks/ai-fixture-capture.md) documents WHEN to capture (post-prompt-change / post-model-upgrade / post-schema-change), HOW (3 options — Sentry breadcrumb pull preferred; dev-mode temporary console.log; live OpenAI call last resort), naming convention (`<descriptor>-NNN.json`), WHERE (the new directory tree), HOW to verify (`npx jest fixture-replay` shows new test case), and the synthetic-vs-real bookkeeping convention. **3 operator decisions resolved per Recommended:** Q1 explicit `FIXTURE_SCHEMA_MAP` (vs auto-derive from dir name — explicit is TypeScript-checked + survives schema renames); Q2 `_synthetic: true` marker on seed fixtures (future tooling can filter to real-only when assessing regression confidence); Q3 start with 3 schemas matching the spec's example list (defer expansion to all 36 schemas to `15-5-followup-fixture-coverage-expansion`). **Cross-story invariants preserved:** `ai-responses.ts` schemas unchanged; existing `ai-responses.test.ts` unchanged; 0 source-module modifications beyond the test infrastructure + fixtures + runbook. **+8 net Jest cases** (3 fixture-replay cases via `it.each` + 5 infrastructure pins = 8 total per the new test file; pre-15-5 baseline 2159 → 2167). All 5 design-system gates green. Verified 2026-05-17, story 15-5.

### Routing (`app/`)

Expand Down
Loading
Loading