diff --git a/docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md b/docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md new file mode 100644 index 0000000..2a3a8c3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md @@ -0,0 +1,2287 @@ +# Plan 7 — TDD Foundation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make full TDD frictionless in this monorepo by closing the ten gaps catalogued in `docs/superpowers/specs/2026-05-05-tdd-foundation-design.md`. + +**Architecture:** Add a new `@repo/core-testing` package providing factories, contract suites, RTL helpers, and Payload mocks. Layer Vitest safety defaults (jsdom + node bases with coverage thresholds) into `core-typescript`. Add tests to every package and app, rewrite the docs to enforce TDD order, and add a CI workflow that runs typecheck + lint + boundaries + test + build + e2e + storybook on every PR. + +**Tech Stack:** Vitest 3, @testing-library/react, @testing-library/user-event, jsdom, @storybook/test-runner, GitHub Actions, msw (or fetch stubs), tsx. + +**Spec:** `docs/superpowers/specs/2026-05-05-tdd-foundation-design.md` — read this first if any task is unclear. + +**Worktree:** Execute on branch `feature/tdd-foundation` in `.worktrees/tdd-foundation/`. + +--- + +## Cross-cutting conventions (re-read at the start of every task) + +- **TDD always:** write a failing test, run it to confirm RED, write minimal implementation, run to confirm GREEN, refactor, commit. +- **Source files use relative imports** (`../foo.js`); test files use `@/` alias. +- **Every new vitest config** must extend `nodeVitestConfig` or `jsdomVitestConfig` from `@repo/core-typescript` and declare the `@/` alias. +- **Commit per task** with a clear message. Never bundle two tasks into one commit. +- **After each task:** run `pnpm typecheck && pnpm lint && pnpm test` from repo root before declaring DONE. +- **If a step says "expected: PASS"** and the test fails, do NOT proceed. Diagnose and fix before moving on. + +--- + +### Task 1: Scaffold `@repo/core-testing` package + +**Files:** +- Create: `packages/core-testing/package.json` +- Create: `packages/core-testing/tsconfig.json` +- Create: `packages/core-testing/vitest.config.ts` +- Create: `packages/core-testing/eslint.config.js` +- Create: `packages/core-testing/turbo.json` +- Create: `packages/core-testing/AGENTS.md` +- Create: `packages/core-testing/src/index.ts` +- Create: `packages/core-testing/src/factory/define-factory.ts` +- Create: `packages/core-testing/src/factory/define-factory.test.ts` +- Create: `packages/core-testing/src/factory/index.ts` +- Create: `packages/core-testing/src/contract/define-contract-suite.ts` +- Create: `packages/core-testing/src/contract/define-contract-suite.test.ts` +- Create: `packages/core-testing/src/contract/index.ts` +- Create: `packages/core-testing/src/setup/jsdom.ts` +- Create: `packages/core-testing/src/setup/node.ts` +- Create: `packages/core-testing/src/payload/stub-config.ts` +- Create: `packages/core-testing/src/payload/mock-payload-module.ts` +- Create: `packages/core-testing/src/payload/index.ts` +- Create: `packages/core-testing/src/react/render-with-providers.tsx` +- Create: `packages/core-testing/src/react/render-with-providers.test.tsx` +- Create: `packages/core-testing/src/react/mock-trpc.ts` +- Create: `packages/core-testing/src/react/index.ts` +- Modify: `tsconfig.base.json` — add `@repo/core-testing/*` aliases + +- [ ] **Step 1: Create package skeleton** + +```bash +mkdir -p packages/core-testing/src/{factory,contract,setup,payload,react} +``` + +- [ ] **Step 2: Write package.json** + +`packages/core-testing/package.json`: +```json +{ + "name": "@repo/core-testing", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./factory": "./src/factory/index.ts", + "./contract": "./src/contract/index.ts", + "./react": "./src/react/index.ts", + "./payload": "./src/payload/index.ts", + "./setup/jsdom": "./src/setup/jsdom.ts", + "./setup/node": "./src/setup/node.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.0", + "@trpc/client": "^11.0.0", + "@trpc/react-query": "^11.0.0", + "@trpc/tanstack-react-query": "^11.0.0", + "@tanstack/react-query": "^5.59.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "superjson": "^2.2.0", + "vitest": "^3.0.0" + }, + "peerDependencies": { + "payload": "^3.0.0" + }, + "peerDependenciesMeta": { + "payload": { "optional": true } + }, + "devDependencies": { + "@repo/core-eslint": "workspace:*", + "@repo/core-typescript": "workspace:*", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "jsdom": "^25.0.0", + "typescript": "^5.8.0" + } +} +``` + +- [ ] **Step 3: Write tsconfig.json** + +`packages/core-testing/tsconfig.json`: +```json +{ + "extends": "@repo/core-typescript/react-library.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +- [ ] **Step 4: Write vitest.config.ts** + +`packages/core-testing/vitest.config.ts`: +```typescript +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + include: ["src/**/*.test.{ts,tsx}"], + setupFiles: ["./src/setup/jsdom.ts"], + clearMocks: true, + restoreMocks: true, + }, + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, +}); +``` + +- [ ] **Step 5: Write eslint.config.js** + +`packages/core-testing/eslint.config.js`: +```javascript +import { config as base } from "@repo/core-eslint/base"; + +export default [ + ...base, + { + rules: { + // test utilities are intended to be used in test contexts + "no-console": "off", + }, + }, +]; +``` + +- [ ] **Step 6: Write turbo.json (tag: tooling)** + +`packages/core-testing/turbo.json`: +```json +{ + "$schema": "https://turborepo.dev/schema.json", + "extends": ["//"], + "tags": ["tooling"] +} +``` + +- [ ] **Step 7: Write failing test for `defineFactory`** + +`packages/core-testing/src/factory/define-factory.test.ts`: +```typescript +import { describe, it, expect, beforeEach } from "vitest"; +import { defineFactory } from "@/factory/define-factory"; + +interface User { + id: string; + name: string; + age: number; + createdAt: Date; +} + +describe("defineFactory", () => { + const userFactory = defineFactory(({ sequence }) => ({ + id: `user-${sequence}`, + name: `User ${sequence}`, + age: 30, + createdAt: new Date("2026-01-01T00:00:00Z"), + })); + + beforeEach(() => userFactory.reset()); + + it("builds a default object", () => { + const u = userFactory.build(); + expect(u).toEqual({ + id: "user-1", + name: "User 1", + age: 30, + createdAt: new Date("2026-01-01T00:00:00Z"), + }); + }); + + it("increments sequence per build", () => { + const a = userFactory.build(); + const b = userFactory.build(); + expect(a.id).toBe("user-1"); + expect(b.id).toBe("user-2"); + }); + + it("applies overrides", () => { + const u = userFactory.build({ name: "Alice", age: 25 }); + expect(u.name).toBe("Alice"); + expect(u.age).toBe(25); + expect(u.id).toBe("user-1"); + }); + + it("buildList builds N items with same overrides", () => { + const list = userFactory.buildList(3, { age: 40 }); + expect(list).toHaveLength(3); + expect(list.map((u) => u.id)).toEqual(["user-1", "user-2", "user-3"]); + expect(list.every((u) => u.age === 40)).toBe(true); + }); + + it("reset() restarts the sequence", () => { + userFactory.build(); + userFactory.build(); + userFactory.reset(); + expect(userFactory.build().id).toBe("user-1"); + }); +}); +``` + +- [ ] **Step 8: Run test to verify RED** + +```bash +pnpm install +cd packages/core-testing && pnpm test +``` + +Expected: FAIL — `defineFactory` not exported. + +- [ ] **Step 9: Implement `defineFactory`** + +`packages/core-testing/src/factory/define-factory.ts`: +```typescript +export interface FactoryContext { + sequence: number; +} + +export interface Factory { + build(overrides?: Partial): T; + buildList(count: number, overrides?: Partial): T[]; + reset(): void; +} + +export function defineFactory( + builder: (ctx: FactoryContext) => T, +): Factory { + let sequence = 0; + return { + build(overrides) { + sequence += 1; + const base = builder({ sequence }); + return { ...base, ...(overrides ?? {}) } as T; + }, + buildList(count, overrides) { + return Array.from({ length: count }, () => this.build(overrides)); + }, + reset() { + sequence = 0; + }, + }; +} +``` + +`packages/core-testing/src/factory/index.ts`: +```typescript +export { defineFactory, type Factory, type FactoryContext } from "./define-factory.js"; +``` + +- [ ] **Step 10: Run test to verify GREEN** + +```bash +cd packages/core-testing && pnpm test +``` + +Expected: 5/5 PASS in `define-factory.test.ts`. + +- [ ] **Step 11: Write failing test for `defineContractSuite`** + +`packages/core-testing/src/contract/define-contract-suite.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import { defineContractSuite } from "@/contract/define-contract-suite"; + +interface Adder { + add(a: number, b: number): number; +} + +const adderContract = defineContractSuite("Adder", ({ buildSubject }) => { + it("adds two positive numbers", async () => { + const subject = await buildSubject(); + expect(subject.add(2, 3)).toBe(5); + }); + it("handles zero", async () => { + const subject = await buildSubject(); + expect(subject.add(0, 0)).toBe(0); + }); +}); + +class RealAdder implements Adder { + add(a: number, b: number) { + return a + b; + } +} + +describe("RealAdder satisfies Adder contract", () => { + adderContract.run(() => new RealAdder()); +}); +``` + +- [ ] **Step 12: Run test to verify RED** + +```bash +cd packages/core-testing && pnpm test +``` + +Expected: FAIL — `defineContractSuite` not exported. + +- [ ] **Step 13: Implement `defineContractSuite`** + +`packages/core-testing/src/contract/define-contract-suite.ts`: +```typescript +import { describe } from "vitest"; + +export interface ContractContext { + buildSubject: () => Promise | T; +} + +export interface ContractSuite { + run(buildSubject: () => Promise | T): void; +} + +export function defineContractSuite( + name: string, + suite: (ctx: ContractContext) => void, +): ContractSuite { + return { + run(buildSubject) { + describe(`Contract: ${name}`, () => { + suite({ buildSubject }); + }); + }, + }; +} +``` + +`packages/core-testing/src/contract/index.ts`: +```typescript +export { defineContractSuite, type ContractContext, type ContractSuite } from "./define-contract-suite.js"; +``` + +- [ ] **Step 14: Run test to verify GREEN** + +```bash +cd packages/core-testing && pnpm test +``` + +Expected: 7/7 PASS (5 factory + 2 contract). + +- [ ] **Step 15: Implement setup files** + +`packages/core-testing/src/setup/jsdom.ts`: +```typescript +import "@testing-library/jest-dom/vitest"; +import { afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; + +afterEach(() => { + cleanup(); +}); +``` + +`packages/core-testing/src/setup/node.ts`: +```typescript +// Reserved for future global node-env setup. Currently a no-op so that +// vitest configs may reference @repo/core-testing/setup/node uniformly. +export {}; +``` + +- [ ] **Step 16: Implement payload stubs** + +`packages/core-testing/src/payload/stub-config.ts`: +```typescript +import type { SanitizedConfig } from "payload"; + +// Minimal SanitizedConfig stub for tests that need to construct repos +// without actually loading the real Payload config. Repository tests +// that mock the `payload` module never read fields off this object. +export const stubPayloadConfig = {} as SanitizedConfig; +``` + +`packages/core-testing/src/payload/mock-payload-module.ts`: +```typescript +import { vi } from "vitest"; +import type { Payload } from "payload"; + +// Helper to mock the `payload` package with a custom getPayload impl. +// Call inside a test file BEFORE importing the SUT. +export function mockPayloadModule(impl: Partial): void { + vi.mock("payload", () => ({ + getPayload: vi.fn().mockResolvedValue(impl), + })); +} +``` + +`packages/core-testing/src/payload/index.ts`: +```typescript +export { stubPayloadConfig } from "./stub-config.js"; +export { mockPayloadModule } from "./mock-payload-module.js"; +``` + +- [ ] **Step 17: Implement react helpers** + +`packages/core-testing/src/react/mock-trpc.ts`: +```typescript +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import superjson from "superjson"; +import type { AnyTRPCRouter } from "@trpc/server"; + +// Returns a tRPC client whose fetch is a stub honouring the provided mocks. +// Mocks are keyed by procedure path ("blog.articleBySlug") returning the +// raw response body. +export function createMockTrpcClient( + mocks: Record = {}, +) { + const fetchStub: typeof fetch = async (input) => { + const url = typeof input === "string" ? input : (input as Request).url; + const path = new URL(url, "http://mock").pathname.replace(/^\/api\/trpc\//, ""); + const result = mocks[path]; + if (result === undefined) { + return new Response(JSON.stringify([{ error: { code: -32603, message: `No mock for ${path}` } }]), { status: 200 }); + } + return new Response(JSON.stringify([{ result: { data: superjson.serialize(result) } }]), { status: 200 }); + }; + + return createTRPCClient({ + links: [ + httpBatchLink({ + url: "http://mock/api/trpc", + transformer: superjson, + fetch: fetchStub, + }), + ], + }); +} +``` + +`packages/core-testing/src/react/render-with-providers.tsx`: +```typescript +import type { PropsWithChildren, ReactElement } from "react"; +import { render, type RenderResult } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +export interface RenderOptions { + queryClient?: QueryClient; +} + +export function renderWithProviders( + ui: ReactElement, + options: RenderOptions = {}, +): RenderResult & { queryClient: QueryClient } { + const queryClient = + options.queryClient ?? + new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + const Wrapper = ({ children }: PropsWithChildren) => ( + {children} + ); + + return { ...render(ui, { wrapper: Wrapper }), queryClient }; +} +``` + +`packages/core-testing/src/react/render-with-providers.test.tsx`: +```typescript +import { describe, it, expect } from "vitest"; +import { screen } from "@testing-library/react"; +import { renderWithProviders } from "@/react/render-with-providers"; + +describe("renderWithProviders", () => { + it("renders the child", () => { + renderWithProviders(
hi
); + expect(screen.getByTestId("x")).toBeInTheDocument(); + }); + + it("returns the queryClient instance", () => { + const { queryClient } = renderWithProviders(
); + expect(queryClient).toBeDefined(); + }); +}); +``` + +`packages/core-testing/src/react/index.ts`: +```typescript +export { renderWithProviders, type RenderOptions } from "./render-with-providers.js"; +export { createMockTrpcClient } from "./mock-trpc.js"; +``` + +- [ ] **Step 18: Write top-level barrel** + +`packages/core-testing/src/index.ts`: +```typescript +export * from "./factory/index.js"; +export * from "./contract/index.js"; +``` + +- [ ] **Step 19: Add tsconfig path aliases** + +Edit `tsconfig.base.json` and add to `compilerOptions.paths`: +```json +"@repo/core-testing": ["packages/core-testing/src/index.ts"], +"@repo/core-testing/factory": ["packages/core-testing/src/factory/index.ts"], +"@repo/core-testing/contract": ["packages/core-testing/src/contract/index.ts"], +"@repo/core-testing/react": ["packages/core-testing/src/react/index.ts"], +"@repo/core-testing/payload": ["packages/core-testing/src/payload/index.ts"], +"@repo/core-testing/setup/jsdom": ["packages/core-testing/src/setup/jsdom.ts"], +"@repo/core-testing/setup/node": ["packages/core-testing/src/setup/node.ts"] +``` + +- [ ] **Step 20: Write AGENTS.md** + +`packages/core-testing/AGENTS.md`: +```markdown +# @repo/core-testing + +Shared testing utilities. Tag: `tooling`. May be depended on by any package as a devDependency. + +## Subpath exports + +- `@repo/core-testing/factory` — `defineFactory(builder)` for test data factories +- `@repo/core-testing/contract` — `defineContractSuite(name, suite)` for cross-impl contract tests +- `@repo/core-testing/react` — `renderWithProviders`, `createMockTrpcClient` +- `@repo/core-testing/payload` — `stubPayloadConfig`, `mockPayloadModule` +- `@repo/core-testing/setup/jsdom` — vitest setupFile (jest-dom + cleanup) +- `@repo/core-testing/setup/node` — vitest setupFile (no-op placeholder) + +## Adding a factory + +```typescript +import { defineFactory } from "@repo/core-testing/factory"; + +export const articleFactory = defineFactory
(({ sequence }) => ({ + id: `article-${sequence}`, + title: `Article ${sequence}`, + // stable defaults — overrides drive variation +})); +``` + +## Adding a contract suite + +See `docs/guides/tdd-workflow.md` §"Contract suite usage". +``` + +- [ ] **Step 21: Verify everything** + +```bash +pnpm install +pnpm typecheck --filter @repo/core-testing +pnpm test --filter @repo/core-testing +pnpm lint --filter @repo/core-testing +pnpm turbo boundaries +``` + +Expected: all green; 7 tests pass. + +- [ ] **Step 22: Commit** + +```bash +git add packages/core-testing tsconfig.base.json +git commit -m "feat(core-testing): scaffold shared testing utilities package + +Adds @repo/core-testing (tag: tooling) with: +- factory/defineFactory: monotonic-sequence object factories with overrides +- contract/defineContractSuite: shared test suites runnable against multiple impls +- react/renderWithProviders + createMockTrpcClient: RTL helpers +- payload/stubPayloadConfig + mockPayloadModule: Payload mocking helpers +- setup/{jsdom,node}: vitest setup files + +Spec: docs/superpowers/specs/2026-05-05-tdd-foundation-design.md §5" +``` + +--- + +### Task 2: Vitest base configs (jsdom + node) in core-typescript + +**Files:** +- Modify: `packages/core-typescript/vitest.base.ts` → split into node/jsdom +- Create: `packages/core-typescript/vitest.base.node.ts` +- Create: `packages/core-typescript/vitest.base.jsdom.ts` +- Modify: `packages/core-typescript/package.json` (exports) +- Modify: 5 feature `vitest.config.ts` to use new base + alias +- Modify: `packages/core-shared/vitest.config.ts` + +- [ ] **Step 1: Write failing test for the new node base** + +`packages/core-typescript/vitest.base.node.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import { nodeVitestConfig } from "./vitest.base.node"; + +describe("nodeVitestConfig", () => { + it("uses node environment", () => { + expect(nodeVitestConfig.test?.environment).toBe("node"); + }); + it("enables clearMocks, restoreMocks, mockReset", () => { + expect(nodeVitestConfig.test?.clearMocks).toBe(true); + expect(nodeVitestConfig.test?.restoreMocks).toBe(true); + expect(nodeVitestConfig.test?.mockReset).toBe(true); + }); + it("declares coverage thresholds", () => { + expect(nodeVitestConfig.test?.coverage?.thresholds).toMatchObject({ + statements: 80, branches: 75, functions: 80, lines: 80, + }); + }); + it("includes src and tests glob", () => { + expect(nodeVitestConfig.test?.include).toEqual( + expect.arrayContaining(["src/**/*.test.ts", "tests/**/*.test.ts"]), + ); + }); + it("excludes factories and contracts from coverage", () => { + expect(nodeVitestConfig.test?.coverage?.exclude).toEqual( + expect.arrayContaining(["src/__factories__/**", "src/__contracts__/**"]), + ); + }); +}); +``` + +- [ ] **Step 2: Run to verify RED** + +```bash +cd packages/core-typescript && pnpm test +``` + +Expected: FAIL — module does not exist. + +- [ ] **Step 3: Implement `vitest.base.node.ts`** + +`packages/core-typescript/vitest.base.node.ts`: +```typescript +import { defineConfig } from "vitest/config"; + +export const nodeVitestConfig = defineConfig({ + test: { + globals: true, + environment: "node", + include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + setupFiles: ["@repo/core-testing/setup/node"], + clearMocks: true, + restoreMocks: true, + mockReset: true, + unstubGlobals: true, + sequence: { shuffle: true }, + coverage: { + provider: "v8", + reporter: ["text", "html", "lcov"], + include: ["src/**"], + exclude: [ + "src/**/*.test.{ts,tsx}", + "src/**/index.ts", + "src/__factories__/**", + "src/__contracts__/**", + ], + thresholds: { + statements: 80, branches: 75, functions: 80, lines: 80, + }, + }, + }, +}); +``` + +- [ ] **Step 4: Write failing test for jsdom base** + +`packages/core-typescript/vitest.base.jsdom.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import { jsdomVitestConfig } from "./vitest.base.jsdom"; + +describe("jsdomVitestConfig", () => { + it("uses jsdom environment", () => { + expect(jsdomVitestConfig.test?.environment).toBe("jsdom"); + }); + it("loads the jsdom setup file", () => { + expect(jsdomVitestConfig.test?.setupFiles).toEqual( + expect.arrayContaining(["@repo/core-testing/setup/jsdom"]), + ); + }); + it("includes tsx files", () => { + expect(jsdomVitestConfig.test?.include).toEqual( + expect.arrayContaining(["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"]), + ); + }); + it("inherits clearMocks from node base", () => { + expect(jsdomVitestConfig.test?.clearMocks).toBe(true); + }); +}); +``` + +- [ ] **Step 5: Run to verify RED** + +```bash +cd packages/core-typescript && pnpm test +``` + +Expected: FAIL — module does not exist. + +- [ ] **Step 6: Implement `vitest.base.jsdom.ts`** + +`packages/core-typescript/vitest.base.jsdom.ts`: +```typescript +import { defineConfig, mergeConfig } from "vitest/config"; +import { nodeVitestConfig } from "./vitest.base.node.js"; + +export const jsdomVitestConfig = mergeConfig( + nodeVitestConfig, + defineConfig({ + test: { + environment: "jsdom", + setupFiles: ["@repo/core-testing/setup/jsdom"], + include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"], + }, + }), +); +``` + +- [ ] **Step 7: Update package.json exports** + +Edit `packages/core-typescript/package.json` exports map to add: +```json +"./vitest.base.node": "./vitest.base.node.ts", +"./vitest.base.jsdom": "./vitest.base.jsdom.ts" +``` +(Keep existing `./vitest.base` for backwards-compat — it should re-export `nodeVitestConfig` as `baseVitestConfig` from `vitest.base.ts`.) + +- [ ] **Step 8: Update legacy `vitest.base.ts`** + +`packages/core-typescript/vitest.base.ts`: +```typescript +// Backwards-compat re-export. New code should import from +// vitest.base.node or vitest.base.jsdom directly. +export { nodeVitestConfig as baseVitestConfig } from "./vitest.base.node.js"; +``` + +- [ ] **Step 9: Add devDependency on @repo/core-testing for the type aliases used by setupFiles** + +Edit `packages/core-typescript/package.json` devDependencies — add `"@repo/core-testing": "workspace:*"`. + +- [ ] **Step 10: Run tests for core-typescript to verify GREEN** + +```bash +pnpm install +cd packages/core-typescript && pnpm test +``` + +Expected: 9/9 PASS. + +- [ ] **Step 11: Migrate feature vitest configs** + +For each of `packages/{auth,blog,marketing-pages,navigation,core-shared}/vitest.config.ts`, replace contents with: + +```typescript +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; + +export default mergeConfig(nodeVitestConfig, { + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, +}); +``` + +- [ ] **Step 12: Run all tests to confirm migration didn't break anything** + +```bash +pnpm test +``` + +Expected: existing 96 tests still pass (some may now flag coverage threshold misses — note them, will fix in Task 12). + +- [ ] **Step 13: Commit** + +```bash +git add packages/core-typescript packages/auth/vitest.config.ts packages/blog/vitest.config.ts packages/marketing-pages/vitest.config.ts packages/navigation/vitest.config.ts packages/core-shared/vitest.config.ts +git commit -m "feat(core-typescript): split vitest base into node + jsdom flavors + +Adds vitest.base.node and vitest.base.jsdom with safety defaults +(clearMocks, restoreMocks, mockReset, unstubGlobals, sequence.shuffle) +and coverage thresholds (80/75/80/80). Migrates all feature configs +to the new base. Existing baseVitestConfig kept as backwards-compat +re-export of nodeVitestConfig. + +Spec: §6.2" +``` + +--- + +### Task 3: Add factories to all 5 features + +**Files:** +- Create: `packages/auth/src/__factories__/user.factory.ts` +- Create: `packages/auth/src/__factories__/user.factory.test.ts` +- Create: `packages/auth/src/__factories__/index.ts` +- Create: `packages/blog/src/__factories__/article.factory.ts` +- Create: `packages/blog/src/__factories__/article.factory.test.ts` +- Create: `packages/blog/src/__factories__/index.ts` +- Create: `packages/marketing-pages/src/__factories__/page.factory.ts` +- Create: `packages/marketing-pages/src/__factories__/page.factory.test.ts` +- Create: `packages/marketing-pages/src/__factories__/site-settings.factory.ts` +- Create: `packages/marketing-pages/src/__factories__/index.ts` +- Create: `packages/navigation/src/__factories__/header.factory.ts` +- Create: `packages/navigation/src/__factories__/header.factory.test.ts` +- Create: `packages/navigation/src/__factories__/index.ts` +- Create: `packages/media/src/__factories__/media.factory.ts` +- Create: `packages/media/src/__factories__/media.factory.test.ts` +- Create: `packages/media/src/__factories__/index.ts` +- Modify: each feature's `package.json` — add `@repo/core-testing` devDependency +- Modify: existing tests in each feature to use factories where it cuts boilerplate (lightweight pass; do not rewrite working tests just for stylistic reasons) + +- [ ] **Step 1: Write failing test for `articleFactory`** + +`packages/blog/src/__factories__/article.factory.test.ts`: +```typescript +import { describe, it, expect, beforeEach } from "vitest"; +import { articleFactory } from "@/__factories__/article.factory"; + +describe("articleFactory", () => { + beforeEach(() => articleFactory.reset()); + it("returns an Article with stable defaults", () => { + const a = articleFactory.build(); + expect(a.title).toBe("Article 1"); + expect(a.slug).toBe("article-1"); + expect(a.status).toBe("draft"); + expect(a.createdAt).toEqual(new Date("2026-01-01T00:00:00Z")); + }); + it("applies overrides", () => { + const a = articleFactory.build({ status: "published", title: "X" }); + expect(a.status).toBe("published"); + expect(a.title).toBe("X"); + }); +}); +``` + +- [ ] **Step 2: Run to verify RED** + +```bash +cd packages/blog && pnpm test +``` + +Expected: FAIL — `article.factory.ts` does not exist. + +- [ ] **Step 3: Implement factory + add devDependency** + +Edit `packages/blog/package.json` to add `"@repo/core-testing": "workspace:*"` under `devDependencies`. Run `pnpm install`. + +`packages/blog/src/__factories__/article.factory.ts`: +```typescript +import { defineFactory } from "@repo/core-testing/factory"; +import type { Article } from "../entities/article.js"; + +export const articleFactory = defineFactory
(({ sequence }) => ({ + id: `article-${sequence}`, + title: `Article ${sequence}`, + slug: `article-${sequence}`, + content: null, + status: "draft", + authorId: "user-1", + createdAt: new Date("2026-01-01T00:00:00Z"), + updatedAt: new Date("2026-01-01T00:00:00Z"), +})); +``` + +`packages/blog/src/__factories__/index.ts`: +```typescript +export { articleFactory } from "./article.factory.js"; +``` + +- [ ] **Step 4: Run to verify GREEN** + +```bash +cd packages/blog && pnpm test +``` + +Expected: existing tests still pass + 2 new factory tests pass. + +- [ ] **Step 5: Repeat steps 1-4 for each remaining factory** + +For each, write a small `*.factory.test.ts` (2-3 tests), then implement: + +`packages/auth/src/__factories__/user.factory.ts`: +```typescript +import { defineFactory } from "@repo/core-testing/factory"; +import type { User } from "../entities/user.js"; + +export const userFactory = defineFactory(({ sequence }) => ({ + id: `user-${sequence}`, + email: `user${sequence}@example.com`, + role: "user", + createdAt: new Date("2026-01-01T00:00:00Z"), + updatedAt: new Date("2026-01-01T00:00:00Z"), +})); +``` +Adjust fields to match the actual `User` entity. Read `packages/auth/src/entities/user.ts` first. + +`packages/marketing-pages/src/__factories__/page.factory.ts`: +```typescript +import { defineFactory } from "@repo/core-testing/factory"; +import type { Page } from "../entities/page.js"; + +export const pageFactory = defineFactory(({ sequence }) => ({ + id: `page-${sequence}`, + slug: `page-${sequence}`, + title: `Page ${sequence}`, + content: null, + createdAt: new Date("2026-01-01T00:00:00Z"), + updatedAt: new Date("2026-01-01T00:00:00Z"), +})); +``` +Read `packages/marketing-pages/src/entities/page.ts` and adapt. + +`packages/marketing-pages/src/__factories__/site-settings.factory.ts`: +```typescript +import { defineFactory } from "@repo/core-testing/factory"; +import type { SiteSettings } from "../entities/site-settings.js"; + +export const siteSettingsFactory = defineFactory(({ sequence }) => ({ + id: `settings-${sequence}`, + siteName: `Site ${sequence}`, + // adapt to actual entity +})); +``` +Read `packages/marketing-pages/src/entities/site-settings.ts` and adapt. + +`packages/navigation/src/__factories__/header.factory.ts`: +```typescript +import { defineFactory } from "@repo/core-testing/factory"; +import type { Header } from "../entities/header.js"; + +export const headerFactory = defineFactory
(({ sequence }) => ({ + id: `header-${sequence}`, + navItems: [], + // adapt to actual entity +})); +``` +Read `packages/navigation/src/entities/header.ts` and adapt. + +`packages/media/src/__factories__/media.factory.ts`: +Media has no entity layer currently — create a minimal `Media` interface in this factory's file or skip if no consumer needs it. Read `packages/media/src` first; if there's no entity, write the factory to return a Payload Media doc shape: +```typescript +import { defineFactory } from "@repo/core-testing/factory"; + +export interface Media { + id: string; + alt: string; + url: string; + filename: string; + mimeType: string; + filesize: number; +} + +export const mediaFactory = defineFactory(({ sequence }) => ({ + id: `media-${sequence}`, + alt: `Media ${sequence}`, + url: `https://cdn.example.com/media-${sequence}.png`, + filename: `media-${sequence}.png`, + mimeType: "image/png", + filesize: 1024, +})); +``` + +- [ ] **Step 6: Refactor existing tests to consume factories where it removes boilerplate** + +In each feature, find tests that inline raw fixture objects (like `packages/blog/src/application/use-cases/get-articles.use-case.test.ts:23-32`) and replace with `articleFactory.build({ overrides })`. Light pass — only edit tests where the change is mechanical and obviously correct. + +Example refactor: +```typescript +// before: +await repo.createArticle({ + id: "1", title: "A", slug: "a", content: null, status: "draft", + authorId: "u1", createdAt: now, updatedAt: now, +}); +// after: +await repo.createArticle(articleFactory.build({ id: "1", title: "A", slug: "a" })); +``` + +- [ ] **Step 7: Run all tests** + +```bash +pnpm test +``` + +Expected: all tests pass; factories add ~10 new tests. + +- [ ] **Step 8: Commit** + +```bash +git add packages/auth packages/blog packages/marketing-pages packages/navigation packages/media +git commit -m "feat(features): add test factories to all 5 features + +Adds src/__factories__/.factory.ts to auth, blog, marketing-pages, +navigation, media. Each factory uses defineFactory from @repo/core-testing +with stable date defaults (2026-01-01) so snapshot diffs reflect SUT +behavior only. Refactors mechanical inline-fixture tests to use factories. + +Spec: §5.1, §6.3" +``` + +--- + +### Task 4: Contract suites for repository interfaces + +**Files:** +- Create: `packages/blog/src/__contracts__/articles-repository.contract.ts` +- Create: `packages/auth/src/__contracts__/users-repository.contract.ts` +- Create: `packages/marketing-pages/src/__contracts__/pages-repository.contract.ts` +- Create: `packages/marketing-pages/src/__contracts__/site-settings-repository.contract.ts` +- Create: `packages/navigation/src/__contracts__/header-repository.contract.ts` +- Modify: each `*.repository.test.ts` (mock + payload impls) to invoke `contract.run()` + +- [ ] **Step 1: Write failing contract test for `IArticlesRepository`** + +`packages/blog/src/__contracts__/articles-repository.contract.ts`: +```typescript +import { it, expect, beforeEach } from "vitest"; +import { defineContractSuite } from "@repo/core-testing/contract"; +import type { IArticlesRepository } from "../application/repositories/articles-repository.interface.js"; +import { articleFactory } from "../__factories__/article.factory.js"; + +export const articlesRepositoryContract = defineContractSuite( + "IArticlesRepository", + ({ buildSubject }) => { + let repo: IArticlesRepository; + beforeEach(async () => { + articleFactory.reset(); + repo = await buildSubject(); + }); + + it("createArticle persists then getArticleBySlug returns it", async () => { + const seed = articleFactory.build({ slug: "x" }); + await repo.createArticle(seed); + const result = await repo.getArticleBySlug("x"); + expect(result?.id).toBe(seed.id); + }); + + it("getArticleBySlug returns undefined for missing slug", async () => { + expect(await repo.getArticleBySlug("does-not-exist")).toBeUndefined(); + }); + + it("listArticles returns all when no filter", async () => { + await repo.createArticle(articleFactory.build()); + await repo.createArticle(articleFactory.build()); + const list = await repo.listArticles(); + expect(list).toHaveLength(2); + }); + + it("listArticles filters by status", async () => { + await repo.createArticle(articleFactory.build({ status: "draft" })); + await repo.createArticle(articleFactory.build({ status: "published" })); + const drafts = await repo.listArticles({ status: "draft" }); + expect(drafts).toHaveLength(1); + expect(drafts[0]?.status).toBe("draft"); + }); + }, +); +``` + +(If the actual `IArticlesRepository` interface differs, adjust assertions to match. Read the interface file first.) + +- [ ] **Step 2: Wire contract into mock impl test** + +Create or edit `packages/blog/src/infrastructure/repositories/mock-articles.repository.test.ts`: +```typescript +import { describe } from "vitest"; +import { MockArticlesRepository } from "./mock-articles.repository"; +import { articlesRepositoryContract } from "../../__contracts__/articles-repository.contract"; + +describe("MockArticlesRepository", () => { + articlesRepositoryContract.run(() => new MockArticlesRepository()); +}); +``` + +- [ ] **Step 3: Run to verify GREEN for mock impl** + +```bash +cd packages/blog && pnpm test mock-articles.repository +``` + +Expected: 4 contract tests pass against MockArticlesRepository. If any fail, the mock has a bug — fix the mock to match the contract. + +- [ ] **Step 4: Wire contract into payload impl test** + +Edit `packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts` — wrap existing impl-specific tests in their own describe and add a contract block. Use `vi.mock('payload')` to back the contract's repo with an in-memory store: + +```typescript +import { describe, vi, beforeEach } from "vitest"; +import { PayloadArticlesRepository } from "./payload-articles.repository"; +import { articlesRepositoryContract } from "../../__contracts__/articles-repository.contract"; +import { stubPayloadConfig } from "@repo/core-testing/payload"; + +// Build an in-memory Payload-shaped store so the same contract suite runs. +function buildPayloadStub() { + const store = new Map(); + return { + create: vi.fn(async ({ data }: { data: { id: string } & Record }) => { + store.set(data.id, data); + return data; + }), + find: vi.fn(async ({ where }: { where?: { slug?: { equals: string } } }) => { + const all = Array.from(store.values()) as Array<{ slug: string }>; + const docs = where?.slug ? all.filter((d) => d.slug === where.slug?.equals) : all; + return { docs }; + }), + findByID: vi.fn(async ({ id }: { id: string }) => store.get(id)), + }; +} + +vi.mock("payload", () => ({ + getPayload: vi.fn(), +})); + +describe("PayloadArticlesRepository", () => { + describe("contract", () => { + articlesRepositoryContract.run(async () => { + const stub = buildPayloadStub(); + const { getPayload } = await import("payload"); + (getPayload as ReturnType).mockResolvedValue(stub); + return new PayloadArticlesRepository(stubPayloadConfig); + }); + }); + + // ... keep existing impl-specific tests (Payload doc → domain mapping, etc.) +}); +``` + +If the existing tests cover specific Payload-doc-to-domain mapping cases that the contract can't hit (e.g., the `author` field becoming `authorId`), keep those as separate `it` blocks alongside the contract. + +- [ ] **Step 5: Run to verify GREEN for payload impl** + +```bash +cd packages/blog && pnpm test payload-articles.repository +``` + +Expected: contract tests pass against PayloadArticlesRepository (with the in-memory stub). + +- [ ] **Step 6: Repeat for the remaining 4 contracts** + +For each of: +- `IUsersRepository` (auth) — mock only currently; contract still defined for future Payload impl +- `IPagesRepository` (marketing-pages) — mock + payload +- `ISiteSettingsRepository` (marketing-pages) — mock + payload +- `IHeaderRepository` (navigation) — mock + payload + +Read the interface, draft the contract suite (4-8 cases per repo), wire into both impls (mock first, then payload via in-memory stub). + +- [ ] **Step 7: Run all tests** + +```bash +pnpm test +``` + +Expected: existing 96 + ~30 new contract assertions pass. + +- [ ] **Step 8: Commit** + +```bash +git add packages/{auth,blog,marketing-pages,navigation}/src/__contracts__ packages/{auth,blog,marketing-pages,navigation}/src/infrastructure/repositories/*.test.ts +git commit -m "feat(features): contract suites for all repository interfaces + +Each repository interface now has a contract suite under +src/__contracts__/. Both Mock and Payload implementations run the +same suite, eliminating mock-vs-real drift. Payload impls back the +contract with an in-memory stub via vi.mock('payload') + a small +buildPayloadStub helper. + +Spec: §5.2, §6.4" +``` + +--- + +### Task 5: Tests + jsdom config for `core-ui` + +**Files:** +- Create: `packages/core-ui/vitest.config.ts` +- Modify: `packages/core-ui/package.json` — add devDeps + scripts +- Create: `packages/core-ui/src/atoms/button/button.test.tsx` +- Create: `packages/core-ui/src/atoms/input/input.test.tsx` +- Create: `packages/core-ui/src/atoms/label/label.test.tsx` +- Create at least one `.test.tsx` per discovered molecule and organism (read `packages/core-ui/src/{molecules,organisms,templates}/` first) + +- [ ] **Step 1: Add devDependencies** + +Edit `packages/core-ui/package.json`: +```json +{ + "scripts": { + "build": "tsc --noEmit", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests" + }, + "devDependencies": { + "@repo/core-eslint": "workspace:*", + "@repo/core-testing": "workspace:*", + "@repo/core-typescript": "workspace:*", + "@storybook/react": "^8.6.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.0", + "@types/react": "^19.0.0", + "jsdom": "^25.0.0", + "vitest": "^3.0.0" + } +} +``` + +Run `pnpm install`. + +- [ ] **Step 2: Create `vitest.config.ts`** + +`packages/core-ui/vitest.config.ts`: +```typescript +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom"; + +export default mergeConfig(jsdomVitestConfig, { + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, +}); +``` + +- [ ] **Step 3: Write failing test for Button** + +`packages/core-ui/src/atoms/button/button.test.tsx`: +```typescript +import { describe, it, expect, vi } from "vitest"; +import { renderWithProviders } from "@repo/core-testing/react"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Button } from "./button"; + +describe("Button", () => { + it("renders children inside a button", () => { + renderWithProviders(); + expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument(); + }); + + it("calls onClick when activated", async () => { + const handleClick = vi.fn(); + renderWithProviders(); + await userEvent.click(screen.getByRole("button", { name: "Go" })); + expect(handleClick).toHaveBeenCalledOnce(); + }); + + it("applies the variant class", () => { + renderWithProviders(); + expect(screen.getByRole("button")).toHaveClass(/destructive/); + }); + + it("applies the size class", () => { + renderWithProviders(); + expect(screen.getByRole("button")).toHaveClass(/h-11/); + }); + + it("disabled prop sets the attribute", () => { + renderWithProviders(); + expect(screen.getByRole("button")).toBeDisabled(); + }); +}); +``` + +- [ ] **Step 4: Run to verify GREEN** + +```bash +cd packages/core-ui && pnpm test +``` + +Expected: 5/5 PASS (Button already exists). + +- [ ] **Step 5: Repeat for Input, Label** + +Read each component file first, then write a small test suite that exercises rendering, props, and one user interaction. + +`packages/core-ui/src/atoms/input/input.test.tsx`: +```typescript +import { describe, it, expect } from "vitest"; +import { renderWithProviders } from "@repo/core-testing/react"; +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Input } from "./input"; + +describe("Input", () => { + it("renders an input element", () => { + renderWithProviders(); + expect(screen.getByPlaceholderText("email")).toBeInTheDocument(); + }); + + it("accepts user input", async () => { + renderWithProviders(); + const input = screen.getByPlaceholderText("email"); + await userEvent.type(input, "hi@example.com"); + expect(input).toHaveValue("hi@example.com"); + }); +}); +``` + +`packages/core-ui/src/atoms/label/label.test.tsx`: +```typescript +import { describe, it, expect } from "vitest"; +import { renderWithProviders } from "@repo/core-testing/react"; +import { screen } from "@testing-library/react"; +import { Label } from "./label"; + +describe("Label", () => { + it("renders the text", () => { + renderWithProviders(); + expect(screen.getByText("Email")).toBeInTheDocument(); + }); +}); +``` + +- [ ] **Step 6: Discover and test molecules + organisms + templates** + +```bash +ls packages/core-ui/src/molecules packages/core-ui/src/organisms packages/core-ui/src/templates +``` + +For each component discovered, create a corresponding `*.test.tsx` with at least: +1. A "renders" smoke test +2. One prop-driven assertion +3. One interaction test if the component handles user events + +Keep tests minimal but real. The goal is establishing the pattern + at least one test per file, not exhaustive coverage in this task. + +- [ ] **Step 7: Run all core-ui tests** + +```bash +cd packages/core-ui && pnpm test +``` + +Expected: all new tests pass. + +- [ ] **Step 8: Commit** + +```bash +git add packages/core-ui +git commit -m "feat(core-ui): add jsdom Vitest config + RTL tests for components + +Adopts jsdomVitestConfig from @repo/core-typescript. Adds +@testing-library/react, @testing-library/user-event, jsdom devDeps. +Writes smoke + interaction tests for every atom/molecule/organism/template +using renderWithProviders from @repo/core-testing/react. + +Spec: §6.1, §6.5" +``` + +--- + +### Task 6: Tests + node config for `core-api`, `core-cms`, `core-trpc` + +**Files:** +- Create: `packages/core-api/vitest.config.ts` +- Create: `packages/core-api/src/router.test.ts` +- Modify: `packages/core-api/package.json` — add test script + devDeps +- Create: `packages/core-cms/vitest.config.ts` +- Create: `packages/core-cms/src/payload.config.test.ts` +- Modify: `packages/core-cms/package.json` — add test script + devDeps +- Create: `packages/core-trpc/vitest.config.ts` +- Create: `packages/core-trpc/src/client.test.ts` +- Modify: `packages/core-trpc/package.json` — add test script + devDeps + +- [ ] **Step 1: Add vitest devDep + script to core-api** + +Edit `packages/core-api/package.json`: +- Scripts: add `"test": "vitest run --passWithNoTests"` +- DevDependencies: add `"@repo/core-testing": "workspace:*"`, `"vitest": "^3.0.0"` + +Run `pnpm install`. + +- [ ] **Step 2: Create vitest.config.ts** + +`packages/core-api/vitest.config.ts`: +```typescript +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; + +export default mergeConfig(nodeVitestConfig, { + resolve: { alias: { "@": path.resolve(__dirname, "./src") } }, +}); +``` + +- [ ] **Step 3: Write failing composition test** + +`packages/core-api/src/router.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import { appRouter } from "./root"; + +describe("appRouter composition", () => { + it("exposes auth, blog, marketingPages, navigation routers", () => { + const procedures = appRouter._def.procedures; + expect(Object.keys(procedures)).toEqual( + expect.arrayContaining(["auth", "blog", "marketingPages", "navigation"]), + ); + }); + + it("blog router has expected procedures", () => { + const blog = (appRouter._def.procedures as Record } }>).blog; + expect(blog._def.procedures).toHaveProperty("articleBySlug"); + expect(blog._def.procedures).toHaveProperty("listArticles"); + }); +}); +``` + +- [ ] **Step 4: Run to verify GREEN** + +```bash +cd packages/core-api && pnpm test +``` + +Expected: 2/2 PASS. If procedure shape differs, adjust assertions to match reality (the goal is verifying composition, not enforcing a specific shape). + +- [ ] **Step 5: Repeat for core-cms** + +Edit `packages/core-cms/package.json` similarly. + +`packages/core-cms/vitest.config.ts`: same pattern. + +`packages/core-cms/src/payload.config.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import config from "./payload.config"; + +describe("payloadConfig composition", () => { + it("registers all feature collections", async () => { + const resolved = await config; + const slugs = resolved.collections?.map((c) => c.slug) ?? []; + expect(slugs).toEqual(expect.arrayContaining(["users", "articles", "pages", "media"])); + }); + + it("registers all feature globals", async () => { + const resolved = await config; + const slugs = resolved.globals?.map((g) => g.slug) ?? []; + expect(slugs).toEqual(expect.arrayContaining(["site-settings", "header"])); + }); +}); +``` + +(`buildConfig` returns either a Promise or value; awaiting works for both.) + +- [ ] **Step 6: Repeat for core-trpc** + +Edit `packages/core-trpc/package.json` similarly. + +`packages/core-trpc/vitest.config.ts`: same pattern, but jsdom (provider tests): +```typescript +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom"; + +export default mergeConfig(jsdomVitestConfig, { + resolve: { alias: { "@": path.resolve(__dirname, "./src") } }, +}); +``` + +`packages/core-trpc/src/client.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import { useTRPC, TRPCProvider } from "./client"; + +describe("core-trpc client exports", () => { + it("exports useTRPC hook", () => { + expect(useTRPC).toBeTypeOf("function"); + }); + it("exports TRPCProvider component", () => { + expect(TRPCProvider).toBeTypeOf("function"); + }); +}); +``` + +Add a more meaningful provider test if the providers/ folder has stable wiring to assert (e.g., assert `httpBatchLink` config includes `superjson`). + +- [ ] **Step 7: Run all tests** + +```bash +pnpm test +``` + +Expected: existing tests + 6+ new core-* tests pass. + +- [ ] **Step 8: Commit** + +```bash +git add packages/core-api packages/core-cms packages/core-trpc +git commit -m "feat(core-*): add Vitest configs + composition tests + +core-api: appRouter exposes all 4 feature routers + blog procedure shape. +core-cms: payloadConfig registers all collections + globals. +core-trpc: client + provider exports verified. + +Spec: §6.1, §6.6" +``` + +--- + +### Task 7: Unit tests for apps + +**Files:** +- Create: `apps/web-next/vitest.config.ts` +- Create: `apps/web-next/src/server/bind-production.test.ts` +- Create: `apps/web-next/src/app/providers.test.tsx` +- Modify: `apps/web-next/package.json` — add test script + devDeps +- Create: `apps/web-tanstack/vitest.config.ts` +- Create: `apps/web-tanstack/src/.test.tsx` +- Modify: `apps/web-tanstack/package.json` — add test script + devDeps +- Create: `apps/cms/vitest.config.ts` +- Create: `apps/cms/src/payload.config.test.ts` (or wherever the config is exported) +- Modify: `apps/cms/package.json` — add test script + devDeps + +- [ ] **Step 1: Add devDeps and script to web-next** + +Edit `apps/web-next/package.json`: +- Scripts: `"test": "vitest run --passWithNoTests"` +- DevDependencies: `"@repo/core-testing": "workspace:*"`, `"vitest": "^3.0.0"`, plus `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`, `jsdom` if not already present. + +Run `pnpm install`. + +- [ ] **Step 2: Create vitest.config.ts** + +`apps/web-next/vitest.config.ts`: +```typescript +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom"; + +export default mergeConfig(jsdomVitestConfig, { + resolve: { alias: { "@": path.resolve(__dirname, "./src") } }, +}); +``` + +- [ ] **Step 3: Write failing test for `bindAllProduction`** + +`apps/web-next/src/server/bind-production.test.ts`: +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("@repo/core-cms", () => ({ default: Promise.resolve({}) })); +vi.mock("@repo/blog/di/bind-production", () => ({ bindProductionBlog: vi.fn() })); +vi.mock("@repo/auth/di/bind-production", () => ({ bindProductionAuth: vi.fn() })); +vi.mock("@repo/marketing-pages/di/bind-production", () => ({ bindProductionMarketingPages: vi.fn() })); +vi.mock("@repo/navigation/di/bind-production", () => ({ bindProductionNavigation: vi.fn() })); + +describe("bindAllProduction", () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it("binds all four feature production repos", async () => { + const { bindAllProduction } = await import("./bind-production"); + const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); + const { bindProductionAuth } = await import("@repo/auth/di/bind-production"); + const { bindProductionMarketingPages } = await import("@repo/marketing-pages/di/bind-production"); + const { bindProductionNavigation } = await import("@repo/navigation/di/bind-production"); + + await bindAllProduction(); + + expect(bindProductionBlog).toHaveBeenCalledOnce(); + expect(bindProductionAuth).toHaveBeenCalledOnce(); + expect(bindProductionMarketingPages).toHaveBeenCalledOnce(); + expect(bindProductionNavigation).toHaveBeenCalledOnce(); + }); + + it("is idempotent — second call does not re-bind", async () => { + const { bindAllProduction } = await import("./bind-production"); + const { bindProductionBlog } = await import("@repo/blog/di/bind-production"); + await bindAllProduction(); + await bindAllProduction(); + expect(bindProductionBlog).toHaveBeenCalledOnce(); + }); +}); +``` + +- [ ] **Step 4: Run to verify GREEN** + +```bash +cd apps/web-next && pnpm test +``` + +Expected: 2/2 PASS. + +- [ ] **Step 5: Write failing providers test** + +Read `apps/web-next/src/app/providers.tsx` to understand its shape, then: + +`apps/web-next/src/app/providers.test.tsx`: +```typescript +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { Providers } from "./providers"; + +describe("Providers", () => { + it("renders children", () => { + render( + +
hi
+
, + ); + expect(screen.getByTestId("child")).toBeInTheDocument(); + }); +}); +``` + +If `Providers` requires server-side data (e.g., a tRPC initial state), pass it as a prop or mock the dependency. + +- [ ] **Step 6: Run to verify GREEN** + +```bash +cd apps/web-next && pnpm test +``` + +Expected: PASS. + +- [ ] **Step 7: Repeat for web-tanstack and cms** + +For `apps/web-tanstack`: +- vitest.config.ts (jsdom) +- One test asserting the providers wire `QueryClientProvider` +- One test asserting the equivalent of bindAllProduction (if applicable) + +For `apps/cms`: +- vitest.config.ts (node) +- A `payload.config.test.ts` that asserts the exported config has expected collections/globals (similar to core-cms but with the app-level config) + +- [ ] **Step 8: Run all tests** + +```bash +pnpm test +``` + +Expected: all tests pass. + +- [ ] **Step 9: Commit** + +```bash +git add apps/web-next apps/web-tanstack apps/cms +git commit -m "feat(apps): add unit tests for providers + bind-production + cms config + +web-next: bindAllProduction calls all 4 feature binders exactly once; +Providers renders children. web-tanstack: equivalent providers + bind tests. +cms: payload.config exports a SanitizedConfig with all expected collections. + +Spec: §6.7, §9" +``` + +--- + +### Task 8: Storybook test-runner integration + +**Files:** +- Modify: `apps/storybook/package.json` — add `@storybook/test-runner`, playwright, scripts +- Create: `apps/storybook/test-runner.config.ts` +- Create: `apps/storybook/.storybook/test-runner.test.ts` (smoke check the test-runner config loads) +- Modify: root `package.json` — add `test:stories` script +- Modify: root `turbo.json` — add `test-storybook` task + +- [ ] **Step 1: Add devDeps** + +Edit `apps/storybook/package.json` devDependencies: +```json +{ + "@storybook/test-runner": "^0.21.0", + "playwright": "^1.50.0", + "concurrently": "^9.0.0", + "http-server": "^14.0.0", + "wait-on": "^8.0.0" +} +``` + +And scripts: +```json +{ + "scripts": { + "build-storybook": "storybook build", + "test-storybook": "test-storybook --url http://localhost:6006", + "test:stories": "concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'pnpm exec http-server storybook-static --port 6006 --silent' 'pnpm exec wait-on tcp:6006 && pnpm test-storybook'" + } +} +``` + +Run `pnpm install`. + +- [ ] **Step 2: Create test-runner.config.ts** + +`apps/storybook/test-runner.config.ts`: +```typescript +import type { TestRunnerConfig } from "@storybook/test-runner"; + +const config: TestRunnerConfig = { + async preVisit(page) { + page.on("console", (msg) => { + if (msg.type() === "error") { + throw new Error(`Console error in story: ${msg.text()}`); + } + }); + }, +}; + +export default config; +``` + +- [ ] **Step 3: Add test script to root package.json** + +Edit root `package.json` scripts: +```json +{ + "scripts": { + "test:stories": "turbo run test:stories" + } +} +``` + +- [ ] **Step 4: Add task to root turbo.json** + +Edit root `turbo.json`: +```json +{ + "tasks": { + "test:stories": { + "dependsOn": ["build-storybook"], + "cache": false + }, + "build-storybook": { + "outputs": ["storybook-static/**"] + } + } +} +``` + +- [ ] **Step 5: Verify the storybook test runner works locally** + +```bash +pnpm install +pnpm exec playwright install --with-deps chromium +pnpm build-storybook --filter @repo/storybook +cd apps/storybook && pnpm test:stories +``` + +Expected: every story mounts without console errors. If any story fails, fix it (or document as a known issue with a TODO). + +- [ ] **Step 6: Commit** + +```bash +git add apps/storybook package.json turbo.json +git commit -m "feat(storybook): wire @storybook/test-runner for story smoke tests + +Every story is now executed as a smoke test (mount + no console errors) +via @storybook/test-runner. New script: pnpm test:stories runs +build-storybook then test-storybook against the static build. + +Spec: §6.8" +``` + +--- + +### Task 9: Documentation — `tdd-workflow.md` + restructure `adding-a-feature.md` + +**Files:** +- Create: `docs/guides/tdd-workflow.md` +- Modify: `docs/guides/adding-a-feature.md` — interleave tests with implementation +- Modify: `docs/guides/testing-strategy.md` — cross-link to tdd-workflow +- Modify: `AGENTS.md` — link both guides +- Modify: `CLAUDE.md` — add TDD subsection under Quick Start + +- [ ] **Step 1: Write `docs/guides/tdd-workflow.md`** + +Sections required (no placeholders): + +1. **Header + intent** (~50 words on why TDD here, not as dogma) +2. **The cycle** — Red, Green, Refactor with a fully worked blog example: write failing `getArticleBySlug` test → run → see fail → implement → run → see pass → refactor +3. **Test naming** — `describe(SubjectUnderTest)` + `it("does X when Y")` with 3 examples +4. **AAA** — Arrange / Act / Assert with two examples (use case + component) +5. **When to mock — decision tree** as a DOT graph block: + ``` + Pure function? → no mock + Use case? → rebind repository at DI level + Repository (Payload)?→ vi.mock('payload') + stubPayloadConfig + Component with data? → renderWithProviders with mocks + Route handler? → mock at boundary only + ``` +6. **Test pyramid for this monorepo** — table with ratios (entities ≥ use-cases ≥ controllers ≥ feature integration ≥ component ≥ e2e) +7. **What NOT to test** — bullet list (getters/setters, framework code, third-party libs, types-only modules, generated code) +8. **Coverage targets** — 80/75/80/80 baseline, 100% in entities/use-cases/controllers, how to inspect locally +9. **Factory usage** — when to call `factory.build()` vs hand-crafted object; how to add a new factory +10. **Contract suite usage** — how to add a new repo impl: write impl → run contract → fix until green; example with code +11. **Running tests** — watch mode, focused tests (`it.only`), debugging failures, `--coverage`, `pnpm test:stories`, `pnpm test:e2e` + +(Each section short and concrete; no "TODO" or "see elsewhere" without an actual link.) + +- [ ] **Step 2: Restructure `adding-a-feature.md`** + +Replace Part 2 ("Build the Layers") with an interleaved test-first sequence. Each layer becomes red→green: + +```markdown +### Step 1: Write failing test for entity schema + +Create `packages//src/entities/.test.ts`: +[concrete failing test code] + +Run: `pnpm test --filter @repo/` +Expected: FAIL — entity does not exist. + +### Step 2: Implement entity to pass +[concrete entity code] + +Run: `pnpm test --filter @repo/` +Expected: PASS. + +### Step 3: Write factory in src/__factories__/.factory.ts +[concrete factory code] + +### Step 4: Write failing test for use case +[concrete use case test using factory] + +### Step 5: Implement use case to pass +[concrete use case code] + +### Step 6: Write contract suite in src/__contracts__/-repository.contract.ts +[concrete contract code] + +### Step 7: Implement Mock repo, run contract +[concrete mock code] + +### Step 8: Implement Payload repo, run same contract +[concrete payload code with vi.mock setup] + +### Step 9: Write failing controller test +[concrete controller test] + +### Step 10: Implement controller to pass +[concrete controller code] + +### Step 11: Write failing tRPC integration test +[concrete tests/.feature.test.ts code] + +### Step 12: Wire router to pass +[concrete router code] + +### Step 13: (UI optional) Write failing component test +### Step 14: Implement component to pass +### Step 15: Wire into core-api / core-cms, run typecheck + lint + boundaries +``` + +Add at the top: +```markdown +> **TDD Order Required:** You may not advance to the next layer until the +> current layer's tests are red, then green. See `docs/guides/tdd-workflow.md`. +``` + +- [ ] **Step 3: Cross-link** + +Edit `docs/guides/testing-strategy.md` — add a sentence at the top: `For the *how* of TDD (red-green-refactor cycle, when to mock, what NOT to test), see ./tdd-workflow.md. This document covers test *placement* and infrastructure.` + +Edit `AGENTS.md` — under "Specification & Guides", add bullet: `**TDD Workflow** — docs/guides/tdd-workflow.md — red-green-refactor cycle, mocking decision tree, coverage targets`. + +Edit `CLAUDE.md` — under "Quick Start", add: +```markdown +## TDD + +```bash +pnpm test --watch --filter @repo/ # watch one feature +pnpm test -- --coverage # full run with coverage +pnpm test:stories # Storybook smoke tests +pnpm test:e2e # Playwright e2e +``` + +See `docs/guides/tdd-workflow.md` for the full cycle. +``` + +- [ ] **Step 4: Commit** + +```bash +git add docs/guides/tdd-workflow.md docs/guides/adding-a-feature.md docs/guides/testing-strategy.md AGENTS.md CLAUDE.md +git commit -m "docs(guides): add TDD workflow + restructure adding-a-feature for TDD order + +New: docs/guides/tdd-workflow.md — red-green-refactor cycle, AAA, +mocking decision tree, coverage targets, factory + contract usage. +Restructured: adding-a-feature.md interleaves tests with implementation; +TDD order is required, not optional. testing-strategy.md cross-links +the new guide. AGENTS.md and CLAUDE.md surface both. + +Spec: §7" +``` + +--- + +### Task 10: ADR-011 + per-feature AGENTS.md updates + +**Files:** +- Create: `docs/decisions/adr-011-tdd-foundation.md` +- Modify: `AGENTS.md` — add `@repo/core-testing` row to package map +- Modify: each feature `AGENTS.md` — add "Tests" section +- Modify: `packages/core-testing/AGENTS.md` (already created in Task 1; verify complete) + +- [ ] **Step 1: Write ADR-011** + +`docs/decisions/adr-011-tdd-foundation.md`: +```markdown +# ADR-011: TDD Foundation + +**Status:** Accepted +**Date:** 2026-05-05 +**Supersedes:** none +**Spec:** docs/superpowers/specs/2026-05-05-tdd-foundation-design.md + +## Context + +The vertical-feature monorepo refactor (ADRs 001-010) established +clean architecture with per-feature DI containers but did not enforce +TDD as the path of least resistance. Agentic workers were producing +code-first commits with tests added later, leading to test theatre and +mock/real drift. + +## Decision + +1. **New package `@repo/core-testing` (tag: tooling)** — shared test + utilities: defineFactory, defineContractSuite, renderWithProviders, + mock-payload helpers, jsdom setup file. Tagged `tooling` so any + package may depend on it as devDependency without boundary violation. + +2. **Vitest base configs split into node + jsdom** with safety defaults + (clearMocks, restoreMocks, mockReset, unstubGlobals, sequence.shuffle) + and coverage thresholds (80/75/80/80 baseline; 100% in entities + + use-cases + controllers). + +3. **Factories per feature** in `src/__factories__/` replace inline + fixtures. Stable date defaults (2026-01-01) so snapshot diffs reflect + SUT behavior only. + +4. **Contract suites per repository interface** in `src/__contracts__/` + run against every implementation (Mock + Payload). Eliminates the + class of bug where the mock and the real impl drift apart. + +5. **Tests in core-* packages and apps** — composition smoke tests + (appRouter, payloadConfig, bind-production, providers). + +6. **Storybook test-runner** — every story executed as a smoke test. + +7. **CI workflow** — typecheck + lint + boundaries + test + build + + e2e + storybook on every PR. Coverage uploaded as artifact. + +8. **Two new docs** — tdd-workflow.md (process) + restructured + adding-a-feature.md (interleaves tests with impl). + +## Alternatives considered + +- **Fixture files instead of factories** — rejected. Fixtures rot with + schema changes and require manual updates per test. +- **One shared test file per impl** — rejected. Contract suites give + the same coverage in fewer LOC and prevent drift. +- **Real Postgres in tests via testcontainers** — rejected for unit + tests (slow, complex). Repository contract suites + vi.mock('payload') + give equivalent confidence in milliseconds. +- **Stryker mutation testing** — deferred. Coverage thresholds + contract + suites get us most of the way; mutation testing is incremental. + +## Consequences + +- New package to maintain (small, mostly stable surface). +- Coverage thresholds may fail builds initially; we add tests to cross + threshold as part of Plan 7. +- Sequence shuffle may surface latent flakes; we fix as found. +- Templates for new features now require writing tests first; this is + by design. + +## Refines + +- ADR-006 (boundary tags) — adds @repo/core-testing as a tooling package. +``` + +- [ ] **Step 2: Update root AGENTS.md package map** + +In `AGENTS.md`, add row to the Package Map table: +```markdown +| `@repo/core-testing` | tooling | Shared test utilities (defineFactory, defineContractSuite, renderWithProviders, payload mocks) | +``` + +Update the "Five tags" section: `tooling (3 packages) — packages/core-eslint, core-typescript, core-testing`. + +- [ ] **Step 3: Add Tests section to each feature AGENTS.md** + +For each `packages//AGENTS.md`, add a section near the bottom: +```markdown +## Tests + +- **Factories:** `src/__factories__/.factory.ts` — use `factoryName.build({ overrides })` to construct test data with stable defaults. +- **Contract suite:** `src/__contracts__/-repository.contract.ts` — runs against every repository implementation (mock + payload). +- **Unit tests:** colocated as `*.test.ts` next to the source file. +- **Feature integration:** `tests/.feature.test.ts` — full slice through tRPC router → controller → use case → mock repo. + +```bash +pnpm test --filter @repo/ # all tests for this feature +pnpm test --filter @repo/ -- --watch # watch mode +``` + +See `docs/guides/tdd-workflow.md` for the cycle. +``` + +- [ ] **Step 4: Verify core-testing AGENTS.md is complete** + +The file was created in Task 1; ensure it documents factory + contract usage. + +- [ ] **Step 5: Commit** + +```bash +git add docs/decisions/adr-011-tdd-foundation.md AGENTS.md packages/*/AGENTS.md +git commit -m "docs(adr): ADR-011 TDD foundation; update AGENTS.md per-feature + +Captures the decision to add @repo/core-testing, factories, contract +suites, vitest safety defaults, coverage thresholds, Storybook +test-runner, and CI as one cohesive TDD foundation. Per-feature +AGENTS.md gains a Tests section pointing to factories, contract suite, +and the canonical test commands. + +Spec: §7.4, §7.5" +``` + +--- + +### Task 11: CI workflow + +**Files:** +- Create: `.github/workflows/ci.yml` + +- [ ] **Step 1: Create `.github/workflows/ci.yml`** + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + CI: true + +jobs: + validate: + name: typecheck + lint + boundaries + test + build + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: cms_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + - run: pnpm lint + - run: pnpm turbo boundaries + - name: Test with coverage + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/cms_test + PAYLOAD_SECRET: test-secret-do-not-use-in-prod + run: pnpm test -- --coverage + - run: pnpm build + - uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage + path: '**/coverage/lcov.info' + retention-days: 7 + + e2e: + name: Playwright e2e + needs: validate + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: cms_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium + - name: Run e2e + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/cms_test + PAYLOAD_SECRET: test-secret-do-not-use-in-prod + run: pnpm test:e2e + + storybook: + name: Storybook smoke tests + needs: validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm build-storybook --filter @repo/storybook + - run: pnpm test:stories +``` + +- [ ] **Step 2: Commit** + +```bash +mkdir -p .github/workflows +git add .github/workflows/ci.yml +git commit -m "ci: add GitHub Actions workflow + +Runs typecheck + lint + boundaries + test (with coverage) + build +on every push to main and every PR. Postgres service for tests that +need DB. Playwright e2e and Storybook smoke tests gated on validate +job passing. Coverage uploaded as artifact (lcov format) for downstream +tools (Codecov, etc.) — wiring left to template users. + +Spec: §6.11" +``` + +--- + +### Task 12: Tighten coverage thresholds where suites are mature + +**Files:** +- Modify: `packages/{auth,blog,marketing-pages,navigation}/vitest.config.ts` — add per-directory thresholds + +- [ ] **Step 1: Run coverage to see current state** + +```bash +pnpm test -- --coverage +``` + +Expected: each feature reports coverage. Note any features below 80/75/80/80 baseline. + +- [ ] **Step 2: Add per-directory thresholds for each feature** + +For each feature where `entities/`, `use-cases/`, and `controllers/` are at 100%: + +```typescript +import path from "node:path"; +import { mergeConfig } from "vitest/config"; +import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; + +export default mergeConfig(nodeVitestConfig, { + test: { + coverage: { + thresholds: { + "src/entities/**": { statements: 100, branches: 100, functions: 100, lines: 100 }, + "src/application/use-cases/**": { statements: 100, branches: 95, functions: 100, lines: 100 }, + "src/interface-adapters/controllers/**": { statements: 100, branches: 95, functions: 100, lines: 100 }, + statements: 80, branches: 75, functions: 80, lines: 80, + }, + }, + }, + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, +}); +``` + +If a directory is below threshold, either add tests to cross threshold or document why (e.g., generated types) and exclude. + +- [ ] **Step 3: Run coverage again** + +```bash +pnpm test -- --coverage +``` + +Expected: all thresholds met. + +- [ ] **Step 4: Commit** + +```bash +git add packages/*/vitest.config.ts +git commit -m "test: enforce per-directory coverage thresholds + +entities + use-cases + controllers must hit 100% (95% branches). +Project-wide baseline remains 80/75/80/80. Tightening these directories +reflects the architectural intent: these are the pure-logic layers and +should be exhaustively tested. + +Spec: §6.9" +``` + +--- + +## Final verification + +After Task 12 commits: + +```bash +pnpm install +pnpm typecheck +pnpm lint +pnpm turbo boundaries +pnpm test -- --coverage +pnpm build +pnpm exec playwright install --with-deps chromium +pnpm test:e2e +pnpm build-storybook --filter @repo/storybook +pnpm test:stories +``` + +Expected: all green. + +## Self-review (run after writing the plan, before execution) + +- [x] Spec coverage: all 10 gaps mapped to tasks (1→T1+T2+core-testing setup, 2→T1+T5, 3→T9, 4→T1+T3, 5→T1+T4, 6→T2, 7→T9, 8→T8, 9→T7, 10→T11) +- [x] No placeholders — every step has concrete code or commands +- [x] Type consistency — `defineFactory`, `defineContractSuite`, `renderWithProviders`, `mockPayloadModule` named identically across all tasks +- [x] Each task is independently committable + +## Execution + +Per `superpowers:subagent-driven-development`: dispatch one implementer subagent per task with the task text + scene-setting context, then spec compliance review, then code quality review, then proceed.