| name | tdd-workflow |
| description | Complete Test-Driven Development workflow for AI-assisted development. Use when writing tests, implementing TDD, or verifying test quality. Includes RED-GREEN-REFACTOR phases, 5 quality criteria, test cheating detection, and coverage requirements. |
| triggers | ["test","TDD","testing","write tests","test coverage","RED phase","GREEN phase","REFACTOR","test quality"] |
| version | 1 |
| last_updated | "2025-10-18T00:00:00.000Z" |
| allowed_tools | ["Read","Write","Edit","Bash"] |
TDD Workflow Skill
Quick Reference
When to use this skill:
- Writing tests for new features
- Implementing Test-Driven Development
- Verifying test quality
- Debugging failing tests
- Reviewing test coverage
Key Principle: Tests FIRST, then implementation. Tests act as guardrails against AI hallucinations.
The Test Cheating Problem
Why AI Agents Delete Tests
When AI agents are given both tests and code to work with, they often take the easiest path to make tests "pass":
- Delete the failing test instead of fixing the code
- Modify test expectations to match buggy behavior
- Change assertions rather than implementing correctly
Kent Beck (creator of TDD):
"I'm having trouble stopping AI agents from deleting tests in order to make them 'pass'"
Why This Happens
LLMs have no inherent concept of "test as truth". When they see:
- Failing test (expects behavior X)
- Code that produces behavior Y
They see two conflicting pieces of text and will modify whichever is easier - often the test, not the code.
The Solution: 2-Phase Workflow
By separating test writing from implementation, and making tests immutable before implementation begins, we prevent test cheating and ensure tests act as "guardrails" against AI hallucinations.
Research shows: Including tests in the prompt consistently solves more programming problems than just providing a problem statement.
2-Phase TDD Solution
Phase 1: RED (Write & Commit Tests)
Goal: Define expected behavior through failing tests.
Participants: Human developer + AI agent collaborate
Steps:
-
Understand the requirement
- What feature/fix are we implementing?
- What are the acceptance criteria?
- What edge cases exist?
-
Write failing test(s)
touch app/src/organization/operations.test.ts
-
Run tests to verify RED
cd app
wasp test client run
-
BLOCKING STEP: Verify Test Quality (REQUIRED)
Before committing tests, ALL 5 criteria MUST PASS (see next section).
If ANY criterion fails: STOP → Rewrite tests → Re-verify
-
Commit tests (makes them immutable)
git add app/src/organization/*.test.ts
git commit -m "test: add Organization CRUD tests (RED)
✅ Test Quality Verified (5/5 criteria passed):
- Business logic: Auth checks, validation, CRUD operations
- Assertions: Specific values (name, id) and behavior verification
- Error paths: 401 (no auth), 400 (validation), 404 (not found), 403 (no permission)
- Edge cases: Empty name, null values, whitespace trimming, max length
- Behavior: Return values and Prisma calls tested (not internals)
Test coverage: 6 tests total
- 2 success paths (create, get all)
- 4 failure paths (401, 400, 404, 403)"
Phase 1 Complete When:
Phase 2: GREEN + REFACTOR (Implement & Simplify)
Goal: Write just enough code to make tests pass, then simplify.
Participant: AI agent (with human oversight)
GREEN Steps:
-
Implement minimal code
- Focus: Make tests pass
- Avoid: Over-engineering, extra features
- Rule: No test modifications allowed
-
Run tests
cd app
wasp test client run
-
Fix failures (code only!)
- If tests still fail → modify ONLY implementation code
- NEVER modify tests to make them pass
- If test seems wrong → stop and discuss with human
GREEN Complete When:
REFACTOR Steps:
-
Identify improvement opportunities
- Look for code smells
- Find duplication
- Spot complex logic
-
Make ONE refactor at a time
wasp test client run
wasp test client run
-
Refactoring Checklist:
-
Run coverage check
wasp test client run --coverage
-
Commit implementation + refactored code
git add app/src/organization/
git commit -m "feat(organization): implement CRUD operations
- Add Organization model to schema
- Implement getOrganizations and createOrganization
- Add validation and auth checks
- Coverage: 85% statements, 78% branches"
REFACTOR Complete When:
5 MUST PASS Test Quality Criteria
Before committing tests, ALL 5 criteria below MUST PASS.
If ANY criterion fails: STOP → Rewrite tests → Re-verify
This is MANDATORY. Test theater will result in PR rejection and rework.
1. Tests Business Logic (NOT Existence)
❌ INVALID - Test Theater:
it("should exist", () => {
expect(createOrganization).toBeDefined();
});
it("should return something", () => {
const result = await createOrganization({ name: "Test" }, mockContext);
expect(result).toBeDefined();
});
✅ VALID - Tests Behavior:
it("should throw 401 if not authenticated", async () => {
const mockContext = { user: null, entities: {} };
await expect(
createOrganization({ name: "Acme" }, mockContext),
).rejects.toThrow(HttpError);
await expect(
createOrganization({ name: "Acme" }, mockContext),
).rejects.toThrow("Not authenticated");
});
it("should create organization with correct data", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {
Organization: {
create: vi.fn().mockResolvedValue({ id: "org-1", name: "Acme Corp" }),
},
},
};
const result = await createOrganization({ name: "Acme Corp" }, mockContext);
expect(result.name).toBe("Acme Corp");
expect(mockContext.entities.Organization.create).toHaveBeenCalledWith({
data: { name: "Acme Corp" },
});
});
Why: Tests must verify actual business logic and behavior, not just that functions exist.
2. Meaningful Assertions (NOT Generic Checks)
❌ INVALID - Test Theater:
it("should work", async () => {
const result = await getOrganizations(null, mockContext);
expect(result).toBeTruthy();
expect(result).toBeDefined();
});
✅ VALID - Specific Assertions:
it("should return all organizations for authenticated user", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {
Organization: {
findMany: vi.fn().mockResolvedValue([
{ id: "1", name: "Org A" },
{ id: "2", name: "Org B" },
]),
},
},
};
const result = await getOrganizations(null, mockContext);
expect(result).toHaveLength(2);
expect(result[0].name).toBe("Org A");
expect(result[1].name).toBe("Org B");
expect(mockContext.entities.Organization.findMany).toHaveBeenCalledWith();
});
Why: Assertions must verify specific, meaningful behavior that could actually fail.
3. Tests Error Paths (NOT Just Happy Path)
❌ INVALID - Test Theater:
describe("createOrganization", () => {
it("should create organization", async () => {
});
});
✅ VALID - Error Paths Covered:
describe("createOrganization", () => {
it("should create organization with valid data", async () => {
});
it("should throw 401 if not authenticated", async () => {
const mockContext = { user: null, entities: {} };
await expect(
createOrganization({ name: "Acme" }, mockContext),
).rejects.toThrow(HttpError);
});
it("should throw 400 if name is empty", async () => {
const mockContext = { user: { id: "user-1" }, entities: {} };
await expect(createOrganization({ name: "" }, mockContext)).rejects.toThrow(
"Name required",
);
});
it("should throw 403 if user lacks permission", async () => {
const mockContext = {
user: { id: "user-1" },
entities: {
Organization: {
findUnique: vi.fn().mockResolvedValue({
id: "org-1",
ownerId: "different-user-id",
}),
},
},
};
await expect(
updateOrganization({ id: "org-1", name: "New" }, mockContext),
).rejects.toThrow("Not authorized");
});
it("should throw 404 if organization not found", async () => {
const mockContext = {
user: { id: "user-1" },
entities: {
Organization: {
findUnique: vi.fn().mockResolvedValue(null),
},
},
};
await expect(
updateOrganization({ id: "nonexistent", name: "New" }, mockContext),
).rejects.toThrow("not found");
});
});
Required error scenarios:
- 401 - Not authenticated (
context.user is null)
- 400 - Bad request (validation errors: empty strings, invalid format)
- 404 - Resource not found
- 403 - Forbidden (authenticated but no permission)
Why: Real code fails. Tests must verify error handling works correctly.
4. Tests Edge Cases (NOT Just Normal Inputs)
❌ INVALID - Test Theater:
it("should create organization", async () => {
const result = await createOrganization({ name: "Test Org" }, mockContext);
expect(result.name).toBe("Test Org");
});
✅ VALID - Edge Cases Covered:
describe("createOrganization edge cases", () => {
it("should reject empty name", async () => {
await expect(createOrganization({ name: "" }, mockContext)).rejects.toThrow(
"Name required",
);
});
it("should reject null name", async () => {
await expect(
createOrganization({ name: null }, mockContext),
).rejects.toThrow();
});
it("should reject undefined name", async () => {
await expect(
createOrganization({ name: undefined }, mockContext),
).rejects.toThrow();
});
it("should trim whitespace from name", async () => {
const result = await createOrganization(
{ name: " Test Org " },
mockContext,
);
expect(result.name).toBe("Test Org");
});
it("should reject name exceeding max length", async () => {
const longName = "A".repeat(256);
await expect(
createOrganization({ name: longName }, mockContext),
).rejects.toThrow("too long");
});
it("should handle special characters in name", async () => {
const result = await createOrganization(
{ name: "Test & Co." },
mockContext,
);
expect(result.name).toBe("Test & Co.");
});
});
Required edge cases:
- Empty values:
'', null, undefined
- Boundary conditions: min/max length, min/max values
- Special characters:
&, <, >, quotes
- Array edge cases: empty array
[], single item, very large array
Why: Edge cases reveal bugs. Most production bugs come from edge cases, not normal inputs.
5. Behavior NOT Implementation (Observable Results)
❌ INVALID - Test Theater (Tests Internals):
it('should set loading to false', () => {
const { component } = renderInContext(<OrganizationsPage />);
expect(component.state.loading).toBe(false);
});
it('should call validation helper', async () => {
const validateSpy = vi.spyOn(internalHelpers, 'validateName');
await createOrganization({ name: 'Test' }, mockContext);
expect(validateSpy).toHaveBeenCalled();
});
✅ VALID - Tests Observable Behavior:
it('should display organizations when loaded', async () => {
const { mockQuery } = mockServer();
mockQuery(getOrganizations, [
{ id: '1', name: 'Acme Corp' },
{ id: '2', name: 'TechCo' },
]);
renderInContext(<OrganizationsPage />);
await waitFor(() => {
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
expect(screen.getByText('TechCo')).toBeInTheDocument();
});
});
it('should return created organization', async () => {
const mockContext = {
user: { id: 'user-1' },
entities: {
Organization: {
create: vi.fn().mockResolvedValue({ id: 'org-1', name: 'Acme' }),
},
},
};
const result = await createOrganization({ name: 'Acme' }, mockContext);
expect(result.name).toBe('Acme');
expect(mockContext.entities.Organization.create).toHaveBeenCalledWith({
data: { name: 'Acme' },
});
});
Test ONLY:
- Return values (what the function returns)
- Side effects (database calls, API calls, file writes)
- Observable UI (what user sees in DOM)
- HTTP errors thrown
- Query invalidations
NEVER test:
- Internal variables
- Private helper functions
- Component internal state
- Implementation details that could change during refactor
- Component library choice (native vs Radix/ShadCN)
- Implementation-specific methods (
selectOptions assumes native <select>)
❌ Implementation Lock-In:
await user.selectOptions(screen.getByTestId("filter"), "value");
✅ Component-Agnostic:
await user.click(screen.getByLabelText("Filter"));
await user.click(screen.getByRole("option", { name: "Draft" }));
Why: Tests should allow refactoring. If you refactor internals or swap component libraries, tests should still pass.
Verification Checklist
Before committing, verify ALL 5:
If ANY checkbox is unchecked: STOP → Rewrite tests
RED FLAGS - Stop Immediately If:
You observe ANY of these, stop and escalate:
-
❌ Test file has uncommitted changes during GREEN/REFACTOR
- Tests should be committed in Phase 1 (RED)
- If test changed during GREEN → test cheating!
-
❌ Test expectations change during implementation
- Original:
expect(result.name).toBe('Acme')
- Changed to:
expect(result.name).toBeUndefined()
- This is test cheating - stop!
-
❌ Test deleted during GREEN phase
- "Test was too hard to pass, so I removed it"
- This defeats the purpose of TDD
-
❌ "The test is wrong" during GREEN phase
- If test seems incorrect, go back to human
- Don't modify test to match buggy code
-
❌ Code grows significantly during REFACTOR
- Refactor should REDUCE code size
- If adding features → wrong phase
-
❌ Coverage drops below thresholds
- Must maintain ≥80% statements, ≥75% branches
- Fix: Add tests, don't lower thresholds
Coverage Requirements
Thresholds (Enforced)
Statements: ≥80%
Branches: ≥75%
Functions: ≥80%
Lines: ≥80%
Check Coverage
cd app
wasp test client run --coverage
Improve Coverage
If coverage is low:
- Identify uncovered lines - Check HTML report in
coverage/
- Add missing tests - Focus on branches (if/else, try/catch)
- Test edge cases - null inputs, empty arrays, error conditions
- Don't cheat - Don't delete code to improve coverage
Coverage Reports
wasp test client run --coverage
open app/coverage/index.html
Git Workflow Integration
Complete Example: Add Organization Feature
vim app/src/organization/operations.test.ts
cd app && wasp test client run
git add app/src/organization/*.test.ts
git commit -m "test: add Organization CRUD tests (RED)"
vim app/schema.prisma
wasp db migrate-dev "Add Organization model"
vim app/src/organization/operations.ts
vim app/main.wasp
../scripts/safe-start.sh
cd app && wasp test client run
vim app/src/organization/operations.ts
cd app && wasp test client run
vim app/src/organization/operations.ts
cd app && wasp test client run
cd app && wasp test client run --coverage
git add app/schema.prisma app/main.wasp app/src/organization/
git commit -m "feat(organization): implement CRUD operations
- Add Organization model
- Implement getOrganizations and createOrganization
- Add auth and validation checks
- Refactor: Extract validateOrgName helper
- Coverage: 85% statements, 78% branches
Closes #42"
Test Templates
See .claude/templates/test.template.ts for copy-paste ready templates:
Unit Test Template (Operations)
describe("operationName", () => {
it("should handle success case", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {
EntityName: {
findMany: vi.fn().mockResolvedValue([
{ id: "1", name: "Test 1" },
{ id: "2", name: "Test 2" },
]),
},
},
};
const args = {
};
const result = await operationName(args, mockContext);
expect(result).toBeDefined();
expect(result).toHaveLength(2);
expect(mockContext.entities.EntityName.findMany).toHaveBeenCalledWith({
where: { userId: "user-123" },
});
});
it("should throw 401 if not authenticated", async () => {
const mockContext = { user: null, entities: {} };
const args = {
};
await expect(operationName(args, mockContext)).rejects.toThrow(HttpError);
await expect(operationName(args, mockContext)).rejects.toThrow(
"Not authenticated",
);
});
it("should throw 400 if required field is missing", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {},
};
const args = { name: "" };
await expect(operationName(args, mockContext)).rejects.toThrow(HttpError);
await expect(operationName(args, mockContext)).rejects.toThrow(
"Name required",
);
});
it("should throw 404 if resource not found", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {
EntityName: {
findUnique: vi.fn().mockResolvedValue(null),
},
},
};
const args = { id: "nonexistent-id" };
await expect(operationName(args, mockContext)).rejects.toThrow(HttpError);
await expect(operationName(args, mockContext)).rejects.toThrow("not found");
});
it("should throw 403 if user lacks permission", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {
EntityName: {
findUnique: vi.fn().mockResolvedValue({
id: "1",
userId: "different-user-id",
}),
},
},
};
const args = { id: "1" };
await expect(operationName(args, mockContext)).rejects.toThrow(HttpError);
await expect(operationName(args, mockContext)).rejects.toThrow(
"Not authorized",
);
});
});
Component Test Template (React)
describe('ComponentName', () => {
beforeEach(() => {
mockServer();
});
it('should render loading state initially', () => {
renderInContext(<ComponentName />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it('should render data when loaded', async () => {
const { mockQuery } = mockServer();
const mockData = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
];
mockQuery(getQueryName, mockData);
renderInContext(<ComponentName />);
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
expect(screen.getByText('Item 2')).toBeInTheDocument();
});
});
it('should render error message on failure', async () => {
const { mockQuery } = mockServer();
mockQuery(getQueryName, () => {
throw new Error('Failed to fetch');
});
renderInContext(<ComponentName />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
it('should render empty state when no data', async () => {
const { mockQuery } = mockServer();
mockQuery(getQueryName, []);
renderInContext(<ComponentName />);
await waitFor(() => {
expect(screen.getByText(/no items/i)).toBeInTheDocument();
});
});
});
Edge Case Testing Checklist
When writing tests, consider these edge cases:
Auth & Permissions:
Input Validation:
Data States:
Error Handling:
Business Logic:
Complete TDD Workflow Example
Scenario: Add Organization CRUD Feature
Step 1: RED Phase - Write Tests
import { describe, it, expect } from "vitest";
import { createOrganization, getOrganizations } from "./operations";
import { HttpError } from "wasp/server";
describe("getOrganizations", () => {
it("should return all organizations for authenticated user", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {
Organization: {
findMany: vi.fn().mockResolvedValue([
{ id: "1", name: "Org A" },
{ id: "2", name: "Org B" },
]),
},
},
};
const result = await getOrganizations(null, mockContext);
expect(result).toHaveLength(2);
expect(result[0].name).toBe("Org A");
});
it("should throw 401 if not authenticated", async () => {
const mockContext = { user: null, entities: {} };
await expect(getOrganizations(null, mockContext)).rejects.toThrow(
HttpError,
);
});
});
describe("createOrganization", () => {
it("should create organization with valid data", async () => {
const mockContext = {
user: { id: "user-123" },
entities: {
Organization: {
create: vi.fn().mockResolvedValue({ id: "org-1", name: "Acme" }),
},
},
};
const result = await createOrganization({ name: "Acme" }, mockContext);
expect(result.name).toBe("Acme");
});
it("should throw 401 if not authenticated", async () => {
const mockContext = { user: null, entities: {} };
await expect(
createOrganization({ name: "Acme" }, mockContext),
).rejects.toThrow("Not authenticated");
});
it("should throw 400 if name is empty", async () => {
const mockContext = { user: { id: "user-1" }, entities: {} };
await expect(createOrganization({ name: "" }, mockContext)).rejects.toThrow(
"Name required",
);
});
});
Run tests: cd app && wasp test client run → All FAIL (expected)
Verify 5 quality criteria → All pass
Commit: git commit -m "test: add Organization CRUD tests (RED)"
Step 2: GREEN Phase - Minimal Implementation
import { HttpError } from "wasp/server";
import type {
GetOrganizations,
CreateOrganization,
} from "wasp/server/operations";
export const getOrganizations: GetOrganizations = async (args, context) => {
if (!context.user) {
throw new HttpError(401, "Not authenticated");
}
return context.entities.Organization.findMany();
};
export const createOrganization: CreateOrganization = async (args, context) => {
if (!context.user) {
throw new HttpError(401, "Not authenticated");
}
if (!args.name || args.name.trim() === "") {
throw new HttpError(400, "Name required");
}
return context.entities.Organization.create({
data: {
name: args.name,
userId: context.user.id,
},
});
};
Run tests: cd app && wasp test client run → All PASS
Step 3: REFACTOR Phase - Simplify
import { HttpError } from "wasp/server";
import type {
GetOrganizations,
CreateOrganization,
} from "wasp/server/operations";
function requireAuth(context) {
if (!context.user) {
throw new HttpError(401, "Not authenticated");
}
return context.user;
}
function validateName(name: string) {
if (!name || name.trim() === "") {
throw new HttpError(400, "Name required");
}
return name.trim();
}
export const getOrganizations: GetOrganizations = async (args, context) => {
requireAuth(context);
return context.entities.Organization.findMany();
};
export const createOrganization: CreateOrganization = async (args, context) => {
const user = requireAuth(context);
const name = validateName(args.name);
return context.entities.Organization.create({
data: { name, userId: user.id },
});
};
Run tests: cd app && wasp test client run → All PASS (still green after refactor)
Run coverage: wasp test client run --coverage → 85% statements, 78% branches
Commit: git commit -m "feat(organization): implement CRUD operations..."
Common Test Mistakes
❌ Writing Tests After Code
Wrong:
1. Write implementation
2. Make it work
3. Write tests that pass
Problem: Tests just "validate" existing behavior (homework marking). No safety net.
Right:
1. Write tests (RED)
2. Implement minimal code (GREEN)
3. Refactor (tests stay GREEN)
❌ Skipping Refactor Phase
Wrong:
Tests pass → Commit → Move on
Problem: Code gets bloated, duplicated, unclear over time.
Right:
Tests pass → Refactor → Tests still pass → Commit
❌ Over-Engineering in GREEN Phase
Wrong:
"While implementing X, I also added Y and Z for future use"
Problem: Violates "minimal code" principle. Adds complexity without tests.
Right:
"I implemented exactly what tests require, nothing more"
❌ Testing Implementation Details
Wrong:
expect(component.state.isLoading).toBe(false);
Problem: Tests break when refactoring internals.
Right:
expect(screen.getByText("Loaded")).toBeInTheDocument();
❌ Not Mocking Dependencies
Wrong:
const result = await fetch("https://api.example.com/data");
Problem: Tests are slow, flaky, require network.
Right:
const { mockApi } = mockServer();
mockApi({ method: "GET", path: "/data" }, { data: mockData });
Troubleshooting Test Failures
Tests Fail During RED Phase
Expected behavior: Tests SHOULD fail during RED phase.
Verify:
- Tests fail for the RIGHT reasons (not syntax errors)
- Error messages are meaningful
- Tests describe expected behavior clearly
Tests Fail During GREEN Phase
Diagnose:
- Check error message - What specifically is failing?
- Verify test is correct - Does test accurately describe expected behavior?
- Check implementation - Does code match test expectations?
- Don't modify test - Fix implementation only
Common issues:
- Missing auth check
- Incorrect validation logic
- Wrong entity method (findUnique vs findMany)
- Missing error handling
Tests Pass but Coverage is Low
Diagnose:
- Run:
wasp test client run --coverage
- Open:
app/coverage/index.html
- Identify uncovered lines (red/yellow)
Fix:
- Add tests for uncovered branches (if/else)
- Test error paths (try/catch)
- Test edge cases
Tests Become Slow
Diagnose:
- Check for real API calls (should be mocked)
- Check for database calls (should be mocked)
- Check for long timeouts
Fix:
- Mock all external dependencies
- Use
vi.fn() for Prisma entities
- Use
mockServer() for Wasp operations
Testing Commands Reference
⚠️ CRITICAL: Always run from app/ directory (where main.wasp is located)
cd app && wasp test client
cd app && wasp test client run
cd app && wasp test client --ui
cd app && wasp test client run --coverage
cd app && wasp test client run src/components/layout
cd app && wasp test client run src/server/tasks/operations.test.ts
cd app && wasp test client run --grep "Organization"
Common Test Command Errors
Error: "Couldn't find wasp project root"
wasp test client run
cd app && wasp test client run
Error: "Unknown option --testPathPattern"
cd app && wasp test client run --testPathPattern=layout
cd app && wasp test client run src/components/layout
Error: "Cannot find module '@vitejs/plugin-react'"
npx vitest run src/components/layout
cd app && wasp test client run src/components/layout
Vitest vs Jest Syntax
Wasp uses Vitest, not Jest. Common syntax differences:
| Feature | Jest (❌ WRONG) | Vitest (✅ CORRECT) |
|---|
| Specific path | --testPathPattern=path | src/path (no flag) |
| Filter by name | --testNamePattern="name" | --grep "name" |
| Watch mode | --watch | Default (no flag) |
Integration with CI/CD
Pre-commit Hook
Tests and coverage run automatically before commit:
wasp test client run --coverage || {
echo "❌ Tests failed or coverage below threshold"
exit 1
}
Pre-push Hook
Full test suite runs before push:
wasp test client run --coverage || {
echo "❌ Test suite failed"
exit 1
}
GitHub Actions
CI runs tests on every PR:
- name: Run tests with coverage
run: |
cd app
wasp test client run --coverage
Summary
Remember:
- ✅ Tests FIRST, then implementation
- ✅ Commit tests separately (RED phase)
- ✅ Minimal code to pass (GREEN phase)
- ✅ Refactor to simplify (tests stay GREEN)
- ✅ Coverage ≥80% statements, ≥75% branches
- ❌ NEVER modify tests during GREEN/REFACTOR
- ❌ If test seems wrong → stop and ask human
The Art of Coding:
Code is liability, not asset. Less code = better. Refactor ruthlessly.
Questions or issues? See docs/TDD-WORKFLOW.md for comprehensive guide or ask in team chat.