| name | tdd |
| description | Enforce test-driven development โ write tests FIRST, then implement. Mandatory for new features and bug fixes. |
TDD โ Test-Driven Development (Enforced)
This is not a suggestion. This is the workflow.
When adding features or fixing bugs in claude-code-discord-bridge, you MUST write tests before writing implementation code. No exceptions.
When to Activate
- Adding any new feature or functionality
- Fixing a bug (write a test that reproduces the bug FIRST)
- Refactoring code (ensure existing tests pass, add missing ones)
- Adding a new Cog, parser rule, UI component, or database operation
The Cycle: RED โ GREEN โ REFACTOR
Step 1: RED โ Write Failing Tests
Write tests that describe the desired behavior. Run them. They must fail.
uv run pytest tests/test_new_feature.py -v
If tests pass before you write any implementation, your tests are wrong.
Step 2: GREEN โ Write Minimal Implementation
Write the minimum code to make the tests pass. No more.
uv run pytest tests/test_new_feature.py -v
Step 3: REFACTOR โ Clean Up
Improve the implementation while keeping all tests green. Then run the full suite:
uv run pytest tests/ -v --cov=claude_discord --cov-report=term-missing
Step 4: VERIFY โ Lint + Format + Full Suite
uv run ruff check claude_discord/
uv run ruff format claude_discord/
uv run pytest tests/ -v --cov=claude_discord
What to Test First (by module type)
New Parser Rule
def test_parse_new_event_type():
line = '{"type":"new_type","data":"value"}'
event = parse_line(line)
assert event is not None
assert event.message_type == MessageType.ASSISTANT
def test_parse_new_event_type_missing_data():
line = '{"type":"new_type"}'
event = parse_line(line)
assert event is not None
New Cog Command
from unittest.mock import AsyncMock, MagicMock
def test_rejects_unauthorized_user():
cog = MyCog(bot=MagicMock(), allowed_user_ids={111})
assert cog._is_authorized(999) is False
def test_accepts_authorized_user():
cog = MyCog(bot=MagicMock(), allowed_user_ids={111})
assert cog._is_authorized(111) is True
def test_validates_input_format():
assert not re.match(r"^[\w-]+$", "invalid; rm -rf /")
New Chunker Behavior
def test_chunk_with_nested_code_blocks():
text = "```python\n```nested```\n```"
chunks = chunk_message(text)
for chunk in chunks:
assert chunk.count("```") % 2 == 0
def test_chunk_unicode_content():
text = "ๆฅๆฌ่ชใในใ ๐" * 500
chunks = chunk_message(text)
assert all(len(c.encode("utf-8")) <= 8000 for c in chunks)
Bug Fix
def test_session_id_with_uppercase_rejected():
"""Bug #123: Session IDs with uppercase hex were accepted."""
runner = ClaudeRunner()
with pytest.raises(ValueError):
runner._build_args("hello", session_id="ABC-DEF")
Database Operation
async def test_save_overwrites_existing(repo):
await repo.save(thread_id=123, session_id="old-id")
await repo.save(thread_id=123, session_id="new-id")
result = await repo.get_session_id(thread_id=123)
assert result == "new-id"
async def test_concurrent_saves(repo):
import asyncio
tasks = [
repo.save(thread_id=i, session_id=f"session-{i}")
for i in range(100)
]
await asyncio.gather(*tasks)
Coverage Requirements
| Module | Target | Rationale |
|---|
| parser.py | 90%+ | Pure logic, no dependencies, easy to test |
| chunker.py | 90%+ | Pure logic, edge cases are critical |
| types.py | 90%+ | Data structures and enums |
| repository.py | 90%+ | Real SQLite, no mocking needed |
| runner.py | 60%+ | Test args/env building; subprocess is harder |
| Cogs | 30%+ | Discord mocking is heavy; test validation logic |
| status.py | 30%+ | Async + Discord API mocking |
| Overall | 50%+ | And always increasing |
Anti-Patterns (Don't Do This)
Writing implementation first, tests later
โ "I'll write the code first and add tests after"
โ
"I'll write the test first, watch it fail, then implement"
Testing implementation details
assert runner._process is not None
args = runner._build_args("hello", None)
assert args[-1] == "hello"
Skipping the RED step
โ Writing a test that passes immediately
โ
Writing a test, running it, confirming it FAILS, then implementing
Over-mocking
mock_everything = MagicMock()
result = function_under_test(mock_everything)
real_runner = ClaudeRunner(command="claude")
args = real_runner._build_args("test prompt", None)
assert "--" in args
Quick Reference
uv run pytest tests/test_parser.py -v
uv run pytest tests/test_parser.py::test_parse_system_message -v
uv run pytest tests/ -v --cov=claude_discord --cov-report=term-missing
uv run pytest tests/ -x
uv run pytest tests/ -v --tb=short -l