``` ╔════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ TESTING ║ ║ Vitest 4 + Supertest + jsdom: 1661 tests across 79 files, no services. ║ ║ ║ ╚════════════════════════════════════════════════════════════════════════════╝ ``` # Testing Cloud Codex has **1661 tests across 79 files** and runs the entire suite in under 30 seconds with no MySQL, no SMTP, and no network. Backend tests use **Vitest 4** + **Supertest** with the database, email transport, and filesystem globally mocked. Frontend tests use **Vitest** + **jsdom** + `@testing-library/react`. ```bash cd cloudcodex npm test # both projects npm run test:watch # both projects, watch mode npm run test:backend # backend project only npm run test:frontend # frontend project only npm run test:coverage # both + v8 coverage report (HTML + lcov + text) npm run test:integration # opt-in: the live-MySQL project (needs a server, see below) ``` --- ## Three default Vitest projects, one command `vitest.config.js` defines four named projects. A single `npm test` runs the three default ones, backend, frontend and design, together with the right environment for each; the fourth, `integration`, is opt-in (next section). Backend and frontend: ``` ┌─────────────────────────────────────────────────────────────┐ │ npm test │ │ │ │ │ ┌───────────────────┴───────────────────┐ │ │ ▼ ▼ │ │ project: backend project: frontend │ │ environment: node environment: jsdom │ │ setup: tests/setup.js setup: tests/setup.frontend.js │ │ include: include: │ │ tests/routes/** tests/src/** │ │ tests/middleware/** │ │ tests/services/** │ │ tests/helpers/** │ │ tests/extensions/** │ │ tests/*.test.{js,jsx} │ └─────────────────────────────────────────────────────────────┘ ``` Both projects share the same coverage config so `npm run test:coverage` produces one unified report. The third default project, `design` (node, `tests/design/**`), runs the gates of the vendored `cloud-city-design` package over `index.html` and `src/`: the vendored copy against its SHA-256 manifest, literal colours, accent fill shades in text positions and suppressed outlines against an exact ledger (`tests/design/ledger.json`, which a fix lowers in the same change), `var()` references to undeclared names, and the contrast of `src/codex.css`'s bindings. `node tests/design/report.mjs` prints the ledger's live buckets. The scripts name `--project backend --project frontend --project design` explicitly, and `tests/test-projects.test.js` fails if a project the config declares (other than `integration`) is missing from `test`, `test:watch` or `test:coverage`, so a new project cannot silently drop out of the default run. ## The opt-in live-MySQL project `npm run test:integration` runs `tests/integration/**` against a real MySQL 8.4 server rather than a mock. Each test file gets its own throwaway `c2_it_` schema built from `init.sql` and adopted by the migration runner, and the run fails if any such schema is left behind. Adoption records every migration file and runs none, so the per-file setup alone proves only that `init.sql` builds on MySQL 8.4 and that the runner's adoption check agrees with it. Migration SQL is executed by one file, `tests/integration/upgrade-path.test.js`: it takes an `init.sql` build back to the pre-runner state with the undo statements in `tests/integration/pre-runner-state.js`, records the pre-runner baseline, lets the runner apply every newer file for real, and requires the result to match a fresh `init.sql` build (columns, indexes, constraints, checks and foreign keys). A new migration file fails that test until it has an undo entry. The upgrade runs on empty tables, so a migration's handling of existing rows is not exercised, and the pre-runner files in `LEGACY_BASELINE` are never run. ```bash # any MySQL 8.4 answering on 3306, for example a scratch container: docker run -d --rm --name c2-it-mysql -p 127.0.0.1:3306:3306 \ -e MYSQL_ROOT_PASSWORD= mysql:8.4.11 IT_DB_ROOT_PASSWORD= npm run test:integration # IT_DB_HOST defaults to 127.0.0.1 ``` CI runs it inside the required `Lint, test and build` job against a `mysql:8.4.11` service container. See `docs/maps/build-test-and-ops.md` section 5 for the mechanism. --- ## What's covered, by area | Area | Files in `tests/` | Notes | |-----------------------------------|--------------------------------|----------------------------------------| | HTTP routes | `tests/routes/*.test.js` | Supertest against the Express app | | Middleware | `tests/middleware/*.test.js` | `auth.js`, `permissions.js` | | Services (collab, email, notif) | `tests/services/*.test.js` | Includes WebSocket + email pipelines | | Route helpers | `tests/helpers/*.test.js` | `ownership`, `shared`, `images`, `mentions`, `activity` | | Tiptap extensions | `tests/extensions/*.test.js` | e.g. `Mention` | | Framework files | `tests/{app,server,mysql_connect}.test.js` | App wiring, startup, DB pool | | Frontend pure logic | `tests/src/{editorUtils,userPrefs,util}*.test.js(x)` | jsdom + RTL | | Frontend hooks | `tests/src/hooks/*.test.jsx` | `useCollab`, `useNotificationChannel`, `useGitHubLink`, `useGitHubStatus`, `usePresence`, `useClickOutside` | | Frontend reusable components | `tests/src/components/*.test.jsx` | Bell, Item, Toast, Dialog, Form, Manager, Tree, etc. | | Frontend libs | `tests/src/lib/*.test.js` | e.g. `githubDiff` | Pages (`src/pages/*.jsx`) are **out of scope** for unit tests today — the giants like `Editor.jsx` and `GitHubPage.jsx` need refactoring that extracts logic into testable hooks before they're worth unit-testing. --- ## What gets mocked `tests/setup.js` (backend) replaces these globally so tests run without external dependencies: - `mysql_connect.js` → in-memory `c2_query` mock (fixtures stub queries) - `services/email.js` → `sendEmail` recorded, never sent - `sharp` → image transforms become no-ops (returns the input buffer) - `fs` writes for avatars / doc-images → recorded but never hit disk `tests/setup.frontend.js` (frontend) registers `@testing-library/jest-dom` matchers and resets `localStorage`, `sessionStorage`, and the DOM between tests. `tests/helpers.js` exports common fixtures (`TEST_USER`, `ADMIN_USER`) and auth helpers (`mockAuthenticated`, `mockUnauthenticated`, `resetMocks`). See [`cloudcodex/tests/README.md`](../cloudcodex/tests/README.md) for the full set of patterns and examples. --- ## Coverage thresholds (CI gate) `vitest.config.js` enforces per-glob coverage thresholds. The **global floor** is set just below the currently achieved coverage so normal churn doesn't block CI, but a meaningful regression will. **Security-critical modules** are locked at higher floors: | Glob | Lines | Statements | Branches | Functions | |---------------------------|-------|------------|----------|-----------| | **global floor** | 43 | 40 | 33 | 26 | | `routes/helpers/**` | 88 | 85 | 65 | 90 | | `middleware/**` | 80 | 78 | 70 | 70 | | `routes/auth.js` | 85 | 85 | 82 | 95 | | `routes/admin.js` | 90 | 90 | 88 | 90 | | `routes/archives.js` | 90 | 88 | 75 | 90 | | `routes/comments.js` | 92 | 90 | 85 | 95 | | `routes/documents.js` | 95 | 92 | 88 | 88 | | `routes/notifications.js` | 95 | 95 | 88 | 95 | | `routes/squads.js` | 85 | 75 | 65 | 95 | | `routes/watches.js` | 85 | 85 | 73 | 95 | | `routes/doc-images-serve.js` | 95 | 95 | 92 | 95 | | `services/email.js` | 95 | 95 | 70 | 95 | | `services/email-templates.js` | 95 | 90 | 90 | 95 | | `services/notifications.js` | 90 | 88 | 80 | 88 | | `services/collab.js` | 65 | 65 | 50 | 75 | | `mysql_connect.js` | 85 | 85 | 80 | 90 | | `src/editorUtils.js` | 95 | 95 | 88 | 95 | | `src/userPrefs.js` | 95 | 95 | 80 | 95 | | `src/lib/**` | 90 | 90 | 70 | 80 | Hooks and other frontend modules each have their own targeted floors — see `vitest.config.js` for the canonical list. > The global floor is intentionally low because pages (`src/pages/*`) and > some heavy components are unmeasured. **Don't read it as the project's > health number** — read the per-glob thresholds for the modules that > matter. --- ## Adding tests with new code The convention: **a route, service, helper, middleware, hook, pure-JS utility, or reusable component change requires a matching test update in the same shape as its neighbors.** Tests mirror the source tree: ``` routes/foo.js → tests/routes/foo.test.js services/foo.js → tests/services/foo.test.js middleware/foo.js → tests/middleware/foo.test.js routes/helpers/foo.js → tests/helpers/foo.test.js src/foo.js → tests/src/foo.test.js src/components/Foo.jsx → tests/src/components/Foo.test.jsx src/hooks/useFoo.js → tests/src/hooks/useFoo.test.jsx ``` Pages are explicitly out of scope until the underlying logic is hookified.