Testing
Civix Testing Guide
This document describes the automated testing setup for the Civix monorepo (web app, Interview API, and mobile app), what was implemented, and how to run and extend tests.
Overview
| Area | Tool | What it covers | Where |
|---|---|---|---|
| Web API (health, interview routes) | Vitest | Route handlers: health, interview start/chat/evaluate/complete; status codes, response shape, validation | apps/web |
| Web UI + API from browser | Playwright | Landing page, GET /api/health, docs page load | apps/web |
| Mobile API client | Jest | api-client.ts: isAPIAvailable, startInterview, sendMessage, evaluateAnswer, completeInterview, error handling (429, 404, 500, network) | apps/mobile |
| Mobile E2E (optional) | Maestro | Interview tab → Mock Interview → disclaimer → difficulty selection | apps/mobile |
No real network or API keys are required for unit/integration tests; E2E tests run against a running app or dev server.
What was implemented
1. Web app – API integration tests (Vitest)
- Config:
apps/web/vitest.config.ts(path alias@/for imports). - Files:
app/api/health/route.test.ts– GET health returnsstatus: "ok",endpointslist includes interview routes.app/api/openapi/route.test.ts– validates OpenAPI spec shape, supported interview mode enums, and phase/timing metadata fields.app/api/interview/start/route.test.ts– POST start returns 200 withsessionId,phase,message; 400 for invalid mode or invalid language; accepts en, es, fa, ps, so; GET returns ok.- Includes signed-request checks in enforce mode (missing signature headers rejected, valid
v2signatures accepted, replay nonce rejected, legacyv1rejected in enforce mode).
- Includes signed-request checks in enforce mode (missing signature headers rejected, valid
app/api/interview/chat/route.test.ts– 400 for missing fields, 404 for unknown session, phase-lock metadata (phaseLocked, timers), deterministic no-repeat N-400 sequencing, minimum required N-400 count before phase advance, and enforce-mode signed/unsigned checks.app/api/interview/evaluate/route.test.ts– 400/404 handling, phase-locked evaluation withOUT_OF_ORDERprotection, civics active question/token sequencing, threshold checks for standard vs exemption modes, and enforce-mode signed/unsigned checks.app/api/interview/complete/route.test.ts– 400/404, 200 withpassed/scores/duration/recommendations, 403 on device mismatch, plus enforce-mode signed/unsigned checks.lib/openai-budget.test.ts– validates daily request cap and monthly budget guardrails for OpenAI usage policy.
- How it works: Tests import and invoke Next.js route handlers (GET/POST) with
NextRequest; no server start. Sessions use in-memory store; rate limiting uses Redis when configured and in-memory fallback otherwise. - Run: From
apps/web:npm run test(ornpm run test:watch).
2. Web app – E2E tests (Playwright)
- Config:
apps/web/playwright.config.ts–testDir: ./tests/e2e,baseURL: http://localhost:3000,webServerrunsnpm run devso the dev server starts automatically in CI. - Files:
tests/e2e/landing.spec.ts– Home page loads, title and hero/CTA visible, no critical JS errors.tests/e2e/health.spec.ts–GET /api/healthreturns 200 and JSON withstatus: "ok".tests/e2e/docs.spec.ts–/docsand/docs/how-to-useload, main content visible.
- How it works: Playwright starts the Next.js dev server (or reuses it when not in CI), then runs browser tests against that URL.
- Run: From
apps/web:npm run test:e2e. First run installs Chromium if needed (npx playwright install chromium).
3. Mobile app – API client unit tests (Jest)
- Files:
apps/mobile/__tests__/api-client.test.ts,apps/mobile/__tests__/algorithm.test.ts,apps/mobile/__tests__/i18n-parity.test.ts,apps/mobile/__tests__/multilang-parity.test.ts, and others. - What it tests:
- algorithm: SM-2 (calculateQuality, sm2Calculate), selectQuestions (review/weak/learn/coverage/senior/difficulty/stateProfile), selectExamQuestions (20 standard, 10 senior), checkExamPassed, updateQuestionStats (including skipped), formatNextReview; calculateMasteryProgress returns correct separate counts for learning, due, overdue (no double-count); review mode selects only due questions when present; coverage mode includes unseen when they exist.
- competency milestones:
computeCompetencySnapshot(...)classifies learner tier (foundation,developing,proficient,interview_ready) from mastery/accuracy/overdue/risk signals. - deterministic shuffle:
shuffleArraySeeded(...)provides reproducible ordering for tests/simulation while preserving randomized production behavior. - pass-risk layer: Priority scoring now includes a risk component that amplifies weak/overdue/high-error questions to improve pass-rate outcomes.
- senior regression coverage: Senior mode excludes state-dependent questions without profile context, includes them when profile exists, prioritizes evaluatable senior exam items, and keeps algorithm
SENIOR_QUESTION_IDSsynchronized withsenior65flags in the canonical EN bank. - See
docs/ALGORITHM-REPORT.mdfor algorithm behavior. - i18n-parity: Every locale (en, es, fa, ps, so) has the same string keys as English; catches missing translations.
- multilang-parity: Question banks for es, fa, ps, so keep canonical ID parity with EN; unapproved overlays fall back to EN; translation progress is bounded. Somali has full 128 approved overlays (so-overlays.ts), so getBank('so') returns questions and answers in Somali.
- api-client:
- isAPIAvailable:
falsewhenEXPO_PUBLIC_API_URLis empty (no fetch);truewhen GET/api/interview/startreturns 200 or 405;falsewhen fetch throws. - startInterview: POST to
/api/interview/startwith body{ mode, language, weakTopics }, headersContent-Type,X-Device-ID; response shape. - sendMessage / evaluateAnswer / completeInterview: Correct URL, method, body, and response shape.
- Error handling: 429 →
APIClientErrorRATE_LIMITED; 404 → SESSION_NOT_FOUND; security codes (MISSING_SIGNATURE_HEADERS,ABUSE_THROTTLED, etc.) map to explicit client guidance; 500 and network reject → appropriate message and code. - canUseAIInterview:
falsewhenlastAIInterviewDateis today; otherwise delegates toisAPIAvailable.
- Practice/quiz design: Answer choices are shuffled per question (
shuffleArrayinlib/algorithm.ts) in Practice and Quiz so the correct answer is not always in the same position—reducing position bias and supporting retention. - How it works:
fetchand env are mocked in the test file;AsyncStorageis already mocked injest.setup.ts. No real API or network. - Run: From
apps/mobile:npm test(all tests) ornpm test -- --testPathPattern=api-client.
4. Mobile app – E2E (Maestro, optional)
- Flow:
apps/mobile/.maestro/flows/interview-smoke.yaml. - Steps: Launch app → tap "Interview" → tap "Mock Interview" → tap "I Understand" (disclaimer) → assert "Choose Difficulty" visible.
- How it works: Maestro drives the app on a device/simulator. Requires Maestro CLI and a built/installed app. On first run the disclaimer appears; after accepting once, subsequent runs may skip that step (flow still taps "I Understand" if present).
- Run: Install Maestro, then from
apps/mobile:npm run test:e2eormaestro test .maestro/flows/interview-smoke.yaml. - Docs:
apps/mobile/.maestro/README.md.
How to run (quick reference)
| Command | Where | Description |
|---|---|---|
npm run test | apps/web | Vitest: API route tests |
npm run test:watch | apps/web | Vitest watch mode |
npm run test:e2e | apps/web | Playwright E2E (starts dev server) |
npm run test:interview-api | apps/web | Live API smoke flow (health -> start -> chat (conversation) -> evaluate (test phases) -> complete) |
npm test | apps/mobile | Jest: all unit tests (includes api-client) |
npm test -- --testPathPattern=api-client | apps/mobile | Jest: api-client only |
npm run test:e2e | apps/mobile | Maestro Interview smoke (app must be installed) |
For signed-request enforce deployments, run smoke test with the signing secret:
cd apps/web
INTERVIEW_SIGNING_SECRET=your-secret npm run test:interview-api
CI and pre-release
- Web: Run
npm run testandnpm run test:e2efromapps/webbefore deploy. E2E will start the dev server; for CI, setCI=trueso Playwright uses one worker and retries. - Mobile: Run
npm test(and optionallynpm run preflight) fromapps/mobile. Maestro E2E is optional and typically run on a device/simulator or in a separate mobile CI job. - Production readiness: See
docs/08-PRODUCTION-READINESS.md; automated tests are part of the quality gates.
Extending tests
- New API route (web): Add
app/api/<route>/route.test.ts; import the GET/POST handler and call it with aNextRequest. Use path alias@/(already in vitest.config). - New E2E (web): Add a spec under
apps/web/tests/e2e/; usepage.goto('/path')andrequest.get('/api/...')withbaseURL. - New api-client usage (mobile): Add cases in
__tests__/api-client.test.tswith mockedfetchand env. - New Maestro flow (mobile): Add a YAML file under
apps/mobile/.maestro/flows/and run withmaestro test <file>.
Summary
Automated tests cover: (1) Web Interview API (health, start, chat, evaluate, complete) via Vitest; (2) Web landing, health endpoint, and docs via Playwright; (3) Mobile algorithm + API client behavior and errors via Jest; (4) Optional mobile Interview flow via Maestro. All of this supports deployment confidence and regression detection without requiring production API keys or live services for unit/integration runs.