# Strict TDD Module — Apply Phase
> **This module is loaded ONLY when Strict TDD Mode is enabled AND a test runner is available.**
> If you are reading this, the orchestrator already verified both conditions. Follow every instruction.
## TDD Philosophy
TDD is not testing. TDD is **software design driven by tests**. You write a test that describes what the code SHOULD do, then write the minimum code to make it real. The tests design the API, the contracts, the behavior. Code is a side effect of tests.
### The Three Laws
1. **Do NOT write production code** until you have a failing test
2. **Do NOT write more test** than is necessary to fail
3. **Do NOT write more code** than is necessary to pass the test
## TDD Implementation Cycle
For EVERY task assigned to you, follow this cycle strictly:
```
FOR EACH TASK:
├── 0. SAFETY NET (only if modifying existing files)
│ ├── Run existing tests for files being modified
│ ├── Capture baseline: "{N} tests passing"
│ ├── If any FAIL → STOP, report as "pre-existing failure"
│ │ (do NOT fix pre-existing failures — report to orchestrator)
│ └── This baseline proves you did not break what already worked
│
├── 1. UNDERSTAND
│ ├── Read the task description
│ ├── Read relevant spec scenarios (these ARE your acceptance criteria)
│ ├── Read the design decisions (these CONSTRAIN your approach)
│ ├── Read existing code and test patterns (match the style)
│ └── Determine test layer (see "Choosing Test Layer" below)
│
├── 2. RED — Write a failing test FIRST
│ ├── Write test(s) that describe the expected behavior from the spec
│ ├── Prefer pure functions where possible (no side effects = easy to test)
│ ├── The test MUST reference production code that does NOT exist yet
│ │ (this guarantees failure — no need to execute to confirm)
│ ├── If the production code/function already exists:
│ │ └── Write a test for the NEW behavior that is NOT yet implemented
│ └── GATE: Do NOT proceed to GREEN until the test is written
│
├── 3. GREEN — Write the MINIMUM code to pass
│ ├── Implement ONLY what the failing test needs
│ ├── Fake It is VALID here (hardcoded return values are OK)
│ ├── EXECUTE tests → must PASS
│ │ ├── ✅ Passed → proceed to TRIANGULATE or REFACTOR
│ │ └── ❌ Failed → fix the implementation, NOT the test
│ └── GATE: Do NOT proceed until GREEN is confirmed by execution
│
├── 4. TRIANGULATE (MANDATORY for most tasks)
│ ├── DEFAULT: triangulation is REQUIRED. You need a compelling reason to skip it.
│ ├── Add a second test case with DIFFERENT inputs/expected outputs
│ ├── EXECUTE tests → if Fake It breaks (hardcoded no longer works):
│ │ └── Generalize to real logic (this is the whole point)
│ ├── Repeat until ALL spec scenarios for this task are covered
│ ├── Each triangulation pass: write test → run → fix implementation
│ ├── MINIMUM: at least 2 test cases per behavior (happy path + one edge case)
│ │ ├── One test with data that produces a NON-EMPTY/NON-TRIVIAL result
│ │ └── One test with data that exercises a DIFFERENT code path
│ ├── WATCH OUT for GREEN that passes trivially:
│ │ ├── If your test passes because the component/element isn't rendered → NOT a real GREEN
│ │ ├── If your test passes because a loop iterates 0 times → NOT a real GREEN
│ │ ├── If your test passes because the setup doesn't trigger the code path → NOT a real GREEN
│ │ └── A real GREEN means: production code RAN and produced the expected output
│ ├── Skip triangulation ONLY when ALL of these are true:
│ │ ├── The task is purely structural (config file, constant definition, type export)
│ │ ├── There is literally ONE possible output (no branching, no logic)
│ │ └── You explicitly note "Triangulation skipped: {reason}" in the evidence table
│ └── GATE: All spec scenarios for this task must have tests before REFACTOR
│
├── 5. REFACTOR — Improve without changing behavior
│ ├── Extract constants (eliminate magic numbers)
│ ├── Extract functions (reduce cyclomatic complexity)
│ ├── Improve naming, remove duplication
│ ├── Push toward pure functions where feasible
│ ├── Apply Boy Scout Rule: leave code cleaner than you found it
│ ├── EXECUTE tests after EACH refactoring step → must STILL PASS
│ │ ├── ✅ Still passing → refactoring is safe, continue
│ │ └── ❌ Failed → REVERT that refactoring step, try smaller
│ └── GATE: Tests green after EVERY refactoring change
│
├── 6. Mark task complete [x]
└── 7. Note any deviations or issues discovered
```
## Choosing Test Layer
Based on the testing capabilities cached in Engram (`sdd/{project}/testing-capabilities`), choose the appropriate test layer for each task:
```
Determine test layer by WHAT the task does:
├── Pure logic, utility function, calculation, data transformation
│ └── Unit test (always available if test runner exists)
│
├── Component rendering, user interaction, state changes
│ ├── IF integration tools available → Integration test
│ └── IF NOT → Unit test with mocks (degrade gracefully)
│
├── Multi-component flow, API interaction, context/provider behavior
│ ├── IF integration tools available → Integration test
│ └── IF NOT → Unit test with mocks
│
├── Critical business flow, full user journey, cross-page navigation
│ ├── IF E2E tools available → E2E test
│ ├── IF NOT but integration available → Integration test
│ └── IF neither → Unit test (degrade gracefully)
│
└── Default: Unit test (always the fallback)
```
**Key rule**: Use the HIGHEST available layer that fits the task. But NEVER skip a task because a layer is unavailable — degrade to the next available layer.
## Test Execution
Detect the test runner from the cached testing capabilities:
```
Read test command from:
├── Cached capabilities → test_runner.command (fastest — already detected)
├── openspec/config.yaml → rules.apply.test_command (override)
└── Fallback: detect from package.json/pyproject.toml/go.mod
When executing tests during TDD:
├── Run ONLY the relevant test file, not the entire suite
│ ├── JS/TS: {runner} {test-file-path} (e.g., pnpm vitest run src/utils/tax.test.ts)
│ ├── Python: pytest {test-file-path}
│ ├── Go: go test ./{package}/... -run {TestName}
│ └── Adapt to the runner's CLI
├── This keeps the cycle FAST
└── Full suite runs happen in sdd-verify, not here
```
## Pure Function Preference
When writing production code in GREEN/TRIANGULATE steps, prefer pure functions:
```
✅ PREFER (pure — easy to test):
function calculateDiscount(price: number, quantity: number): number {
return quantity >= 5 ? price * quantity * 0.1 : 0
}
❌ AVOID (impure — hard to test):
function calculateDiscount(item: Item) {
globalState.lastDiscount = item.price * 0.1 // side effect
updateDOM() // side effect
return globalState.lastDiscount
}
```
**Why**: Pure functions are deterministic (same input → same output), have no side effects, and are trivially testable. TDD naturally pushes you toward pure functions — embrace it.
## Approval Testing (for refactoring existing code)
When a task involves REFACTORING existing code (not writing new code):
```
BEFORE touching production code:
├── 1. Identify existing behavior to preserve
├── 2. Write "approval tests" that capture current behavior:
│ ├── Call the function with known inputs
│ ├── Assert the CURRENT outputs (even if ugly or wrong)
│ └── These tests document what the code does NOW
├── 3. Run approval tests → must PASS (they describe current reality)
├── 4. NOW refactor the production code
├── 5. Run approval tests again → must STILL PASS
│ ├── ✅ Passing → refactoring preserved behavior
│ └── ❌ Failing → refactoring broke something, revert
└── 6. If the spec says behavior should CHANGE:
├── Update the approval test to reflect NEW expected behavior
├── Run → test FAILS (RED — new behavior not implemented yet)
└── Implement new behavior → GREEN
```
## Return Summary Extension
When Strict TDD Mode is active, your return summary MUST include this section:
```markdown
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|------|-----------|-------|------------|-----|-------|-------------|----------|
| 1.1 | `path/test.ext` | Unit | ✅ 5/5 | ✅ Written | ✅ Passed | ✅ 3 cases | ✅ Clean |
| 1.2 | `path/test.ext` | Integration | N/A (new) | ✅ Written | ✅ Passed | ➖ Single | ✅ Clean |
| 1.3 | `path/test.ext` | Unit | ✅ 2/2 | ✅ Written | ✅ Passed | ✅ 2 cases | ➖ None needed |
### Test Summary
- **Total tests written**: {N}
- **Total tests passing**: {N}
- **Layers used**: Unit ({N}), Integration ({N}), E2E ({N})
- **Approval tests** (refactoring): {N} or "None — no refactoring tasks"
- **Pure functions created**: {N}
```
**Column definitions**:
- **Safety Net**: Pre-existing tests run before modifying files. "N/A (new)" for new files.
- **RED**: Test written first, referencing code that doesn't exist yet. Always "✅ Written".
- **GREEN**: Tests executed and passing after minimal implementation. Must show execution result.
- **TRIANGULATE**: Additional test cases added to force real logic. "➖ Single" if spec has only one scenario.
- **REFACTOR**: Code improved with tests still passing. "➖ None needed" if code was already clean.
## Assertion Quality Rules (MANDATORY)
**Every assertion must verify REAL behavior.** A test that passes without exercising production logic is worse than no test — it gives false confidence.
### Banned Assertion Patterns (NEVER write these)
```
# TRIVIAL ASSERTIONS — test proves nothing
expect(true).toBe(true) # ❌ Tautology
expect(false).toBe(false) # ❌ Tautology
expect(1).toBe(1) # ❌ Tautology — no production code involved
assert True # ❌ Always passes
assert 1 == 1 # ❌ Always passes
# EMPTY COLLECTION ASSERTIONS without setup context
expect(result).toEqual([]) # ❌ ONLY valid if you set up conditions for empty
expect(result).toHaveLength(0) # ❌ Same — why is it empty? Did production code run?
assert len(result) == 0 # ❌ Same — prove the emptiness comes from real logic
assert result == [] # ❌ Same
# TYPE-ONLY ASSERTIONS — proves existence, not behavior
expect(result).toBeDefined() # ❌ Alone is useless — WHAT is the value?
expect(result).not.toBeNull() # ❌ Alone is useless — assert the actual value
expect(typeof result).toBe('object') # ❌ Alone is useless — what does the object contain?
assert result is not None # ❌ Alone — assert what result actually IS
# GHOST LOOP — assertion inside a loop that iterates 0 times
const items = screen.queryAllByTestId("item"); // returns []
for (const item of items) {
expect(item).toHaveTextContent("value"); # ❌ NEVER EXECUTES — loop body is dead code
}
# FIX: assert the collection is non-empty FIRST, or set up data so it IS non-empty:
expect(items).toHaveLength(3); # ✅ Proves items exist
for (const item of items) { ... } # ✅ Now the loop actually runs
# INCOMPLETE TDD CYCLE — GREEN without TRIANGULATE
# If your GREEN test passes because the setup doesn't exercise the code path,
# you are NOT done. You MUST triangulate with a setup that DOES exercise it.
# Example: testing "search doesn't update until Enter" but the component
# that receives the search is never rendered → the test proves nothing.
# FIX: add a test where the component IS rendered and verify the behavior.
```
### What Makes a REAL Assertion
Every test assertion must satisfy ALL of these:
1. **Calls production code** — the test invokes a function, method, or component from the implementation
2. **Asserts a specific output** — compares against a concrete expected value derived from the spec
3. **Would FAIL if the production code were wrong** — if you change the implementation logic, THIS test breaks
```
# ✅ REAL assertions — production code determines the result
expect(calculateDiscount(100, 10)).toBe(10) # Real input → real output
expect(screen.getByText('Welcome, John')).toBeInTheDocument() # Rendered from data
assert result[0].status == "FAIL" # Specific finding from check execution
assert response.status_code == 403 # Real HTTP response from the endpoint
expect(result).toHaveLength(3) # AND you set up exactly 3 items
```
### Empty Collection Rule
`expect(result).toEqual([])` or `assert len(result) == 0` is ONLY valid when:
1. You set up a specific precondition that SHOULD produce an empty result (e.g., no matching records)
2. The production code actually ran and filtered/processed data to arrive at empty
3. A companion test with different setup produces a NON-EMPTY result (triangulation)
If you cannot explain WHY the result is empty based on setup → the assertion is trivial.
### Smoke Test Rule
A test that only renders a component without asserting any output is NOT a valid test:
```
# ❌ SMOKE TEST ONLY — proves nothing about behavior
render();
expect(screen.getByTestId("wrapper")).toBeInTheDocument(); # Just proves it rendered
# ✅ BEHAVIORAL TEST — proves what the component DOES with the data
render();
expect(screen.getByText("Expected Title")).toBeInTheDocument(); # Verifies output from data
expect(screen.getByRole("button")).toHaveTextContent("Submit"); # Verifies real content
```
"Renders without crash" is a smoke test. It is NOT a unit test, NOT an integration test, and it does NOT count toward TDD coverage. If you need a smoke test, it must be accompanied by real behavioral assertions.
### Mock Hygiene Rules
**If you need more mocks than assertions, you are testing at the WRONG level.**
```
Mock/assertion ratio guide:
├── ≤ 3 mocks for a test file → ✅ Healthy — focused test
├── 4–6 mocks → ⚠️ Consider extracting logic to a pure function
├── 7+ mocks → ❌ STOP — you are testing at the wrong layer
│ ├── Extract the logic under test to a PURE FUNCTION and test it without mocks
│ ├── OR move the test to integration/E2E layer where real dependencies exist
│ └── NEVER write 10+ mocks to verify a one-line transformation
```
**Extract-Before-Mock Rule**: If the behavior you want to test is a data transformation, mapping, filtering, or conditional logic (e.g., `MUTED → FAIL` status conversion), EXTRACT it to a pure function FIRST, then test the pure function directly. No mocks needed.
```
# ❌ BAD: 15 mocks to test a one-line status conversion
vi.mock("next/navigation", ...);
vi.mock("next/link", ...);
vi.mock("@/components/shadcn", ...);
// ... 12 more mocks ...
render();
expect(screen.getByText("FAIL")).toBeInTheDocument();
# ✅ GOOD: extract and test the logic directly
// In production code:
export function resolveDisplayStatus(status: string, isMuted: boolean): string {
return status === "MUTED" ? "FAIL" : status;
}
// In test — ZERO mocks needed:
expect(resolveDisplayStatus("MUTED", true)).toBe("FAIL");
expect(resolveDisplayStatus("PASS", false)).toBe("PASS");
```
### Implementation Detail Coupling Rule
Tests must assert **behavior visible to the user**, not internal implementation details:
```
# ❌ COUPLED TO IMPLEMENTATION — breaks on any style refactor
expect(element.className).toContain("text-xs");
expect(element.className).toContain("-mt-2.5");
expect(element.className).toContain("border-border-error-primary");
expect(element.style.color).toBe("red");
# ❌ COUPLED TO INTERNALS — breaks when implementation changes
expect(mockService.mock.calls.length).toBe(3); # Why 3? Brittle.
expect(component.state.isLoading).toBe(true); # Internal state, not behavior.
# ✅ BEHAVIORAL — survives refactors, tests what users see
expect(screen.getByText("Error: Payment failed")).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveTextContent("Risk:");
expect(screen.getByRole("button")).toBeDisabled();
```
**CSS class assertions are NEVER valid test assertions.** If you need to verify visual styling:
1. Test the **semantic outcome** (e.g., element has `role="alert"`, text is visible, button is disabled)
2. OR use a visual regression tool / E2E screenshot comparison
3. NEVER assert specific Tailwind/CSS class names — they are implementation details
## Rules (Strict TDD specific)
- NEVER write production code before writing its test — this is the ONE rule that cannot be broken
- NEVER skip the GREEN execution gate — you MUST run tests and confirm they pass
- NEVER skip triangulation when the spec defines multiple scenarios — hardcoded Fake It must be forced out
- NEVER write trivial assertions (see Banned Assertion Patterns above) — they are WORSE than no test
- ALWAYS verify that every assertion CALLS production code and asserts a SPECIFIC expected value
- ALWAYS run the Safety Net before modifying existing files — protect what already works
- ALWAYS report the TDD Cycle Evidence table — the verify phase will check it
- If a test runner execution fails for infrastructure reasons (not test failures), report as "Blocked" and continue to next task
- Prefer pure functions — but don't force it where it doesn't fit (e.g., React components with state)
- For refactoring tasks, ALWAYS write approval tests before touching code
- Run ONLY the relevant test file during the cycle, not the full suite