Test-Driven Development Write test first. Commit it red. Write minimal code to pass. Commit green. Refactor. Core principle: tests verify behavior through public interfaces, not implementation. Code can change entirely; tests shouldn't. A test that breaks when you rename an internal function — with no behavior change — was testing implementation. Delete it. No watched failure = no proof the test tests the right thing. Commit history is the evidence; hooks check its structure. See tests.md for examples and mocking.md for mocking guidelines. Iron Law NO PRODUCTION CODE WITHOUT A FAILING TEST CO…

\nEMPTY_TREE=4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\nfail() { echo \"FAIL: $*\" >&2; exit 1; }\n\ncheck_conventions() {\n # Go/Rust markers require red-tests CI job + name prefixes (invisible-marker prevention)\n local go_files rust_files\n go_files=$(grep -rl '//go:build red' --include='*_test.go' . 2>/dev/null || true)\n rust_files=$(grep -rl '#\\[ignore = \"red\"\\]' --include='*.rs' . 2>/dev/null || true)\n [ -z \"$go_files$rust_files\" ] && return 0\n\n grep -rqs 'red-tests' .forgejo/workflows .github/workflows .gitlab-ci.yml .woodpecker.yml 2>/dev/null \\\n || fail \"Go/Rust red markers present but no 'red-tests' CI job — markers invisible without inverse job\"\n\n local f\n for f in $go_files; do\n grep -E '^func Test' \"$f\" | grep -qvE '^func TestRed' \\\n && fail \"$f: red-tagged file has test without TestRed prefix — invisible to inverse job\"\n done\n for f in $rust_files; do\n awk '/#\\[ignore = \"red\"\\]/{p=1;next} p&&/fn /{if($0!~/fn red_/)exit 1; p=0}' \"$f\" \\\n || fail \"$f: red-ignored test without red_ prefix — invisible to inverse job\"\n done\n}\n\ncheck_changes() { # $1 = commit subject, $2 = changed files, $3 = diff\n local msg=\"$1\" files=\"$2\" diff=\"$3\"\n if [[ \"$msg\" =~ ^test\\(red\\): ]]; then\n while IFS= read -r f; do\n [ -z \"$f\" ] && continue\n [[ \"$f\" =~ $TEST_PATH ]] || fail \"red commit touches non-test file: $f\"\n done \u003c\u003c\u003c \"$files\"\n grep -E '^\\+' \u003c\u003c\u003c \"$diff\" | grep -qE \"$MARKERS\" \\\n || fail \"red commit adds no red marker\"\n else\n grep -E '^\\+' \u003c\u003c\u003c \"$diff\" | grep -qE \"$MARKERS\" \\\n && fail \"red marker added outside test(red) commit\"\n fi\n check_conventions\n}\n\nmode=\"${1:?usage: lint-red.sh staged | commit \u003cref> | tree}\"\ncase \"$mode\" in\n tree)\n if grep -rEn \"$MARKERS\" --exclude-dir={.git,node_modules,vendor,target} .; then\n fail \"red markers in tree — behaviors never verified green\"\n fi\n check_conventions\n ;;\n commit)\n ref=\"${2:-HEAD}\"\n parent=$(git rev-parse -q --verify \"$ref~1\" 2>/dev/null || echo \"$EMPTY_TREE\")\n check_changes \"$(git log -1 --format=%s \"$ref\")\" \\\n \"$(git diff-tree --no-commit-id --name-only -r \"$ref\")\" \\\n \"$(git diff \"$parent\" \"$ref\")\"\n ;;\n staged)\n msg=$(head -n1 \"${COMMIT_MSG_FILE:-.git/COMMIT_EDITMSG}\")\n check_changes \"$msg\" \\\n \"$(git diff --cached --name-only --diff-filter=ACMR)\" \\\n \"$(git diff --cached)\"\n ;;\n *) fail \"unknown mode: $mode (usage: lint-red.sh staged | commit \u003cref> | tree)\" ;;\nesac\necho \"lint-red: OK\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3096,"content_sha256":"874ec603e6ac5c302386b50a76b9500bb9a4f9508e2c18b00154a6d9b5c21dad"},{"filename":"mocking.md","content":"# When to Mock\n\nMock at **system boundaries** only:\n\n- External APIs (payment, email, etc.)\n- Databases (sometimes - prefer test DB)\n- Time/randomness\n- File system (sometimes)\n\nDon't mock:\n\n- Your own classes/modules\n- Internal collaborators\n- Anything you control\n\n## Designing for Mockability\n\nAt system boundaries, design interfaces that are easy to mock:\n\n### 1. Use dependency injection\n\nPass external dependencies in rather than creating them internally:\n\n```typescript\n// Easy to mock\nfunction processPayment(order, paymentClient) {\n return paymentClient.charge(order.total);\n}\n\n// Hard to mock\nfunction processPayment(order) {\n const client = new StripeClient(process.env.STRIPE_KEY);\n return client.charge(order.total);\n}\n```\n\n### 2. Prefer SDK-style interfaces over generic fetchers\n\nCreate specific functions for each external operation instead of one generic function with conditional logic:\n\n```typescript\n// GOOD: Each function is independently mockable\nconst api = {\n getUser: (id) => fetch(`/users/${id}`),\n getOrders: (userId) => fetch(`/users/${userId}/orders`),\n createOrder: (data) => fetch('/orders', { method: 'POST', body: data }),\n};\n\n// BAD: Mocking requires conditional logic inside the mock\nconst api = {\n fetch: (endpoint, options) => fetch(endpoint, options),\n};\n```\n\nThe SDK approach means:\n\n- Each mock returns one specific shape\n- No conditional logic in test setup\n- Easier to see which endpoints a test exercises\n- Type safety per endpoint\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1482,"content_sha256":"8591e9f49f25e1c9a76a4084d8d3c0b5f06104e44f4b22bdf2a90013a3266849"},{"filename":"refactoring.md","content":"# Refactor Candidates\n\nAfter TDD cycle, look for:\n\n- **Duplication** → Extract function/class\n- **Long methods** → Break into private helpers (keep tests on public interface)\n- **Shallow modules** → Combine or deepen\n- **Feature envy** → Move logic to where data lives\n- **Primitive obsession** → Introduce value objects\n- **Existing code** the new code reveals as problematic\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":387,"content_sha256":"54fced22dd1911b7094c3fe7979b7c1a40d40be307482c4adf7dc0588f27d6cc"},{"filename":"testing-anti-patterns.md","content":"# Testing Anti-Patterns\n\n**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code.\n\n## Overview\n\nTests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.\n\n**Core principle:** Test what the code does, not what the mocks do.\n\n**Following strict TDD prevents these anti-patterns.**\n\n## The Iron Laws\n\n```text\n1. NEVER test mock behavior\n2. NEVER add test-only methods to production classes\n3. NEVER mock without understanding dependencies\n```\n\n## Anti-Pattern 1: Testing Mock Behavior\n\n**The violation:**\n\n```typescript\n// ❌ BAD: Testing that the mock exists\ntest('renders sidebar', () => {\n render(\u003cPage />);\n expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();\n});\n```\n\n**Why this is wrong:**\n\n- You're verifying the mock works, not that the component works\n- Test passes when mock is present, fails when it's not\n- Tells you nothing about real behavior\n\n**your human partner's correction:** \"Are we testing the behavior of a mock?\"\n\n**The fix:**\n\n```typescript\n// ✅ GOOD: Test real component or don't mock it\ntest('renders sidebar', () => {\n render(\u003cPage />); // Don't mock sidebar\n expect(screen.getByRole('navigation')).toBeInTheDocument();\n});\n\n// OR if sidebar must be mocked for isolation:\n// Don't assert on the mock - test Page's behavior with sidebar present\n```\n\n### Gate Function: Mock Behavior\n\n```text\nBEFORE asserting on any mock element:\n Ask: \"Am I testing real component behavior or just mock existence?\"\n\n IF testing mock existence:\n STOP - Delete the assertion or unmock the component\n\n Test real behavior instead\n```\n\n## Anti-Pattern 2: Test-Only Methods in Production\n\n**The violation:**\n\n```typescript\n// ❌ BAD: destroy() only used in tests\nclass Session {\n async destroy() { // Looks like production API!\n await this._workspaceManager?.destroyWorkspace(this.id);\n // ... cleanup\n }\n}\n\n// In tests\nafterEach(() => session.destroy());\n```\n\n**Why this is wrong:**\n\n- Production class polluted with test-only code\n- Dangerous if accidentally called in production\n- Violates YAGNI and separation of concerns\n- Confuses object lifecycle with entity lifecycle\n\n**The fix:**\n\n```typescript\n// ✅ GOOD: Test utilities handle test cleanup\n// Session has no destroy() - it's stateless in production\n\n// In test-utils/\nexport async function cleanupSession(session: Session) {\n const workspace = session.getWorkspaceInfo();\n if (workspace) {\n await workspaceManager.destroyWorkspace(workspace.id);\n }\n}\n\n// In tests\nafterEach(() => cleanupSession(session));\n```\n\n### Gate Function: Test-Only Methods\n\n```text\nBEFORE adding any method to production class:\n Ask: \"Is this only used by tests?\"\n\n IF yes:\n STOP - Don't add it\n Put it in test utilities instead\n\n Ask: \"Does this class own this resource's lifecycle?\"\n\n IF no:\n STOP - Wrong class for this method\n```\n\n## Anti-Pattern 3: Mocking Without Understanding\n\n**The violation:**\n\n```typescript\n// ❌ BAD: Mock breaks test logic\ntest('detects duplicate server', () => {\n // Mock prevents config write that test depends on!\n vi.mock('ToolCatalog', () => ({\n discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)\n }));\n\n await addServer(config);\n await addServer(config); // Should throw - but won't!\n});\n```\n\n**Why this is wrong:**\n\n- Mocked method had side effect test depended on (writing config)\n- Over-mocking to \"be safe\" breaks actual behavior\n- Test passes for wrong reason or fails mysteriously\n\n**The fix:**\n\n```typescript\n// ✅ GOOD: Mock at correct level\ntest('detects duplicate server', () => {\n // Mock the slow part, preserve behavior test needs\n vi.mock('MCPServerManager'); // Just mock slow server startup\n\n await addServer(config); // Config written\n await addServer(config); // Duplicate detected ✓\n});\n```\n\n### Gate Function: Mocking Dependencies\n\n```text\nBEFORE mocking any method:\n STOP - Don't mock yet\n\n 1. Ask: \"What side effects does the real method have?\"\n 2. Ask: \"Does this test depend on any of those side effects?\"\n 3. Ask: \"Do I fully understand what this test needs?\"\n\n IF depends on side effects:\n Mock at lower level (the actual slow/external operation)\n OR use test doubles that preserve necessary behavior\n NOT the high-level method the test depends on\n\n IF unsure what test depends on:\n Run test with real implementation FIRST\n Observe what actually needs to happen\n THEN add minimal mocking at the right level\n\n Red flags:\n - \"I'll mock this to be safe\"\n - \"This might be slow, better mock it\"\n - Mocking without understanding the dependency chain\n```\n\n## Anti-Pattern 4: Incomplete Mocks\n\n**The violation:**\n\n```typescript\n// ❌ BAD: Partial mock - only fields you think you need\nconst mockResponse = {\n status: 'success',\n data: { userId: '123', name: 'Alice' }\n // Missing: metadata that downstream code uses\n};\n\n// Later: breaks when code accesses response.metadata.requestId\n```\n\n**Why this is wrong:**\n\n- **Partial mocks hide structural assumptions** - You only mocked fields you know about\n- **Downstream code may depend on fields you didn't include** - Silent failures\n- **Tests pass but integration fails** - Mock incomplete, real API complete\n- **False confidence** - Test proves nothing about real behavior\n\n**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.\n\n**The fix:**\n\n```typescript\n// ✅ GOOD: Mirror real API completeness\nconst mockResponse = {\n status: 'success',\n data: { userId: '123', name: 'Alice' },\n metadata: { requestId: 'req-789', timestamp: 1234567890 }\n // All fields real API returns\n};\n```\n\n### Gate Function: Incomplete Mocks\n\n```text\nBEFORE creating mock responses:\n Check: \"What fields does the real API response contain?\"\n\n Actions:\n 1. Examine actual API response from docs/examples\n 2. Include ALL fields system might consume downstream\n 3. Verify mock matches real response schema completely\n\n Critical:\n If you're creating a mock, you must understand the ENTIRE structure\n Partial mocks fail silently when code depends on omitted fields\n\n If uncertain: Include all documented fields\n```\n\n## Anti-Pattern 5: Integration Tests as Afterthought\n\n**The violation:**\n\n```text\n✅ Implementation complete\n❌ No tests written\n\"Ready for testing\"\n```\n\n**Why this is wrong:**\n\n- Testing is part of implementation, not optional follow-up\n- TDD would have caught this\n- Can't claim complete without tests\n\n**The fix:**\n\n```text\nTDD cycle:\n1. Write failing test\n2. Implement to pass\n3. Refactor\n4. THEN claim complete\n```\n\n## When Mocks Become Too Complex\n\n**Warning signs:**\n\n- Mock setup longer than test logic\n- Mocking everything to make test pass\n- Mocks missing methods real components have\n- Test breaks when mock changes\n\n**your human partner's question:** \"Do we need to be using a mock here?\"\n\n**Consider:** Integration tests with real components often simpler than complex mocks\n\n## TDD Prevents These Anti-Patterns\n\n**Why TDD helps:**\n\n1. **Write test first** → Forces you to think about what you're actually testing\n2. **Watch it fail** → Confirms test tests real behavior, not mocks\n3. **Minimal implementation** → No test-only methods creep in\n4. **Real dependencies** → You see what the test actually needs before mocking\n\n**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first.\n\n## Quick Reference\n\n| Anti-Pattern | Fix |\n| --- | --- |\n| Assert on mock elements | Test real component or unmock it |\n| Test-only methods in production | Move to test utilities |\n| Mock without understanding | Understand dependencies first, mock minimally |\n| Incomplete mocks | Mirror real API completely |\n| Tests as afterthought | TDD - tests first |\n| Over-complex mocks | Consider integration tests |\n\n## Red Flags\n\n- Assertion checks for `*-mock` test IDs\n- Methods only called in test files\n- Mock setup is >50% of test\n- Test fails when you remove mock\n- Can't explain why mock is needed\n- Mocking \"just to be safe\"\n\n## The Bottom Line\n\n**Mocks are tools to isolate, not things to test.**\n\nIf TDD reveals you're testing mock behavior, you've gone wrong.\n\nFix: Test real behavior or question why you're mocking at all.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8361,"content_sha256":"b0e96a36ccdf6b922b513ce681f0e836fcdd7b003a9b0decba1975a2f2015a03"},{"filename":"tests.md","content":"# Good and Bad Tests\n\n## Good Tests\n\n**Integration-style**: Test through real interfaces, not mocks of internal parts.\n\n```typescript\n// GOOD: Tests observable behavior\ntest(\"user can checkout with valid cart\", async () => {\n const cart = createCart();\n cart.add(product);\n const result = await checkout(cart, paymentMethod);\n expect(result.status).toBe(\"confirmed\");\n});\n```\n\nCharacteristics:\n\n- Tests behavior users/callers care about\n- Uses public API only\n- Survives internal refactors\n- Describes WHAT, not HOW\n- One logical assertion per test\n\n## Bad Tests\n\n**Implementation-detail tests**: Coupled to internal structure.\n\n```typescript\n// BAD: Tests implementation details\ntest(\"checkout calls paymentService.process\", async () => {\n const mockPayment = jest.mock(paymentService);\n await checkout(cart, payment);\n expect(mockPayment.process).toHaveBeenCalledWith(cart.total);\n});\n```\n\nRed flags:\n\n- Mocking internal collaborators\n- Testing private methods\n- Asserting on call counts/order\n- Test breaks when refactoring without behavior change\n- Test name describes HOW not WHAT\n- Verifying through external means instead of interface\n\n```typescript\n// BAD: Bypasses interface to verify\ntest(\"createUser saves to database\", async () => {\n await createUser({ name: \"Alice\" });\n const row = await db.query(\"SELECT * FROM users WHERE name = ?\", [\"Alice\"]);\n expect(row).toBeDefined();\n});\n\n// GOOD: Verifies through interface\ntest(\"createUser makes user retrievable\", async () => {\n const user = await createUser({ name: \"Alice\" });\n const retrieved = await getUser(user.id);\n expect(retrieved.name).toBe(\"Alice\");\n});\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1640,"content_sha256":"e12182f5c4c6a844b7c6ab4b8d2623b642aaaae5460fe58ed087efd4f3d5fb8a"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Test-Driven Development","type":"text"}]},{"type":"paragraph","content":[{"text":"Write test first. Commit it red. Write minimal code to pass. Commit green. Refactor.","type":"text"}]},{"type":"paragraph","content":[{"text":"Core principle:","type":"text","marks":[{"type":"strong"}]},{"text":" tests verify behavior through public interfaces, not implementation. Code can change entirely; tests shouldn't. A test that breaks when you rename an internal function — with no behavior change — was testing implementation. Delete it.","type":"text"}]},{"type":"paragraph","content":[{"text":"No watched failure = no proof the test tests the right thing. Commit history is the evidence; hooks check its structure.","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"tests.md","type":"text","marks":[{"type":"link","attrs":{"href":"tests.md","title":null}}]},{"text":" for examples and ","type":"text"},{"text":"mocking.md","type":"text","marks":[{"type":"link","attrs":{"href":"mocking.md","title":null}}]},{"text":" for mocking guidelines.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Iron Law","type":"text"}]},{"type":"paragraph","content":[{"text":"NO PRODUCTION CODE WITHOUT A FAILING TEST COMMITTED FIRST.","type":"text"}]},{"type":"paragraph","content":[{"text":"Wrote code before the test? Delete it. Implement fresh from tests. Delete means delete. Exceptions (throwaway prototypes, generated code, config) need human sign-off. Thinking \"skip TDD just this once\"? That is rationalization.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"1. Plan (before any code)","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the project's domain glossary so test names and interface vocabulary match the codebase; respect ADRs in the area you touch.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"List the ","type":"text"},{"text":"behaviors","type":"text","marks":[{"type":"em"}]},{"text":" to test, not implementation steps. Prioritize critical paths and complex logic — you can't test everything.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Design interfaces for ","type":"text"},{"text":"testability","type":"text","marks":[{"type":"link","attrs":{"href":"interface-design.md","title":null}}]},{"text":"; identify opportunities for ","type":"text"},{"text":"deep modules","type":"text","marks":[{"type":"link","attrs":{"href":"deep-modules.md","title":null}}]},{"text":" (small interface, deep implementation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confirm the public interface and the priority behaviors with the user, then proceed.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"2. Vertical slices, not horizontal","type":"text"}]},{"type":"paragraph","content":[{"text":"DO NOT write all tests, then all implementation. That is horizontal slicing and it produces crap tests: written in bulk they test ","type":"text"},{"text":"imagined","type":"text","marks":[{"type":"em"}]},{"text":" behavior and the ","type":"text"},{"text":"shape","type":"text","marks":[{"type":"em"}]},{"text":" of things (signatures, data structures), go insensitive to real changes, and commit you to test structure before you understand the code.","type":"text"}]},{"type":"paragraph","content":[{"text":"Work in vertical slices — one test → one implementation → repeat. Each test responds to what the last cycle taught you.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"WRONG (horizontal): RED: t1 t2 t3 t4 GREEN: i1 i2 i3 i4\nRIGHT (vertical): t1→i1 t2→i2 t3→i3 ...","type":"text"}]},{"type":"paragraph","content":[{"text":"The first slice is a tracer bullet: it proves the path works end to end.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"3. Commit protocol","type":"text"}]},{"type":"paragraph","content":[{"text":"One behavior = one RED commit + one GREEN commit. Multiple cycles per branch is fine; prefer one cycle in flight in Go/Rust repos (see markers).","type":"text"}]},{"type":"paragraph","content":[{"text":"Every commit — RED included — leaves the tree lint-clean.","type":"text","marks":[{"type":"strong"}]},{"text":" After each commit, run the repo's full hook suite over the whole tree (e.g. ","type":"text"},{"text":"prek run --all-files","type":"text","marks":[{"type":"code_inline"}]},{"text":"): exit code 0 and no files modified. An auto-fixer changing files IS a failure — apply the fix and amend it into the commit that introduced the problem before starting the next cycle. CI evaluating only the PR head never excuses a dirty intermediate commit; per-commit hygiene is verified here, at commit time, or not at all.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"STUB commit (only when the RED test needs a missing API surface)","type":"text"}]},{"type":"paragraph","content":[{"text":"A RED commit is red on ","type":"text"},{"text":"tests only","type":"text","marks":[{"type":"strong"}]},{"text":": lint, format, and type checks still pass on it. When the new test calls a function, method, or parameter that does not exist yet, type checking would fail — so first commit a minimal signature-only stub as its own commit, prefix ","type":"text"},{"text":"chore(stub):","type":"text","marks":[{"type":"code_inline"}]},{"text":". Signature + docstring, zero behavior: a new callable raises ","type":"text"},{"text":"NotImplementedError","type":"text","marks":[{"type":"code_inline"}]},{"text":"; a new parameter is accepted and ignored. This is the one sanctioned exception to the Iron Law: a stub defines ","type":"text"},{"text":"interface","type":"text","marks":[{"type":"em"}]},{"text":", and must contain no ","type":"text"},{"text":"implementation","type":"text","marks":[{"type":"em"}]},{"text":" — any logic in a stub commit is production code without a failing test. Skip this step entirely when the surface already exists.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"RED commit","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Write one failing test. One behavior, clear name, real code — no mocks unless unavoidable.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run it ","type":"text"},{"text":"unmarked","type":"text","marks":[{"type":"em"}]},{"text":"; watch it fail for the ","type":"text"},{"text":"right reason","type":"text","marks":[{"type":"strong"}]},{"text":" (feature missing — behavior wrong, assertion fails, or the stub's ","type":"text"},{"text":"NotImplementedError","type":"text","marks":[{"type":"code_inline"}]},{"text":" — never a typo, import error, or missing symbol). Passes immediately? It tests existing behavior — fix the test.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add the marker (below), suite green, commit. Tests only, prefix ","type":"text"},{"text":"test(red):","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Language","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Marker","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Strict?","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python (pytest)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"@pytest.mark.xfail(strict=True)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yes — XPASS fails suite","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TS/JS (vitest)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"test.fails(...)","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"it.fails(...)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TS/JS (jest)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"it.failing(...)","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Go","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"//go:build red","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"TestRed","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix + ","type":"text"},{"text":"red-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" job","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"aggregate only","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rust","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"#[ignore = \"red\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"red_","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix + ","type":"text"},{"text":"red-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" job","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"aggregate only","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Other","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"find a strict expected-failure mechanism; none exists → commit unmarked, tell the human the repo lacks red enforcement","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"n/a","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Go/Rust build-tag/ignore markers ","type":"text"},{"text":"exclude","type":"text","marks":[{"type":"em"}]},{"text":" tests from the normal suite, so a ","type":"text"},{"text":"red-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" CI job must run only the marked tests and expect failure (no-op when none exist). Its exit code is aggregate: two red tests in flight, one wrongly passing → the job still passes. Strict markers catch this per-test; the job doesn't.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"GREEN commit","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Simplest code that passes. YAGNI — no speculative generality.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Remove this cycle's red markers. No other test changes in this commit.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full suite green, output pristine. Fails? Fix the code, not the test. Prefix ","type":"text"},{"text":"feat:","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"fix:","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"REFACTOR (after green only)","type":"text"}]},{"type":"paragraph","content":[{"text":"Remove duplication, improve names, extract helpers, deepen modules. Tests stay green, no new behavior, separate commit. ","type":"text"},{"text":"Never refactor while red.","type":"text","marks":[{"type":"strong"}]},{"text":" Then start the next cycle.","type":"text"}]},{"type":"paragraph","content":[{"text":"After all tests pass, look for ","type":"text"},{"text":"refactor candidates","type":"text","marks":[{"type":"link","attrs":{"href":"refactoring.md","title":null}}]},{"text":":","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Enforcement — and its limits","type":"text"}]},{"type":"paragraph","content":[{"text":"lint-red.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (ships with skill; wire into prek + CI): ","type":"text"},{"text":"staged","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"commit","type":"text","marks":[{"type":"code_inline"}]},{"text":" modes check that ","type":"text"},{"text":"test(red):","type":"text","marks":[{"type":"code_inline"}]},{"text":" commits touch only test files and add a marker, others add none; ","type":"text"},{"text":"merge","type":"text","marks":[{"type":"code_inline"}]},{"text":" mode rejects any markers in tree; if Go/Rust markers are present, a ","type":"text"},{"text":"red-tests","type":"text","marks":[{"type":"code_inline"}]},{"text":" job must exist.","type":"text"}]},{"type":"paragraph","content":[{"text":"Hooks verify commit ","type":"text"},{"text":"structure","type":"text","marks":[{"type":"em"}]},{"text":" and marker hygiene. They do ","type":"text"},{"text":"not","type":"text","marks":[{"type":"strong"}]},{"text":" verify you ran the unmarked test and watched it fail for the right reason — that stays on you. Same for per-commit lint: an installed pre-commit hook blocks dirty commits at commit time, but where no hook is installed (fresh remote environments), the after-every-commit ","type":"text"},{"text":"--all-files","type":"text","marks":[{"type":"code_inline"}]},{"text":" run is manual discipline — do it anyway. Strict markers partially compensate (XPASS catches tests of already-existing behavior). \"Lint passed\" ≠ \"TDD verified.\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Good tests","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quality","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rule","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Exercises a real path through the public API; survives refactors","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Minimal","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One thing. \"and\" in the name? Split it.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Clear","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Name states the behavior, not ","type":"text"},{"text":"test1","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Honest","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tests the code, never the mock","type":"text"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"ts"},"content":[{"text":"// Good — tests real behavior (vitest; jest: it.failing)\ntest.fails('retries failed operations 3 times', async () => {\n let attempts = 0;\n const op = () => { attempts++; if (attempts \u003c 3) throw new Error('fail'); return 'ok'; };\n expect(await retryOperation(op)).toBe('ok');\n expect(attempts).toBe(3);\n});","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Red flags — STOP, delete, restart","type":"text"}]},{"type":"paragraph","content":[{"text":"Unmarked test passes before implementation exists · can't explain why the test failed · weakening an assertion in GREEN to make it pass · testing the mock · \"just this once\" · \"I'm being pragmatic, TDD is dogmatic.\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"testing-anti-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"testing-anti-patterns.md","title":null}}]},{"text":" to avoid common pitfalls.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rationalization table","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Excuse","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reality","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Too simple to test\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Simple code breaks. The test takes 30s.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"I'll test after\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tests-after are biased by the implementation: \"what does this do?\" not \"what should this?\"","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Deleting hours of code is wasteful\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sunk cost. Unverified code is debt.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Test is hard to write\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hard to test = hard to use. Listen to it; simplify the interface.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Must mock everything\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Too coupled. Inject dependencies.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"Lint passed, TDD done\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Hooks check structure, not that you watched the failure.","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When stuck / debugging","type":"text"}]},{"type":"paragraph","content":[{"text":"Don't know how to test → write the wished-for API and assertion first. Found a bug → write a failing test reproducing it, then run the full protocol. Never fix a bug without a test.","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"tdd","author":"@skillopedia","source":{"stars":1,"repo_name":"skills","origin_url":"https://github.com/pandoscope/skills/blob/HEAD/derived/tdd/SKILL.md","repo_owner":"pandoscope","body_sha256":"39b8321a30f5d9caf9c689c447b04b381a15c6dd7157ed1739530e309a7ea968","cluster_key":"f7ca341e95a4f7792f36e726eba423ac41f0edbc568b14f8dfd07699470b61f4","clean_bundle":{"format":"clean-skill-bundle-v1","source":"pandoscope/skills/derived/tdd/SKILL.md","text_bytes":16866,"source_commit":"925fe390437f8371bc1784b5b6890f28db70835e","skills_sh_rank":8845,"attachment_count":7,"text_attachments":7,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"derived/tdd/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"testing-qa","import_tag":"skills-sh-top20k-2026-11","description":"Test-driven development via red-green-refactor with an auditable commit protocol. Use when implementing any feature, bugfix, refactor, or behavior change before writing implementation code, or when the user mentions TDD, red-green-refactor, or test-first development.","metadata.derived-from":"merge of mattpocock/skills' `tdd` (https://github.com/mattpocock/skills/blob/7afa86d3a5dd96edde06ffa014e16c64e733681e/skills/engineering/tdd/SKILL.md) + obra/superpowers' `test-driven-development` (https://github.com/obra/superpowers/blob/030a222af19c1f3a93c6eb876a7422d8e4fc0162/skills/test-driven-development/SKILL.md)","metadata.derivation-note":"Commit protocol (red/green commits, markers, lint-red.sh) and the vertical-slice framing are the load-bearing original parts. Diff prose against upstream, not the protocol."}},"renderedAt":1793666754635}

Test-Driven Development Write test first. Commit it red. Write minimal code to pass. Commit green. Refactor. Core principle: tests verify behavior through public interfaces, not implementation. Code can change entirely; tests shouldn't. A test that breaks when you rename an internal function — with no behavior change — was testing implementation. Delete it. No watched failure = no proof the test tests the right thing. Commit history is the evidence; hooks check its structure. See tests.md for examples and mocking.md for mocking guidelines. Iron Law NO PRODUCTION CODE WITHOUT A FAILING TEST CO…