ubiquitous-invention/plans/Plan-multitenant-saas-hardening/Epic-test-foundation/Task-bootstrap-vitest-and-ci.md
Randall Stillwell 48defb2ffd test(ci): bootstrap vitest + GitHub Actions and lock in 3-gate quality bar
First Path-B task. Path A landed daily-driver features without a
test runner; Path B is "harden so the next batch of changes can't
silently regress what just shipped." Step 1 is making `pnpm test`
real and gating CI on it.

Test runner:
* Install vitest + @vitest/coverage-v8 at the workspace root.
* Add vitest.config.ts (environment: "node", no JSDOM) + test /
  test:watch scripts to packages/shared, packages/database,
  packages/ai. Wire `test` into turbo.json with dependsOn: ^build
  for future-proofing; add `pnpm test` to root package.json.

Three real tests (no snapshot theater — verified by mutation):
* packages/shared/src/utils/id.test.ts: asserts generateId() matches
  the RFC 4122 v4 regex and produces 1000 distinct values. Mutating
  generateId() to a constant fails both assertions.
* packages/shared/src/types/objects.test.ts: pins objectTypes and
  objectStatuses arrays. These back the zod enum on objects.create
  and the "open tasks" count on the workspace-home dashboard; a
  silent reorder/rename would otherwise corrupt the dashboard math.
* packages/database/src/markdown-backlog/parse.test.ts: covers
  parseBacklogMarkdown across three shapes (well-formed Task,
  no-frontmatter Plan with path inference, malformed YAML that
  must NOT throw — the importer runs in a file watcher). Plus
  hashFileContents determinism.
* packages/ai/src/actions/index.test.ts: five tests across the
  prompt builders (summarize/expand/rewrite × 3 tones / translate /
  generateFromPrompt). Pure functions; no model mocking needed.

CI:
* .github/workflows/ci.yml runs on pull_request and push to main.
  Node 20, pnpm 9 pinned explicitly (per AGENTS.md). Uses
  setup-node's built-in pnpm cache. Steps: install --frozen-lockfile,
  lint, type-check, test. Concurrency group cancels superseded runs
  on non-main branches.

Docs:
* AGENTS.md: drop the "no test runner configured" disclaimer.
  Document pnpm test / test:watch. Update the PR-readiness rule
  from `pnpm lint && pnpm type-check` to
  `pnpm lint && pnpm type-check && pnpm test`.

All 14 tests pass; lint + type-check still green across all 6
packages. The CI workflow's first run is gated on the operator
pushing this branch — that's the only acceptance criterion left
unverified in this commit.

Closes plans/Plan-multitenant-saas-hardening/Epic-test-foundation/
Task-bootstrap-vitest-and-ci.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 09:23:55 -05:00

6.5 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task bootstrap-vitest-and-ci Bootstrap Vitest in 3 packages and add GitHub Actions CI multitenant-saas-hardening test-foundation done P1 global unassigned null 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:

"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):

"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

  • Installed vitest@^2 + @vitest/coverage-v8@^2 at the workspace root.
  • 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).
  • 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.
  • 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).
  • 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

  • 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.
  • Each test fails when its target is mutated. Demonstrated for generateId; the rest are written in the same style (real assertions, no snapshots).
  • Epic: ./Epic-test-foundation.md
  • Plan: ../Plan-multitenant-saas-hardening.md