From a21935c3aa747a23424cd79fe8eb399614f8bc1f Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 5 May 2026 12:46:53 +0200 Subject: [PATCH] =?UTF-8?q?docs(spec):=20TDD=20foundation=20design=20?= =?UTF-8?q?=E2=80=94=20close=2010=20gaps=20for=20agentic=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Catalogues the ten gaps that prevent reliable TDD by agents and humans in this monorepo, and specifies the closure plan: a new @repo/core-testing package (factories + contract suites + RTL helpers + payload mocks), jsdom + safety defaults in core-typescript Vitest bases, tests in core-* and apps, Storybook test-runner, coverage thresholds, CI workflow, and two new docs (tdd-workflow.md plus a restructured adding-a-feature.md that interleaves tests with implementation). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/2026-05-05-tdd-foundation-design.md | 667 ++++++++++++++++++ 1 file changed, 667 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-05-tdd-foundation-design.md diff --git a/docs/superpowers/specs/2026-05-05-tdd-foundation-design.md b/docs/superpowers/specs/2026-05-05-tdd-foundation-design.md new file mode 100644 index 0000000..a791f79 --- /dev/null +++ b/docs/superpowers/specs/2026-05-05-tdd-foundation-design.md @@ -0,0 +1,667 @@ +# TDD Foundation — Design Spec + +**Date:** 2026-05-05 +**Status:** Approved +**Author:** Claude Opus 4.7 (1M context) +**Reviewer:** Danijel +**Supersedes:** none — extends `2026-04-21-vertical-monorepo-refactor-design.md` §13B + +--- + +## 1. Goal + +Make full TDD the path of least resistance in this monorepo so that agentic workers (and humans) cannot produce production code without first writing a failing test. Close the ten gaps catalogued in §3. + +The success criterion is operational, not aspirational: a fresh subagent given any plan task in this template should hit a failing test before they touch production code, and a human reviewer should be able to read a single guide (`docs/guides/tdd-workflow.md`) and the per-feature `AGENTS.md` to understand why each test exists. + +## 2. Non-goals + +- Mutation testing (deferred — Tier 3) +- Visual regression beyond Storybook smoke (deferred — Tier 3) +- Property-based testing (deferred) +- Replacing existing passing tests (we extend the test suite, we do not rewrite working tests for stylistic reasons) +- Docker-in-CI for full Payload integration tests (mock at `payload` module boundary instead — much faster, and the contract suite catches drift) + +## 3. The ten gaps + +| # | Gap | Closed by | +|---|-----|-----------| +| 1 | Zero tests in `core-ui`, `core-api`, `core-cms`, `core-trpc` | §6.5, §6.6 | +| 2 | No React component testing infrastructure | §5, §6.5 | +| 3 | No TDD process documentation (only placement docs exist) | §7.1 | +| 4 | Inline fixtures duplicated across tests; no factories | §5.1, §6.3 | +| 5 | No contract tests — Mock and Payload repos drift independently | §5.2, §6.4 | +| 6 | Vitest base lacks safety defaults (`clearMocks`, `restoreMocks`, jsdom env) | §6.2 | +| 7 | `adding-a-feature.md` does not enforce TDD order | §7.2 | +| 8 | Storybook stories are not executed as tests | §6.8 | +| 9 | Apps have no unit tests (route handlers, providers, bind-production wiring) | §6.7 | +| 10 | No CI — tests can rot silently | §6.11 | + +## 4. Architecture overview + +Three concentric layers of TDD support: + +1. **Tooling layer** — `core-typescript` and `core-eslint` provide configs every package inherits. +2. **Test-utils layer** — a new package `@repo/core-testing` (tag: `tooling`) provides shared helpers: `defineFactory`, `defineContractSuite`, `renderWithProviders`, mock-payload helper, mock-trpc helper, jsdom setup file. +3. **Per-package convention** — every feature/core package follows the same skeleton: factories in `src/__factories__/`, contract suites in `src/__contracts__/`, colocated `*.test.{ts,tsx}` for units, `tests/*.feature.test.ts` for integration. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ apps/* (web-next, web-tanstack, cms, storybook) │ +│ ├─ vitest.config.ts (jsdom) │ +│ ├─ src/**/*.test.{ts,tsx} ← unit + RSC smoke │ +│ └─ e2e/*.spec.ts ← Playwright (existing) │ +├─────────────────────────────────────────────────────────────┤ +│ packages//* (auth, blog, media, marketing-pages, │ +│ navigation) │ +│ ├─ vitest.config.ts (node by default; jsdom for ui/) │ +│ ├─ src/__factories__/*.factory.ts │ +│ ├─ src/__contracts__/*-repository.contract.ts │ +│ ├─ src/**/*.test.ts ← unit │ +│ └─ tests/*.feature.test.ts ← feature integration │ +├─────────────────────────────────────────────────────────────┤ +│ packages/core-* (shared, ui, api, cms, trpc) │ +│ ├─ vitest.config.ts (jsdom for core-ui; node for rest) │ +│ └─ src/**/*.test.{ts,tsx} ← composition smoke │ +├─────────────────────────────────────────────────────────────┤ +│ packages/core-testing (NEW, tooling tag) │ +│ ├─ factory/define-factory │ +│ ├─ contract/define-contract-suite │ +│ ├─ react/render-with-providers, mock-trpc │ +│ ├─ payload/mock-payload-module, stub-config │ +│ └─ setup/{jsdom,node} │ +├─────────────────────────────────────────────────────────────┤ +│ packages/core-typescript (tooling) │ +│ ├─ vitest.base.node.ts ← safety defaults + coverage │ +│ └─ vitest.base.jsdom.ts ← extends node + jsdom + setup │ +└─────────────────────────────────────────────────────────────┘ +``` + +Boundary rules unchanged. `core-testing` is `tooling`, so any package may depend on it (devDependency only). + +## 5. New package: `@repo/core-testing` + +### 5.1 `factory/` + +```typescript +// define-factory.ts +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 deepMerge(base, overrides ?? {}) as T; + }, + buildList(count, overrides) { + return Array.from({ length: count }, () => this.build(overrides)); + }, + reset() { + sequence = 0; + }, + }; +} +``` + +Required factories per feature (created in plan): + +| Feature | Factories | +|---------|-----------| +| `auth` | `userFactory`, `sessionFactory` | +| `blog` | `articleFactory` | +| `media` | `mediaFactory` | +| `marketing-pages` | `pageFactory`, `siteSettingsFactory` | +| `navigation` | `headerFactory`, `navItemFactory` | + +Mock repositories are refactored to consume factories where they need defaults — eliminates per-test inline fixture duplication. + +### 5.2 `contract/` + +```typescript +// define-contract-suite.ts +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 }); + }); + }, + }; +} +``` + +Repository interfaces with multiple implementations get a contract suite. The same suite runs against the Mock impl AND the Payload impl (with `vi.mock('payload')`). Required for: + +- `IArticlesRepository` (Mock + Payload) +- `IUsersRepository` (Mock + Payload) +- `ISessionsRepository` (Mock + Payload) +- `IMediaRepository` (Mock + Payload) +- `IPagesRepository` (Mock + Payload) +- `ISiteSettingsRepository` (Mock + Payload) +- `IHeaderRepository` (Mock + Payload) + +### 5.3 `react/` + +```typescript +// render-with-providers.tsx +export interface RenderOptions { + trpc?: { mocks?: Record }; + 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 trpcClient = createMockTrpcClient(options.trpc?.mocks ?? {}); + const Wrapper = ({ children }: PropsWithChildren) => ( + + + {children} + + + ); + return { ...render(ui, { wrapper: Wrapper }), queryClient }; +} +``` + +`createMockTrpcClient` is a thin wrapper that returns hardcoded data per procedure path; full type inference preserved via the `AppRouter` type from `@repo/core-api`. + +### 5.4 `payload/` + +```typescript +// mock-payload-module.ts +export function mockPayloadModule(impl: Partial): void { + vi.mock('payload', () => ({ + getPayload: vi.fn().mockResolvedValue(impl), + })); +} + +// stub-config.ts +export const stubPayloadConfig = {} as SanitizedConfig; +``` + +### 5.5 `setup/` + +```typescript +// jsdom.ts +import '@testing-library/jest-dom/vitest'; +import { afterEach } from 'vitest'; +import { cleanup } from '@testing-library/react'; + +afterEach(() => { + cleanup(); +}); + +// node.ts +// Currently a no-op; reserved for future global setup. +export {}; +``` + +### 5.6 Package shape + +``` +packages/core-testing/ +├── src/ +│ ├── factory/ +│ │ ├── define-factory.ts +│ │ ├── define-factory.test.ts +│ │ └── index.ts +│ ├── contract/ +│ │ ├── define-contract-suite.ts +│ │ ├── define-contract-suite.test.ts +│ │ └── index.ts +│ ├── react/ +│ │ ├── render-with-providers.tsx +│ │ ├── render-with-providers.test.tsx +│ │ ├── mock-trpc.ts +│ │ └── index.ts +│ ├── payload/ +│ │ ├── mock-payload-module.ts +│ │ ├── stub-config.ts +│ │ └── index.ts +│ ├── setup/ +│ │ ├── jsdom.ts +│ │ └── node.ts +│ └── index.ts +├── package.json # tag: tooling, exports: ./factory, ./contract, ./react, ./payload, ./setup/jsdom, ./setup/node +├── tsconfig.json +├── vitest.config.ts +└── AGENTS.md +``` + +`package.json` exports map: + +```json +{ + "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" + } +} +``` + +## 6. Closing each gap + +### 6.1 Tests in core packages (Gap 1) + +- **`core-ui`** — jsdom env. Add one `.test.tsx` per primitive (Button, Input, Card, etc. — discovered during execution). Use RTL + `renderWithProviders` (no providers needed for primitives, but pattern uniform). +- **`core-api`** — node env. Test that `appRouter` exposes the expected feature routers (`appRouter._def.procedures` walked recursively). +- **`core-cms`** — node env. Test that the composed Payload config has expected collections + globals (`config.collections.map(c => c.slug)`). +- **`core-trpc`** — node env (provider tests use jsdom). Test that the client factory wires `httpBatchLink` + `superjson`. Use `msw` or fetch stub. + +### 6.2 React component testing (Gap 2) + +`core-typescript` exports two configs: + +```typescript +// vitest.base.node.ts +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 }, + }, + }, +}); + +// vitest.base.jsdom.ts +import { mergeConfig } from 'vitest/config'; +import { nodeVitestConfig } from './vitest.base.node'; + +export const jsdomVitestConfig = mergeConfig(nodeVitestConfig, defineConfig({ + test: { + environment: 'jsdom', + setupFiles: ['@repo/core-testing/setup/jsdom'], + include: ['src/**/*.test.{ts,tsx}', 'tests/**/*.test.{ts,tsx}'], + }, +})); +``` + +### 6.3 Factories (Gap 4) + +See §5.1. One factory file per entity, in `src/__factories__/`. Factories deliver stable defaults so snapshot diffs only reflect the SUT's behavior. `__factories__` excluded from coverage. + +### 6.4 Contract suites (Gap 5) + +See §5.2. One contract suite per repository interface, run against every implementation. Each repo `*.test.ts` becomes ~5 lines: `vi.mock('payload', ...); contractSuite.run(() => new RealRepo(stubConfig));`. Specific edge cases unique to one impl (e.g., Payload doc → domain mapping) stay in the impl-specific test file alongside the contract run. + +### 6.5 jsdom + RTL (Gap 2 + Gap 1) + +`core-ui` adopts `jsdomVitestConfig`. Test pattern: + +```typescript +// packages/core-ui/src/atoms/button/button.test.tsx +import { describe, it, expect } from 'vitest'; +import { renderWithProviders } from '@repo/core-testing/react'; +import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; +import { Button } from './button'; + +describe('Button', () => { + it('renders the label', () => { + 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')); + expect(handleClick).toHaveBeenCalledOnce(); + }); +}); +``` + +### 6.6 Composition tests for `core-api`, `core-cms`, `core-trpc` (Gap 1) + +```typescript +// packages/core-api/src/router.test.ts +describe('appRouter composition', () => { + it('exposes blog router', () => { + expect(appRouter._def.procedures).toHaveProperty('blog'); + }); + it('exposes auth, media, marketing-pages, navigation routers', () => { ... }); +}); + +// packages/core-cms/src/config.test.ts +describe('payloadConfig composition', () => { + it('registers all feature collections', () => { + const slugs = config.collections.map(c => c.slug); + expect(slugs).toEqual(expect.arrayContaining(['articles', 'users', 'media', 'pages'])); + }); +}); +``` + +### 6.7 App tests (Gap 9) + +Per app: + +```typescript +// apps/web-next/src/server/bind-production.test.ts +describe('bindProduction', () => { + it('binds all 4 feature repositories exactly once', async () => { + const spies = { + blog: vi.spyOn(blogModule, 'bindProductionArticles'), + auth: vi.spyOn(authModule, 'bindProductionUsers'), + media: vi.spyOn(mediaModule, 'bindProductionMedia'), + marketing: vi.spyOn(marketingModule, 'bindProductionPages'), + }; + await bindProduction(); + Object.values(spies).forEach(s => expect(s).toHaveBeenCalledOnce()); + }); +}); + +// apps/web-next/src/app/providers.test.tsx +describe('Providers', () => { + it('wraps children with QueryClientProvider and TrpcProvider', () => { + renderWithProviders(
); + expect(screen.getByTestId('child')).toBeInTheDocument(); + }); +}); + +// apps/cms/src/payload.config.test.ts +describe('payload.config', () => { + it('exports a SanitizedConfig with all feature collections', () => { + expect(payloadConfig.collections.length).toBeGreaterThanOrEqual(4); + }); +}); +``` + +### 6.8 Storybook test runner (Gap 8) + +```bash +pnpm add -D -w @storybook/test-runner playwright --filter @repo/storybook +``` + +`apps/storybook/package.json` gains: +```json +{ + "scripts": { + "test-storybook": "test-storybook --url http://localhost:6006" + } +} +``` + +Root script: `pnpm test:stories` runs Storybook test-runner against a started Storybook instance. CI builds Storybook static, serves it, runs `test-storybook` against the static URL. Smoke-tests every story (mounts, no console errors). + +### 6.9 Coverage thresholds (Gap 6) + +Initial threshold: 80% statements / 75% branches / 80% functions / 80% lines (project-wide). `entities/`, `application/use-cases/`, `interface-adapters/controllers/` should hit 100% — enforced via per-directory config: + +```typescript +coverage: { + thresholds: { + 'src/entities/**': { statements: 100, branches: 100 }, + 'src/application/use-cases/**': { statements: 100, branches: 95 }, + 'src/interface-adapters/controllers/**': { statements: 100, branches: 95 }, + statements: 80, branches: 75, functions: 80, lines: 80, + }, +}, +``` + +Tightening allowed as suite matures. Initial pass may need test additions to cross threshold — included as part of each task. + +### 6.10 Sequence shuffling + +`sequence: { shuffle: true }` surfaces tests that depend on execution order (a class of flake that's nearly impossible to debug otherwise). If a test fails on a non-default seed, the failure log includes the seed for reproduction. + +### 6.11 CI (Gap 10) + +`.github/workflows/ci.yml`: + +```yaml +name: CI +on: + push: { branches: [main] } + pull_request: + +env: + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + +jobs: + validate: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: cms + 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 + - run: pnpm test -- --coverage + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/cms + PAYLOAD_SECRET: test-secret-do-not-use-in-prod + - run: pnpm build + - uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage + path: '**/coverage/lcov.info' + + e2e: + needs: validate + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: { POSTGRES_PASSWORD: postgres, POSTGRES_USER: postgres, POSTGRES_DB: cms } + 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 + - run: pnpm test:e2e + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/cms + PAYLOAD_SECRET: test-secret-do-not-use-in-prod + + storybook: + 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 build-storybook --filter @repo/storybook + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "pnpm exec http-server apps/storybook/storybook-static --port 6006 --silent" "pnpm exec wait-on tcp:6006 && pnpm test-storybook --filter @repo/storybook" +``` + +## 7. Documentation changes + +### 7.1 New: `docs/guides/tdd-workflow.md` + +Outline: +1. **The cycle** — red, green, refactor, with the blog example walked end-to-end. +2. **Test naming** — `describe(SUT)` / `it("does X when Y")`. +3. **AAA structure** — Arrange / Act / Assert with examples. +4. **When to mock — decision tree** (DOT graph): + ``` + Pure function? → no mock + Use case? → mock repository at DI level (rebind) + Repository (Payload)?→ mock 'payload' module + use stubPayloadConfig + Component with data? → renderWithProviders with trpc.mocks + Route handler? → mock at boundary only (no deep mocks) + ``` +5. **Test pyramid for this monorepo** — counts per layer; entities + use-cases + controllers > integration > component > e2e. +6. **What NOT to test** — getters/setters, framework code, third-party libs, types-only modules. +7. **Coverage targets** — 80/75/80/80 baseline; 100% in entities + use-cases + controllers. +8. **Factory usage** — when to call `factory.build()` vs construct objects manually. +9. **Contract suite usage** — how to add a new repo impl (write impl, run contract). +10. **Running tests** — watch mode, focused tests (`it.only`), debugging failures. + +### 7.2 Restructure: `docs/guides/adding-a-feature.md` + +Interleave tests with implementation. Each layer becomes a red-green pair: + +``` +Step 1: Write failing test for entity schema (Zod parse rejects invalid input) +Step 2: Implement entity to pass +Step 3: Write factory in src/__factories__/.factory.ts +Step 4: Write failing test for use case using factory + mock repo +Step 5: Implement use case to pass +Step 6: Write contract suite in src/__contracts__/-repository.contract.ts +Step 7: Implement Mock repo, run contract against it (red → green) +Step 8: Implement Payload repo, run same contract (red → green) +Step 9: Write failing controller test +Step 10: Implement controller to pass +Step 11: Write failing tRPC integration test in tests/.feature.test.ts +Step 12: Wire router to pass +Step 13: (UI optional) Write failing component test with renderWithProviders +Step 14: Implement component to pass +Step 15: Wire into core-api / core-cms, run typecheck + lint + boundaries +``` + +Add at top: **"You may not advance to the next layer until the current layer's tests are red, then green."** + +### 7.3 Cross-link + +`testing-strategy.md` becomes the *placement* doc; `tdd-workflow.md` is the *process* doc. Each links to the other. `AGENTS.md` references both. + +### 7.4 ADR + +New: `docs/decisions/adr-011-tdd-foundation.md` capturing: +- Why a dedicated `core-testing` package (vs duplicating helpers) +- Why factories instead of fixture files +- Why contract suites instead of separate test files per impl +- Why `sequence.shuffle` (catches order-dependent tests) +- Why `vi.mock('payload')` over a real test DB (speed, determinism) + +### 7.5 AGENTS.md updates + +Root `AGENTS.md`: add `@repo/core-testing` row to package map, link `docs/guides/tdd-workflow.md`. + +Each feature `AGENTS.md`: add a "Tests" section listing factories, contract suites, and example test commands. + +Root `CLAUDE.md`: add a "TDD" subsection under Quick Start with the canonical commands. + +## 8. Plan structure (preview) + +Plan 7 — TDD Foundation, with these tasks (each TDD'd, self-reviewed, two-stage reviewed): + +| # | Task | Approx. files | +|---|------|---------------| +| 1 | Scaffold `@repo/core-testing` (TDD itself) | ~20 created | +| 2 | Add jsdom + node Vitest bases to `core-typescript`, migrate 5 feature configs | 2 created, 5 modified | +| 3 | Add factories to all 5 features, refactor mock repos to consume them | 8 created, 5 modified | +| 4 | Define + run contract suites for all 7 repository interfaces | 7 created, 14 modified (each impl `.test.ts` runs the contract) | +| 5 | Add tests + jsdom config to `core-ui` | ~10 created | +| 6 | Add tests + node config to `core-api`, `core-cms`, `core-trpc` | ~10 created | +| 7 | Add unit tests to `apps/web-next`, `apps/web-tanstack`, `apps/cms` | ~10 created | +| 8 | Storybook test-runner integration in `apps/storybook` | 1 modified, 1 created | +| 9 | Write `docs/guides/tdd-workflow.md`, restructure `adding-a-feature.md`, cross-link | 1 created, 2 modified | +| 10 | ADR-011 + AGENTS.md updates + CLAUDE.md update | 1 created, 6 modified | +| 11 | CI workflow `.github/workflows/ci.yml` with services + coverage upload | 1 created | +| 12 | Coverage thresholds enforced; tighten where suites are mature | 0 created, 12 modified (per-package vitest configs) | + +Estimated rough total: ~70 files created, ~25 files modified. + +## 9. Tradeoffs + +| Decision | Pro | Con | +|----------|-----|-----| +| New `core-testing` package | DRY; one canonical place for helpers; testable in isolation | One more package to maintain (but small, mostly stable) | +| Coverage thresholds | Builds fail when coverage drops; agents can't merge regressions | Initial enforcement may need test additions to cross threshold (planned for) | +| Sequence shuffle | Catches order-dependent flakes early | First run may surface latent flakes; we fix them as found | +| `vi.mock('payload')` over real DB in tests | Fast (~ms vs seconds), deterministic, no Docker requirement | Requires contract suite to catch mock/real drift (which is exactly why we add §5.2) | +| Storybook test-runner | Every story becomes a smoke test for free | Adds a CI job (~1-2 min); offset by Turbo cache | +| CI in this template | TDD without CI rots in weeks | Template users must rotate the Turbo token themselves; documented in CI yaml comments | + +## 10. Acceptance criteria + +- `@repo/core-testing` exists, exports the documented surface, has its own tests passing. +- Every feature has factories in `src/__factories__/` and a contract suite per repository interface. +- `core-ui`, `core-api`, `core-cms`, `core-trpc` each have ≥1 passing test. +- All 4 apps have ≥1 unit test (provider/bind-production/route). +- `pnpm test -- --coverage` passes with thresholds. +- `pnpm test-storybook` passes against built Storybook. +- `docs/guides/tdd-workflow.md` exists; `adding-a-feature.md` interleaves tests with implementation. +- `.github/workflows/ci.yml` runs typecheck, lint, boundaries, test, build, e2e, storybook on every PR. +- ADR-011 committed. +- AGENTS.md and CLAUDE.md reference the new TDD workflow. + +## 11. Out of scope (explicit) + +- Mutation testing (Stryker) +- Property-based testing (fast-check) +- Visual regression beyond Storybook smoke (Chromatic, Percy) +- Real-DB integration tests (testcontainers Postgres) +- Performance/load testing +- Security scanning in CI (separate concern) + +These are good ideas for later but would expand scope past "make TDD frictionless." Tracked as future work.