# Scope: Website Test Suite

**Created by:** Ana
**Date:** 2026-05-20

## Intent
Make the website a real testable surface. Add a test script and 30-40 tests across 7 new files covering the data layer computation functions, pure utilities, and structural integrity. After this ships, `(cd website && pnpm vitest run)` runs 38-48 meaningful tests instead of 8 marketing-stats tests. Pipeline verification of website changes becomes real verification, not theater.

This is stage 4 of the surface awareness roadmap (1.1.2). Stages 1-3 (scan detection, ana.json surfaces, bridge) are shipped and on main.

## Complexity Assessment
- **Kind:** feature
- **Size:** small — 7 new test files, 1 line added to package.json, zero production code
- **Surface:** website
- **Files affected:**
  - Modified: `website/package.json` (add test script)
  - New: `website/lib/__tests__/docs-data/proofs.test.ts`
  - New: `website/lib/__tests__/docs-data/docs-stat-values.test.ts`
  - New: `website/lib/__tests__/docs-data/strip-jsx.test.ts`
  - New: `website/lib/__tests__/docs-data/data-integrity.test.ts`
  - New: `website/lib/__tests__/format.test.ts`
  - New: `website/lib/__tests__/proof-feed.test.ts`
  - New: `website/lib/__tests__/copy.test.ts`
- **Blast radius:** None. Test-only scope. No production code changes. Existing 8 marketing-stats tests are untouched.
- **Estimated effort:** ~1 pipeline cycle, ~355-445 lines of test code
- **Multi-phase:** no

## Approach
Add the missing `"test": "vitest run"` script to complete the website's surface integration with Turborepo and CI. Then build test coverage for the data layer and pure utilities that power the website — proof computation, dynamic stats, JSX stripping for search, copy catalog integrity, and formatting helpers. Tests use synthetic inline fixtures and module-level mocking following the convention established by marketing-stats.test.ts. A single data integrity contract test reads real generated JSON files with `describe.skipIf` for graceful degradation on fresh clones.

## Acceptance Criteria
- AC1: `website/package.json` has `"test": "vitest run"` script
- AC2: `cd website && pnpm vitest run` passes with 38+ tests (8 existing + 30+ new)
- AC3: `pnpm run test -- --run` (root) runs both CLI and website tests without hanging
- AC4: proofs.test.ts tests `getProofStats` and `getMedianTimings` computation logic with mocked `node:fs`
- AC5: docs-stat-values.test.ts tests `buildDocsStatValues` and `resolveDocsStatTags` as pure functions (no mocking)
- AC6: strip-jsx.test.ts tests regex/stripping logic with 5 mocked data accessor imports (proofs ×3, skills ×1, gotchas ×1)
- AC7: data-integrity.test.ts validates real `data/docs/*.json` files exist and match expected shapes, skipped via `describe.skipIf` when data dir doesn't exist
- AC8: format.test.ts tests `splitHeadline` parsing of `*emphasis*` markers
- AC9: proof-feed.test.ts tests `formatAge` boundary behavior (seconds/minutes/hours/days thresholds, future timestamp clamping)
- AC10: copy.test.ts validates structural integrity of the copy catalog (top-level sections, nav links, footer columns, non-empty strings)
- AC11: Zero production code changes — no modifications to any `lib/`, `app/`, or `components/` source files
- AC12: All new test files are in `lib/__tests__/` or `lib/__tests__/docs-data/` (matches existing vitest include pattern)

## Edge Cases & Risks
- **Fresh clone without build:** Data files in `data/docs/` are generated by prebuild and gitignored. The data integrity contract test uses `describe.skipIf(!dataExists)` so `pnpm test` works on fresh clones. All other tests use mocks or pure functions — no dependency on generated files.
- **Module-level caching in data accessors:** Every accessor has `let cached = null`. Tests must use `vi.resetAllMocks()` in `beforeEach` and mock at the import boundary (or `node:fs` for proofs.ts) to prevent data leaking across tests.
- **Turborepo watch mode hang:** The test script must be `vitest run` (not `vitest`) so `turbo run test` exits cleanly. Already confirmed this is the convention.
- **proofs.ts mocking is different from marketing-stats pattern:** marketing-stats.test.ts mocks at the module import boundary. proofs.test.ts must mock `node:fs` because we're testing exports from the same module that does the IO. Plan must use `vi.mock('node:fs')` with controlled `readFileSync` returns.
- **stripJsx mock for getProofStats:** The REQ's example mock is missing `assertions` field. The mock must include all 4 ProofStats fields: `{ entries, assertions, findings, rejections }`.
- **formatAge's two clamps:** `Math.max(0, diff)` prevents negative diff from future timestamps. `Math.max(1, Math.floor(diff))` prevents "0s ago". Both produce "1s ago" for edge cases but for different reasons. Tests should cover both.

## Rejected Approaches
- **Data accessor smoke tests (mock readFileSync, assert shape):** Three independent scrutiny agents identified these as tautological — you control the mock input AND the assertion, testing `JSON.parse` not application code. Replaced by the data integrity contract test that reads real generated files.
- **Testing unexported proof-feed internals (extractFeatureEm, resolveKind, mapEntry):** Would require either exporting them (production code change, out of scope) or testing through `getProofFeed` (requires mocking `fetch` + complex async setup). Deferred to a future scope when AnaWeb ships and these become critical infrastructure.
- **Component smoke tests (jsdom):** Different test infrastructure (requires React testing setup, jsdom environment). Future scope.
- **extract-docs-data integration test:** Requires CLI source files at test time, adds build-time dependency. The contract test covers shape drift without the script dependency.

## Open Questions
None — all design decisions resolved in the REQ and Ana's investigation.

## Exploration Findings

### Patterns Discovered
- `marketing-stats.test.ts`: `vi.mock()` at module boundary, `vi.mocked()` for typed mock access, `beforeEach(() => vi.resetAllMocks())`. This is the convention for all new test files.
- `proofs.ts` lines 31-34: Zero-valued timing stages filtered with `> 0` checks before pushing to median arrays. Tests must cover this.
- `stripJsx.ts` lines 47-66: Two arrays define component handling — `blockComponents` (preserve children) and `stripFull` (remove entirely). Tests should cover both categories.
- `formatAge` line 209: Inner `Math.max(1, ...)` prevents "0s ago" — distinct from the outer `Math.max(0, ...)` diff clamp.
- `copy.ts`: 20 top-level sections, `nav.links` and `footer.columns` are arrays with typed entries.

### Constraints Discovered
- [TYPE-VERIFIED] ProofStats requires 4 fields (types.ts:58-63) — `{ entries, assertions, findings, rejections }`
- [TYPE-VERIFIED] ProofEntry.timing has `think`, `plan`, `build`, `verify`, `totalMinutes` fields (types.ts:22-29)
- [OBSERVED] vitest.config.ts include pattern `lib/__tests__/**/*.test.ts` already supports subdirectories
- [OBSERVED] No test script in package.json scripts object — confirmed via direct read
- [OBSERVED] `docsStatValues.ts` is genuinely pure — no imports beyond its own type interface

### Test Infrastructure
- `vitest.config.ts`: globals: true, environment: 'node', `@` alias to website root
- `marketing-stats.test.ts`: canonical mocking pattern — vi.mock before import, vi.mocked for typed access, beforeEach resetAllMocks

## For AnaPlan

### Structural Analog
`website/lib/__tests__/marketing-stats.test.ts` — the one existing test file. Same mocking convention (vi.mock → vi.mocked → beforeEach reset), same directory, same vitest config. Every new test file should follow this structure.

### Relevant Code Paths
- `website/lib/docs-data/proofs.ts` — computation functions (getProofStats lines 54-69, getMedianTimings lines 26-52), readFileSync IO (lines 1-15)
- `website/lib/docs-data/docsStatValues.ts` — pure functions (buildDocsStatValues lines 28-40, resolveDocsStatTags lines 54-59)
- `website/lib/docs-data/stripJsx.ts` — regex stripping with 5 data accessor imports (lines 1-4), blockComponents array (line 47), stripFull array (line 56)
- `website/lib/docs-data/types.ts` — ProofEntry (lines 31-56), ProofStats (lines 58-63), ProofTiming (lines 22-29)
- `website/lib/format.ts` — splitHeadline (lines 18-27), TextSegment interface (lines 7-9)
- `website/lib/proof-feed.ts` — formatAge (lines 207-213), two Math.max clamps
- `website/lib/copy.ts` — 20 top-level sections, nav.links array, footer.columns array

### Patterns to Follow
- `marketing-stats.test.ts` — the canonical mock/test pattern for this surface
- vitest.config.ts — existing config, no changes needed

### Known Gotchas
- proofs.ts exports AND does IO in the same module — can't mock the module to test its own exports. Must mock `node:fs` instead. This is a different pattern from marketing-stats.test.ts.
- stripJsx mock for getProofStats must include all 4 ProofStats fields (the REQ example was missing `assertions`)
- `describe.skipIf` is a vitest API — confirm it's available in vitest 4.x (it is)
- Data files are in `data/docs/`, not `lib/docs-data/` — the JSON files and the TS modules that read them are in different directories

### Things to Investigate
- Decide the exact `vi.mock('node:fs')` pattern for proofs.test.ts: mock `readFileSync` to return a JSON string of synthetic ProofEntry[], then test getProofStats and getMedianTimings against that controlled data. Consider whether `vi.resetModules()` + dynamic import is needed to bust the module-level cache between test groups, or whether `vi.mock('node:fs')` with per-test `mockReturnValue` is sufficient.
