playbook/outfitter-agents/plugins/outfitter/skills/tdd/examples/bug-fix.md

421 lines
9.2 KiB
Markdown

# TDD Session: Bug Fix
TDD workflow for fixing a production bug with test reproduction first.
## Bug Report
**Issue**: Division by zero crashes calculator service
**Reporter**: User reported 500 error when calculating percentage with zero total
**Stack Trace**:
```
Error: Division by zero
at calculatePercentage (calculator.ts:45)
at handler (api/stats.ts:23)
```
**Priority**: High — causes service crash
## TDD Workflow
### Task Setup
```text
1. Write failing test reproducing bug [in_progress]
2. Fix bug with minimal code [pending]
3. Refactor if needed [pending]
4. Verify fix [pending]
```
## RED Phase: Reproduce Bug (4 min)
Confidence: `▓▓░░░` → Can reproduce, know the fix
First, understand the current implementation:
**Read**: `src/calculator/percentage.ts`
```typescript
export function calculatePercentage(value: number, total: number): number {
return (value / total) * 100
}
```
Issue clear: No validation for zero total.
**Write failing test**: `src/calculator/percentage.test.ts`
```typescript
import { describe, test, expect } from 'bun:test'
import { calculatePercentage } from './percentage'
describe('calculatePercentage', () => {
// Existing tests
test('calculates percentage correctly', () => {
expect(calculatePercentage(25, 100)).toBe(25)
expect(calculatePercentage(50, 200)).toBe(25)
})
// NEW: Bug reproduction test
test('handles division by zero gracefully', () => {
const result = calculatePercentage(10, 0)
expect(result).toBe(0)
// OR throw error approach:
// expect(() => calculatePercentage(10, 0)).toThrow('Total cannot be zero')
})
test('handles zero value with valid total', () => {
expect(calculatePercentage(0, 100)).toBe(0)
})
})
```
**Run test**: `bun test percentage.test.ts`
```
calculatePercentage
✓ calculates percentage correctly
✗ handles division by zero gracefully
Expected: 0
Received: Infinity
✓ handles zero value with valid total
1 failing
```
Bug reproduced successfully — test fails as expected.
**Commit**: `test: add failing test for division by zero bug`
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [in_progress]
3. Refactor if needed [pending]
4. Verify fix [pending]
```
## GREEN Phase: Fix Bug (3 min)
Confidence: `▓▓▓▓░` → Know exact fix needed
**Update**: `src/calculator/percentage.ts`
```typescript
export function calculatePercentage(value: number, total: number): number {
// Fix: Handle division by zero
if (total === 0) {
return 0
}
return (value / total) * 100
}
```
**Run tests**: `bun test percentage.test.ts`
```
calculatePercentage
✓ calculates percentage correctly
✓ handles division by zero gracefully
✓ handles zero value with valid total
3 passing
```
All tests pass — bug fixed.
**Verify no regressions**: `bun test`
```
All test suites passing (23 tests total)
```
No other tests broken — safe fix.
**Commit**: `fix: handle division by zero in percentage calculation`
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [in_progress]
4. Verify fix [pending]
```
## REFACTOR Phase: Consider Improvements (5 min)
Current fix works but could be more robust. Consider:
1. Should negative totals be handled?
2. Should we return error instead of 0?
3. Type safety improvements?
**Decision**: Add input validation, use Result type for better error handling.
**Refactored**: `src/calculator/percentage.ts`
```typescript
type PercentageResult =
| { type: 'success'; value: number }
| { type: 'error'; code: 'ZERO_TOTAL' | 'NEGATIVE_TOTAL' }
export function calculatePercentage(
value: number,
total: number
): PercentageResult {
// Validate total
if (total === 0) {
return { type: 'error', code: 'ZERO_TOTAL' }
}
if (total < 0) {
return { type: 'error', code: 'NEGATIVE_TOTAL' }
}
return {
type: 'success',
value: (value / total) * 100,
}
}
```
**Update tests** to match new signature:
```typescript
describe('calculatePercentage', () => {
test('calculates percentage correctly', () => {
const result1 = calculatePercentage(25, 100)
const result2 = calculatePercentage(50, 200)
expect(result1).toEqual({ type: 'success', value: 25 })
expect(result2).toEqual({ type: 'success', value: 25 })
})
test('returns error for division by zero', () => {
const result = calculatePercentage(10, 0)
expect(result).toEqual({
type: 'error',
code: 'ZERO_TOTAL',
})
})
test('returns error for negative total', () => {
const result = calculatePercentage(10, -100)
expect(result).toEqual({
type: 'error',
code: 'NEGATIVE_TOTAL',
})
})
test('handles zero value with valid total', () => {
const result = calculatePercentage(0, 100)
expect(result).toEqual({ type: 'success', value: 0 })
})
})
```
**Run tests**: `bun test percentage.test.ts`
```
calculatePercentage
✓ calculates percentage correctly
✓ returns error for division by zero
✓ returns error for negative total
✓ handles zero value with valid total
4 passing
```
**Update usage** in `api/stats.ts`:
```typescript
// Before
const percentage = calculatePercentage(completed, total)
return { percentage }
// After
const result = calculatePercentage(completed, total)
if (result.type === 'error') {
return { error: 'Invalid calculation', code: result.code }
}
return { percentage: result.value }
```
**Run all tests**: `bun test`
```
All tests passing (24 tests total)
```
No regressions — refactor successful.
**Commit**: `refactor: use Result type for percentage calculation errors`
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [completed]
4. Verify fix [in_progress]
```
## VERIFY Phase: Comprehensive Validation (4 min)
Run full test suite with coverage:
```bash
bun test --coverage
```
```
Coverage Summary:
percentage.ts: 100% statements, 100% branches
All tests passing (24 total)
```
Run mutation testing on updated code:
```bash
bun x stryker run --mutate "src/calculator/percentage.ts"
```
```
Mutation Score: 100%
All mutants killed
Mutations tested:
✓ === → !== (killed by "returns error for division by zero")
✓ < → <= (killed by "returns error for negative total")
✓ / → * (killed by "calculates percentage correctly")
✓ 0 → 1 (killed by "handles zero value with valid total")
```
Perfect mutation score — tests thoroughly verify behavior.
**Integration test** to verify API fix:
```typescript
describe('GET /api/stats', () => {
test('handles zero total gracefully', async () => {
const response = await request(app)
.get('/api/stats')
.query({ completed: 10, total: 0 })
expect(response.status).toBe(200)
expect(response.body).toMatchObject({
error: 'Invalid calculation',
code: 'ZERO_TOTAL',
})
})
})
```
**Run integration tests**: `bun test tests/integration/`
```
API Integration Tests
✓ handles zero total gracefully
All integration tests passing
```
**Task Update**:
```text
1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [completed]
4. Verify fix [completed]
```
## Session Summary
**Duration**: 16 minutes total
- RED: 4 min
- GREEN: 3 min
- REFACTOR: 5 min
- VERIFY: 4 min
**Bug**: Division by zero crash
**Fix**: Added validation with Result type
**Tests**: 4 new tests + 1 integration test
**Coverage**: 100% on changed code
**Mutation Score**: 100%
**Improvements beyond minimal fix**:
- Used discriminated union for error handling
- Added negative total validation
- Updated API to handle error results
- Added integration test
**Production deployment**:
- All tests passing
- No regressions detected
- Error handling verified
- Ready to deploy
## Key TDD Bug Fix Principles
1. **RED first**: Always reproduce bug with failing test before fixing
2. **Minimal GREEN**: Fix the immediate issue first
3. **Refactor for robustness**: Improve error handling and edge cases
4. **Verify thoroughly**: Run full suite + mutation tests + integration tests
5. **Document in test**: Test name describes the bug being fixed
## Anti-patterns Avoided
Avoided jumping straight to fix without test:
```typescript
// ❌ Wrong approach
// 1. See bug report
// 2. Add if (total === 0) return 0
// 3. Deploy and hope
// ✓ Correct TDD approach
// 1. Write failing test reproducing bug
// 2. Verify test fails
// 3. Add minimal fix
// 4. Verify test passes
// 5. Refactor for robustness
// 6. Verify with mutation testing
```
Avoided over-engineering initial fix:
```typescript
// ❌ Too complex for first fix
if (total === 0 || total < 0 || !isFinite(total) || isNaN(total)) {
throw new ValidationError(...)
}
// ✓ Minimal fix first (GREEN phase)
if (total === 0) {
return 0
}
// ✓ Then refactor with proper error handling (REFACTOR phase)
if (total === 0) {
return { type: 'error', code: 'ZERO_TOTAL' }
}
```
## Commit History
```
test: add failing test for division by zero bug
fix: handle division by zero in percentage calculation
refactor: use Result type for percentage calculation errors
```
Clean, focused commits showing TDD progression.