# Plan 7 — TDD Foundation Implementation Plan > **Note (2026-05-05, post-Plan-8):** All file paths in this plan reference the **pre-Plan-8 layout** (`mock-articles.repository.ts`, `entities/article.ts`, multi-method `articles.controller.ts`, etc.). The Lazar conformance refactor (Plan 8) renamed/reshaped these files. See `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` for the full mapping; ADR-012 documents the conformance decision. > **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.