First slice of the Plan 8 deferred doc-update checklist: - CLAUDE.md Key Conventions: factory-function use cases/controllers, entities/models/<x>.ts paths, .toDynamicValue DI bindings, direct injection in tests - docs/architecture/overview.md data-flow box updated to factory style (controller resolved via container.get<IXController>; use case factory takes deps as args) - docs/decisions/adr-012-lazar-conformance.md created — records the conformance decision and four intentional divergences - docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md and the matching spec annotated with a "pre-Plan-8 layout" note pointing at the refactor log Remaining Plan 8 doc-update items (root AGENTS.md, per-feature AGENTS.md, adding-a-feature.md, tdd-workflow.md, testing-strategy.md, vertical-feature-spec.md §6/§10, core-testing AGENTS.md) intentionally paused — Plan 9 (input/output unification) will change overlapping content, so resuming after Plan 9 lands avoids double-churn. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
27 KiB
TDD Foundation — Design Spec
Note (2026-05-05, post-Plan-8): File-path references in this spec describe the pre-Plan-8 layout. Plan 8 (Lazar pattern conformance) introduced
entities/models/,entities/errors/<domain>.ts, the.mock.tssuffix, droppedpayload-prefixes, and made use cases / controllers factory functions. The TDD principles, gap catalogue, andcore-testingpackage design remain valid; only the example paths and snippets shifted. Seedocs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.mdand ADR-012 for the mapping.
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
payloadmodule 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:
- Tooling layer —
core-typescriptandcore-eslintprovide configs every package inherits. - 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. - Per-package convention — every feature/core package follows the same skeleton: factories in
src/__factories__/, contract suites insrc/__contracts__/, colocated*.test.{ts,tsx}for units,tests/*.feature.test.tsfor 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/<feature>/* (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/
// define-factory.ts
export interface FactoryContext {
sequence: number;
}
export interface Factory<T> {
build(overrides?: Partial<T>): T;
buildList(count: number, overrides?: Partial<T>): T[];
reset(): void;
}
export function defineFactory<T>(builder: (ctx: FactoryContext) => T): Factory<T> {
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/
// define-contract-suite.ts
export interface ContractContext<T> {
buildSubject: () => Promise<T> | T;
}
export interface ContractSuite<T> {
run(buildSubject: () => Promise<T> | T): void;
}
export function defineContractSuite<T>(
name: string,
suite: (ctx: ContractContext<T>) => void,
): ContractSuite<T> {
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/
// render-with-providers.tsx
export interface RenderOptions {
trpc?: { mocks?: Record<string, unknown> };
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) => (
<QueryClientProvider client={queryClient}>
<TrpcProvider client={trpcClient} queryClient={queryClient}>
{children}
</TrpcProvider>
</QueryClientProvider>
);
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/
// mock-payload-module.ts
export function mockPayloadModule(impl: Partial<Payload>): void {
vi.mock('payload', () => ({
getPayload: vi.fn().mockResolvedValue(impl),
}));
}
// stub-config.ts
export const stubPayloadConfig = {} as SanitizedConfig;
5.5 setup/
// 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:
{
"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.tsxper primitive (Button, Input, Card, etc. — discovered during execution). Use RTL +renderWithProviders(no providers needed for primitives, but pattern uniform).core-api— node env. Test thatappRouterexposes the expected feature routers (appRouter._def.procedureswalked 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 wireshttpBatchLink+superjson. Usemswor fetch stub.
6.2 React component testing (Gap 2)
core-typescript exports two configs:
// 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:
// 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(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when activated', async () => {
const handleClick = vi.fn();
renderWithProviders(<Button onClick={handleClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledOnce();
});
});
6.6 Composition tests for core-api, core-cms, core-trpc (Gap 1)
// 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:
// 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(<Providers><div data-testid="child" /></Providers>);
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)
pnpm add -D -w @storybook/test-runner playwright --filter @repo/storybook
apps/storybook/package.json gains:
{
"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:
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:
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:
- The cycle — red, green, refactor, with the blog example walked end-to-end.
- Test naming —
describe(SUT)/it("does X when Y"). - AAA structure — Arrange / Act / Assert with examples.
- 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) - Test pyramid for this monorepo — counts per layer; entities + use-cases + controllers > integration > component > e2e.
- What NOT to test — getters/setters, framework code, third-party libs, types-only modules.
- Coverage targets — 80/75/80/80 baseline; 100% in entities + use-cases + controllers.
- Factory usage — when to call
factory.build()vs construct objects manually. - Contract suite usage — how to add a new repo impl (write impl, run contract).
- 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__/<entity>.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__/<entity>-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>.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-testingpackage (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-testingexists, 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-trpceach have ≥1 passing test.- All 4 apps have ≥1 unit test (provider/bind-production/route).
pnpm test -- --coveragepasses with thresholds.pnpm test-storybookpasses against built Storybook.docs/guides/tdd-workflow.mdexists;adding-a-feature.mdinterleaves tests with implementation..github/workflows/ci.ymlruns 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.