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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,48 @@ jobs:
- name: Tests
run: npm test -- --no-coverage

- name: Setup Deno
# Story 15-3: install Deno for Edge Function _shared utility tests.
# `denoland/setup-deno@v2` is the ACTION installer (second major
# release of the installer); `deno-version: v1.x` pins the Deno
# RUNTIME. Two independent version axes — don't conflate. Deno 2.0
# runtime migration is deferred to a follow-up story. The action
# caches the install for faster subsequent runs.
# SHA-pinning of this action (and actions/checkout / setup-node)
# deferred to `15-3-followup-action-sha-pinning` per R1 BH-1.
uses: denoland/setup-deno@v2
with:
deno-version: v1.x

- name: Deno tests (Edge Function _shared utilities)
# Story 15-3: wire Story 11-3 fetch-with-timeout + Story 12-11
# parse-upstream-error Deno tests into CI. Pre-15-3 these were
# manual-run only. Now gated on PR merge.
#
# Test files are named EXPLICITLY (R1 BH-2 / EH-1 vacuous-pass
# defense): if either file is renamed or moved, the step fails
# loudly with `Module not found` instead of silently passing on an
# empty directory match. A future file added to _shared/__tests__/
# must be appended here to be picked up.
#
# Permission flags:
# --allow-net=127.0.0.1 — required by the fetch-with-timeout
# happy-path test which spins up a local HTTP server.
# --no-check — runtime-only verification (R1 EH-10); the
# `npm run type-check` step already covers TS soundness for the
# source modules; Deno's redundant typecheck adds ~5s + a
# drift surface against std lib type updates.
# NO `--allow-all` — keep the permission surface minimal.
# pgTAP migrations (supabase/migrations/__tests__/*.sql) NOT wired
# here — requires a Postgres service container. Deferred to
# `15-3-followup-pgtap-ci-wiring`.
# Deno deps cache deferred to `15-3-followup-deno-cache` per R1 BH-4.
timeout-minutes: 3
run: |
deno test --no-check --allow-net=127.0.0.1 \
supabase/functions/_shared/__tests__/fetch-with-timeout_test.ts \
supabase/functions/_shared/__tests__/parse-upstream-error_test.ts

- name: Dependency vulnerability gate
# Story 12-10: prevent regression of `npm audit reports 0 high
# vulnerabilities` (Epic 12 AC). Calibrated to `--audit-level=high`
Expand Down
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.

**Golden-flow E2E Maestro skeleton flows + operator runbook:** post-Epic-15.4 ships the structural framework for end-to-end smoke testing. **Operator pre-decision at autopilot kickoff: Maestro** over Detox per the spec's recommended default (faster iteration, simpler YAML config, no Xcode/Android Studio project-level integration required). **SKELETON-ONLY scope** because Maestro requires a running simulator/emulator + built app binary + Maestro CLI installed locally — none of which are available in the autopilot session; the cleanest scope-cut is YAML flow files + operator runbook + drift detector, with CI wiring deferred to `15-4-followup-maestro-ci-wiring` after the operator sets up simulator/emulator infrastructure on a dedicated runner. NEW `.maestro/config.yaml` (declares `appId: com.companion.app`) + 5 YAML flow files covering the spec's golden flows: [`.maestro/01-signup-flow.yaml`](.maestro/01-signup-flow.yaml) (sign-up → Story 12-9 EmailVerificationGate); [`.maestro/02-onboarding-flow.yaml`](.maestro/02-onboarding-flow.yaml) (sign-in → 3-step onboarding wizard → placement test 15Q → home); [`.maestro/03-first-exercise.yaml`](.maestro/03-first-exercise.yaml) (Practice tab → Grammar → AI exercise → grade); [`.maestro/04-first-conversation.yaml`](.maestro/04-first-conversation.yaml) (Conversation tab → topic → start Realtime session → mic permission → end — validates SHELL connection + UI state transitions, NOT AI response content since Maestro can't inject audio); [`.maestro/05-mock-test-partial-review.yaml`](.maestro/05-mock-test-partial-review.yaml) (Mock Test tab → TCF full sim → listening section 29Q → submit partial → results screen with Story 13-4 parallel section generation + Story 14-7 results reconstruction). Each flow file carries `# TODO: verify selector` markers throughout — operator must verify testIDs / accessibility labels / text strings against the actual running app via `maestro studio` before first execution. Env-injected test account emails (`e2e-test+<flow>-001@invalid.localdomain` + `Abcdefghi1` password complies with Story 12-8 ≥10-char policy). NEW operator runbook [`_bmad-output/planning-artifacts/runbooks/maestro-e2e-setup.md`](_bmad-output/planning-artifacts/runbooks/maestro-e2e-setup.md) (7 sections) walks: install Maestro CLI → boot iOS Simulator → `npx expo run:ios` dev client → `maestro studio` selector verification → seed verified test accounts (Supabase Studio Admin OR seed script using `email_confirm: true`) → run flows locally → CI wire-up procedure for the deferred follow-up. NEW [`src/lib/__tests__/maestro-flows-source-drift.test.ts`](src/lib/__tests__/maestro-flows-source-drift.test.ts) (5 cases) pins: config.yaml with canonical appId; all 5 flow files exist; each flow has non-trivial body (>20 lines + appId); each tags itself with `smoke`; NEGATIVE — NO Maestro CI step is wired in `ci.yml` yet (15-4 skeleton-only contract; CI wiring should fail loudly when added without the 15-4-followup completing first). **3 operator decisions resolved per Recommended:** Q1 Maestro (pre-decided at autopilot kickoff); Q2 defer CI wiring (`15-4-followup-maestro-ci-wiring` filed); Q3 selector-verification TODO markers (vs hard-coded selectors — hardcoding without `maestro studio` verification is hope-driven). **Cross-story invariants preserved:** Story 12-8 password policy (test accounts use 10-char `Abcdefghi1`); Story 12-9 EmailVerificationGate (Flow 1 verifies it fires; Flows 2-5 assume verified accounts); Story 14-1 chrome rule (flows use English button/tab labels); Story 12-10 R1-H2 silent-disable defense (runbook explicitly forbids `continue-on-error: true` on the future CI step — partial gating is a footgun); 0 source-module modifications outside `.maestro/` + new drift test + runbook. **+5 net Jest cases** (drift only; Maestro flows execute outside Jest via `maestro test` CLI). All 5 design-system gates green. Verified 2026-05-17, story 15-4.
**Edge Function Deno tests wired into CI:** post-Epic-15.3 partially closes Epic 12 AI #7 + Epic 13 AI #8 (pgTAP CI wiring follow-through — Deno portion shipped; pgTAP portion deferred to `15-3-followup-pgtap-ci-wiring`). Pre-15-3 the 2 Deno test files (Story 11-3 `fetch-with-timeout_test.ts` + Story 12-11 `parse-upstream-error_test.ts`) were manual-run only; regressions could silently survive PR merges. Story 15-3 wires both into [`.github/workflows/ci.yml`](.github/workflows/ci.yml) via new `Setup Deno` step (`denoland/setup-deno@v2`, pinned `deno-version: v1.x`) + `Deno tests (Edge Function _shared utilities)` step running `deno test --allow-net=127.0.0.1 supabase/functions/_shared/__tests__/`. Permission surface minimal — NO `--allow-all`. Ordering: Deno steps AFTER `Tests` so npm-test failures short-circuit before Deno install. NEW [`src/lib/__tests__/ci-deno-step-source-drift.test.ts`](src/lib/__tests__/ci-deno-step-source-drift.test.ts) (5 cases) pins via Story 12-2 P12 + Story 12-10 R1-H2 patterns. TIGHT scope — pgTAP (5 SQL files) DEFERRED to `15-3-followup-pgtap-ci-wiring`; NEW Edge Function tests DEFERRED to `15-3-followup-edge-function-coverage`. 3 operator decisions resolved per Recommended. +5 net Jest cases (drift only; Deno tests run in CI). All 5 design-system gates green. Verified 2026-05-17, story 15-3.

### Routing (`app/`)

Expand Down
Loading
Loading