# TESTING — anwarpy.ai > Owner decision 2026-10-11: **tier (b) is the bar** — Vitest unit tests for the core libs + regression tests for API route behavior. Tier (c) — Playwright E2E for critical journeys — activates when Phase 3 (Stripe) lands; journeys to cover then: signup→verify→login, VIP ghost-read (billing on), checkout happy path, webhook apply. ## 1. Standing gates (before any commit/merge) ```bash cd web && npx tsc --noEmit # must exit 0 cd web && npm run build # must exit 0 (includes generate:types) ``` Both green = minimum. Gates alone are not proof of behavior — see §3. ## 2. Unit tests (Vitest, tier b) - Add `vitest` as a dev dependency; tests live beside the code or under `web/src/lib/__tests__/` — pick one location and stay consistent (recommend `__tests__/` next to `src/lib`). - **Required suites (the known-risk list):** | Suite | Must pin | |---|---| | `launch.test.ts` | policy truth table: `billingEnabled()` false when `LAUNCH_MODE` unset/**free_early_access**/**paid**… — including the fail-safe default | | `lexical.test.ts` | `lexicalToText`/`countLexicalWords` **descend into `content.root`** (regression: the 2026-10-10 launch-blocking bug where both returned 0/'' on real serialized trees); `extractH2s`, `lexicalToPlain` | | `secret-scan.test.ts` | one case per pattern (aws_access_key, pem_private_key, openai_api_key, github_token, generic_credential) returns **pattern name only, never the matched text**; clean payload passes; string content is scanned too | | `topics.test.ts` | `isTopic` accepts exactly agents/models/automation/foundations; `topicsFor` mapping | | `format.test.ts` | `guidesLabel` (1 دليل، 2 دليلان، 3–10 أدلة، 11+), `minutesLabel`, `MEMBERSHIP_PRICE` | - Rule: tests assert **behavior**, not implementation text. No snapshot-of-source, no tautologies (`expect(fn).toBeDefined()`), no mock-echo tests, no bare "does not throw". - Runtime env manipulation: use `vi.stubEnv`/`unstubAllEnvs` — never let one test's `LAUNCH_MODE` leak into another. ## 3. API route regression tests Behavior tests for the routes with contracts (per PRD §3.6, SECURITY §4): - `POST/PUT /api/agent/posts`: 401 wrong/missing key (note: **`x-agent-key`**, not Bearer); 400 on string `content`; 422 with pattern names on secret-bearing Lexical; `sourceId` dedup returns existing post (no 409, no duplicate row); `pickFields` strips `access`/`readingMinutes`; PUT cannot set `status`. - `POST /api/subscribe`: 503 + exact Arabic body while billing is off. - `POST /api/notify`: records row then honest 503 when mail transport fails; duplicate email → 200 `alreadySubscribed: true`; no double-send on dup. - Comments route: unauthenticated → reject; client-sent `author` ignored (server-forced). - Likes: 401 anonymous; second like by same user → no duplicate row. Implementation: pure logic moves into `src/lib` helpers where possible and gets unit-tested; handler-level tests run the route module with a mocked Payload local API (or hit a dev server for the smoke tier — §5). Prefer extracting decision logic over wiring up heavy mocks. ## 4. What must NOT become a test - Wiring/copies/forwarding, mock echoes, source-text greps, "file exists". - Incidental defaults that aren't contracts (an unstyled div's class list is not a spec). - Anything already verified cheaper by the ghost-read/smoke script in §5. - Per the gstack rule: existing tests that pin wording/implementation instead of behavior get deleted, not "fixed". ## 5. Smoke & verification scripts (throwaway, not committed) - Prod verification is HTTP-level and scripted ad hoc (curl batches): ghost-read a VIP post logged out; check `isAccessibleForFree`, og:image 1200×630, JSON-LD types; subscribe 503; notify row + cleanup; sitemap contents; www 308. Use `@example.com` rows and **delete them after** (ground truth: users=6, launch_notifications=0). - Keep the failing-before/passing-after pair when fixing a bug — a permanent regression test in §2/§3 if it's pure logic, the script transcript if it's deploy/config. ## 6. Before-merge checklist (condensed) 1. `tsc --noEmit` + `npm run build` exit 0. 2. New/changed lib behavior → matching Vitest suite updated or added. 3. Changed route contract → §3 regression updated (status + body shape). 4. Changed access logic → ghost-read check (logged-out VIP post) recorded in the PR. 5. No new `any`, no new `'use client'` without justification, no new dependency without a line of justification (CODE_STYLE §8).