Reproduce, isolate, fix, and verify a bug using test-driven debugging
โฑ ~15 min ยท Hands-on
You'll fix a real bug reported by QA โ email validation rejecting valid plus signs โ using the test-first debugging approach. Write a failing test that reproduces the bug, find the root cause, fix it, and verify no regressions.
QA has filed a bug report:
Steps to reproduce: Try to register or log in with an email like user+tag@example.com.
Expected: Login succeeds (plus signs are valid in email local parts per RFC 5321).
Actual: 400 error โ "Invalid email format".
Impact: Users who use email aliases (Gmail plus addressing) cannot use the application.
Before touching code, restate the bug in precise technical terms:
Bug: Email validation regex rejects the "+" character in the local part of email addresses.
Root cause hypothesis: The regex pattern used for email validation
does not include "+" in its character class for the local part.
Affected component: src/validators/email-validator.js (or equivalent)
Restating the bug in your own words forces you to understand it. A vague understanding leads to a vague fix. "Login is broken" could mean anything. "Regex rejects valid + character" tells you exactly where to look and what to fix.
Before writing any code, create a branch for the bug fix:
/agile-code-branch
๐ฟ Branch created: fix/BUG-042-email-plus-sign
Source: main
Convention: fix/[BUG-ID]-[short-description]
Bug link: BUG-042 โ Login fails with plus sign in email
Switched to branch 'fix/BUG-042-email-plus-sign'
The first thing you do with any bug is prove it exists with a test:
/agile-code-tdd
๐ด RED โ Writing test to reproduce bug BUG-042...
// test/validators/email-validator.test.js
describe('EmailValidator', () => {
describe('plus sign handling', () => {
it('should_accept_email_with_plus_sign', () => {
const result = EmailValidator.isValid('user+tag@example.com');
expect(result).toBe(true);
});
});
});
Running test...
โ should_accept_email_with_plus_sign โ FAILED
Expected: true
Received: false
1 failing โ Bug reproduced! The test proves the defect exists.
A failing test that reproduces the bug is the most valuable artifact in bug fixing. It proves the bug exists, it defines exactly what "fixed" means, and it prevents the bug from ever coming back (regression).
Now look at the actual validation code:
// src/validators/email-validator.js (current โ buggy)
class EmailValidator {
static EMAIL_REGEX = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
static isValid(email) {
return this.EMAIL_REGEX.test(email);
}
}
Found it. The character class [a-zA-Z0-9._-] allows dots, underscores, and hyphens in the local part โ but not plus signs. The + character is missing from the regex.
Email validation is notoriously tricky. The original developer covered the most common characters but missed that + is valid per RFC 5321. This is why tests against edge cases matter โ they catch what human intuition misses.
Add the + character to the regex character class:
// src/validators/email-validator.js (fixed)
class EmailValidator {
static EMAIL_REGEX = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
static isValid(email) {
return this.EMAIL_REGEX.test(email);
}
}
The only change: [a-zA-Z0-9._-] became [a-zA-Z0-9._+-] โ adding + to the allowed characters.
๐ข GREEN โ Running reproduction test...
โ should_accept_email_with_plus_sign โ PASSED
1 passing โ Bug is fixed!
The same test that proved the bug exists now proves it's fixed. No ambiguity, no manual testing needed. This test will run in CI forever, ensuring this bug never returns.
One fix is good, but comprehensive coverage is better. Add tests for related edge cases:
// Additional edge case tests
describe('EmailValidator โ edge cases', () => {
it('should_accept_email_with_dots', () => {
expect(EmailValidator.isValid('first.last@example.com')).toBe(true);
});
it('should_accept_email_with_hyphens', () => {
expect(EmailValidator.isValid('first-last@example.com')).toBe(true);
});
it('should_accept_email_with_multiple_plus_signs', () => {
expect(EmailValidator.isValid('user+tag+extra@example.com')).toBe(true);
});
it('should_reject_email_without_at_sign', () => {
expect(EmailValidator.isValid('userexample.com')).toBe(false);
});
it('should_reject_email_without_domain', () => {
expect(EmailValidator.isValid('user@')).toBe(false);
});
});
Running all email validation tests...
โ should_accept_email_with_plus_sign โ PASSED
โ should_accept_email_with_dots โ PASSED
โ should_accept_email_with_hyphens โ PASSED
โ should_accept_email_with_multiple_plus_signs โ PASSED
โ should_reject_email_without_at_sign โ PASSED
โ should_reject_email_without_domain โ PASSED
6 passing โ Full edge case coverage.
When you fix a bug, always add tests for nearby edge cases. If + was missing, what else might be? Test dots, hyphens, multiple specials, and invalid formats. This hardens the area around the fix.
Verify no regressions across the entire test suite:
/agile-code-ci
๐ Running CI pipeline locally...
Build:
โ
Compilation successful
Tests:
โ
48 tests passing (was 42 โ added 6 edge case tests)
โ
Coverage: 96% lines
โ
0 regressions โ all existing tests still pass
Lint:
โ
ESLint: 0 errors
โ
Prettier: all formatted
Security:
โ
No vulnerabilities
โ
All CI checks pass. No regressions detected.
The fix changed one character in a regex โ but CI verified that all 48 tests pass, including the 42 that existed before. This confidence is what lets you ship a bug fix quickly without fear.
/agile-code-commit
๐ฆ Commit created:
fix(auth): accept plus signs in email validation
The email regex was missing "+" from the local part character class,
causing emails like user+tag@example.com to be rejected. Added "+"
to the regex and 6 edge case tests for comprehensive coverage.
Fixes: BUG-042
/agile-code-pr
๐ Pull Request created: #22
Title: fix(auth): accept plus signs in email validation
Branch: fix/BUG-042-email-plus-sign โ main
Bug: BUG-042
Changes:
~1 file modified: src/validators/email-validator.js (1 character added)
+1 file modified: test/validators/email-validator.test.js (6 tests added)
Test coverage: 96%
CI status: โ
All checks passing
/agile-code-pr-review
๐ Code Review โ PR #22
โโโโโโโโโโโโโโโโโโโโโโโ
Findings:
โ
Fix is minimal and correct โ only the necessary character added
โ
No over-engineering (didn't rewrite the entire validator)
โ
Edge case tests are thorough
โ
Commit message follows conventions with Fixes: BUG-042
Notes:
@lead: "Clean fix. Consider switching to a battle-tested email
validation library (e.g., validator.js) in a future sprint to
avoid maintaining our own regex. Not blocking this PR."
Verdict: โ
APPROVED by @lead
With the PR approved, merge the fix branch into main:
/agile-code-merge
๐ Merge โ PR #22
โโโโโโโโโโโโโโโโโโโ
Pre-merge checks:
โ
PR approved by @lead
โ
CI passing on branch
โ
No merge conflicts
โ
Branch up to date with main
Merging: fix/BUG-042-email-plus-sign โ main
Strategy: squash merge (project convention)
Merge complete.
Branch fix/BUG-042-email-plus-sign deleted (was 9c4e1a2).
โ
PR #22 merged to main.
If writing a failing test doesn't reproduce the bug (e.g., it only happens in production, under load, or with specific data), use /agile-code-debug for systematic investigation:
/agile-code-debug
๐ Debug Mode โ Systematic Investigation
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hypothesis 1: Race condition under concurrent requests
Test: Run 100 concurrent login requests with valid emails
Result: All pass โ Not a race condition
Hypothesis 2: Character encoding issue in production
Test: Send email with UTF-8 encoding header vs ASCII
Result: Both pass locally โ Might be environment-specific
Hypothesis 3: Database collation rejecting special characters
Test: Direct DB query with plus sign email
Result: โ Query returns empty! โ Found it!
Root cause: DB column uses C collation, not UTF-8
Attempts: 3/5 (max 5 before escalation)
Status: Root cause identified
The /agile-code-debug command forms hypotheses and tests them one at a time, with a maximum of 5 attempts. If all 5 fail, it escalates with a diagnostic capture instead of spinning endlessly. This prevents the "I've been debugging for 3 hours" trap.
What's the first thing to do when fixing a bug?
fix(scope) convention and reference the bug ID