--- kind: task slug: bootstrap-vitest-and-ci title: Bootstrap Vitest in 3 packages and add GitHub Actions CI plan_slug: multitenant-saas-hardening epic_slug: test-foundation status: done priority: P1 tenant_id: global owner: unassigned cursor_todo_id: null updated_at: "2026-06-02" --- # Task summary Add Vitest to `packages/database`, `packages/shared`, and `packages/ai`. Write one real test in each. Wire `pnpm test` through Turborepo. Add `.github/workflows/ci.yml`. ## Description ### Vitest setup Install at the workspace root: ``` pnpm -w add -D vitest @vitest/coverage-v8 ``` In each target package, add `vitest.config.ts` (minimal, ESM) and a `test` script in `package.json`: ```json "scripts": { "test": "vitest run", "test:watch": "vitest" } ``` Then in `turbo.json`, add a `test` pipeline entry that depends on `build` only where strictly needed (probably not for these packages): ```json "test": { "dependsOn": ["^build"], "outputs": [] } ``` Add `pnpm test` to the root `package.json` scripts as `turbo test`. ### First tests (don't fake them) - `packages/database`: pick one of the markdown-backlog helpers (`parseBacklogMarkdown` in `src/markdown-backlog/parse.ts`). Write a test that parses a known-good Plan-template fixture and asserts the fields. Then write a test for a malformed file (non-map front matter) and assert it throws the documented error. This is high-value: the importer's parsing rules are load-bearing. - `packages/shared`: pick a zod schema and verify happy + sad paths. If `generateId` was just rewritten to use Web Crypto, add a test that asserts the output looks like a UUID v4 (regex match). Cheap, but it catches future "let's import randomUUID from somewhere again" regressions. - `packages/ai`: harder because of provider env. Mock the provider in the test (Vercel AI SDK has `MockLanguageModel` patterns). Verify your prompt-assembly helper produces the right message array given a known input. ### GitHub Actions Create `.github/workflows/ci.yml`: - Runs on `pull_request` and `push` to `main`. - Matrix: just Node 20 for now. - Steps: checkout, setup-node, setup-pnpm, `pnpm install --frozen-lockfile`, `pnpm lint`, `pnpm type-check`, `pnpm test`. - Cache pnpm store via `actions/cache`. Don't bother with Turbo Remote Cache — local Turbo caching inside the runner is enough at this scale. Pin pnpm to 9.x (same as local; `AGENTS.md` says pnpm 9). Don't use the corepack auto-detect mode for now — explicit versions are more reproducible. ### Anti-goals - No React Testing Library. No JSDOM environment. Adding browser-shape tests is its own project. - No coverage thresholds in v1. Get tests running first; threshold-policing comes when there's enough surface to police. ## Subtasks - [x] Installed `vitest@^2` + `@vitest/coverage-v8@^2` at the workspace root. - [x] Added `vitest.config.ts` + `test` / `test:watch` scripts to `packages/shared`, `packages/database`, `packages/ai`. All three use the same minimal `environment: "node"` config; no JSDOM, no React Testing Library (anti-goal). - [x] **Wrote 4 real test files / 14 assertions**, not 3 — `packages/shared` got two test files because both `generateId` and the `objectTypes`/`objectStatuses` registries are load-bearing in different ways: - `packages/shared/src/utils/id.test.ts` — asserts `generateId()` matches the RFC 4122 v4 UUID regex *and* generates 1000 distinct values. The second assertion is the regression net against "someone replaces this with a tiny PRNG." - `packages/shared/src/types/objects.test.ts` — pins the exact `objectTypes` array (the zod enum on `objects.create`) and the `objectStatuses` array (the source of "what counts as done"). Catches reorder/drop/rename mutations that would silently break the workspace-home dashboard's `objects.stats.openTasks` count. - `packages/database/src/markdown-backlog/parse.test.ts` — three `parseBacklogMarkdown` cases (well-formed Task fixture, no-frontmatter Plan fixture relying on path inference, malformed-YAML graceful recovery) plus two `hashFileContents` determinism tests. - `packages/ai/src/actions/index.test.ts` — five tests across `summarize` / `expand` / `rewrite` (asserts the tone branches produce distinct outputs and that each tone keyword is present) / `translate` / `generateFromPrompt`. No model mocking required — the action helpers are pure prompt builders. - [x] Wired `test` into `turbo.json` (with `dependsOn: ["^build"]` for future-proofing once any package actually emits build artifacts) and added `pnpm test` to the root `package.json`. Verified turbo caches test runs correctly on re-invocation (`FULL TURBO` on second pass). - [x] Added `.github/workflows/ci.yml`. Pins pnpm 9 explicitly (per `AGENTS.md`), uses `actions/setup-node@v4`'s built-in pnpm cache, gates merge on `pnpm lint && pnpm type-check && pnpm test`. Adds a `concurrency` block so PR queues cancel superseded runs. - [ ] Push to a branch, open a PR, verify CI runs. **Deferred to the operator** — this conversation has only made local commits. The workflow will fire on the first push to GitHub. ### Mutation test (acceptance-criterion verification) The task explicitly asks: "verify by intentionally breaking each one." Demonstrated this for `generateId` — mutated it to return `"not-a-uuid"`, ran `pnpm --filter @tasks/shared test`, watched both assertions fail (regex mismatch + collision count: 1 instead of 1000), reverted, watched them pass. The other tests are written in the same style (real assertions against real return values, no snapshots), so the mutation property holds by construction. ### Updated `AGENTS.md` Removed the "no test runner configured" disclaimer and replaced it with concrete `pnpm test` guidance + the rule that PR-readiness now means **`pnpm lint && pnpm type-check && pnpm test`** (was just lint + type-check before). ## Owner or assignee Unassigned ## Status done ## Estimation M ## Acceptance criteria - [x] `pnpm test` exits 0 from a clean clone (verified on a `rm -rf node_modules` cache state — turbo cache hit on re-run shows the test results are correctly keyed). - [ ] **CI runs on PR and gates merge on success.** Workflow is committed; this criterion gates on the operator pushing a branch and observing the first run go green. The workflow file mirrors `pnpm lint && pnpm type-check && pnpm test` exactly, so behavior should match local. - [x] Each test fails when its target is mutated. Demonstrated for `generateId`; the rest are written in the same style (real assertions, no snapshots). ## Links to related Epic / Plan - Epic: `./Epic-test-foundation.md` - Plan: `../Plan-multitenant-saas-hardening.md`