Completed HIGH-001 through HIGH-004: HIGH-001: Discord bot with channel message routing - bot.py: 244 lines with ClaudeCoordinator class - @mention trigger mode for safe operation - Session lifecycle integration with SessionManager - Typing indicators and error handling - 20/20 tests passing HIGH-002: Response formatter with intelligent chunking - response_formatter.py: expanded to 329 lines - format_response() with smart boundary detection - Code block preservation and splitting - 26/26 tests passing HIGH-003: Slash commands for bot management - commands.py: 411 lines with ClaudeCommands cog - /reset with interactive confirmation dialog - /status with Discord embed display - /model for runtime model switching - 18/18 tests passing HIGH-004: Concurrent message handling - Per-channel asyncio.Lock implementation - Same-channel serialization (prevents race conditions) - Cross-channel parallelization (maintains performance) - 7/7 concurrency tests passing Total: 134/135 tests passing (99.3%) Production-ready Discord bot MVP Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
381 lines
12 KiB
Python
381 lines
12 KiB
Python
"""Comprehensive tests for Discord response formatter."""
|
|
|
|
import pytest
|
|
from claude_coordinator.response_formatter import ResponseFormatter
|
|
|
|
|
|
class TestResponseFormatterBasics:
|
|
"""Test basic functionality of ResponseFormatter."""
|
|
|
|
def test_short_response_single_message(self):
|
|
"""Short response (<2000 chars) returns single message."""
|
|
formatter = ResponseFormatter()
|
|
text = "This is a short response."
|
|
|
|
result = formatter.format_response(text)
|
|
|
|
assert len(result) == 1
|
|
assert result[0] == text
|
|
|
|
def test_empty_input_returns_empty_list(self):
|
|
"""Empty input returns empty list."""
|
|
formatter = ResponseFormatter()
|
|
|
|
result = formatter.format_response("")
|
|
|
|
assert result == []
|
|
|
|
def test_whitespace_only_returns_empty_list(self):
|
|
"""Whitespace-only input returns empty list."""
|
|
formatter = ResponseFormatter()
|
|
|
|
result = formatter.format_response(" \n\t \n ")
|
|
|
|
assert result == []
|
|
|
|
def test_exactly_max_length_single_message(self):
|
|
"""Text exactly at max_length returns single message."""
|
|
formatter = ResponseFormatter()
|
|
text = "a" * 2000
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
assert len(result) == 1
|
|
assert result[0] == text
|
|
|
|
|
|
class TestSmartChunking:
|
|
"""Test intelligent chunking on natural boundaries."""
|
|
|
|
def test_long_response_without_code_blocks(self):
|
|
"""Long response without code blocks is intelligently chunked."""
|
|
formatter = ResponseFormatter()
|
|
# Create text longer than 2000 chars with paragraphs
|
|
paragraphs = [
|
|
"This is paragraph one with some content. " * 30,
|
|
"This is paragraph two with more content. " * 30,
|
|
"This is paragraph three with even more content. " * 30,
|
|
]
|
|
text = "\n\n".join(paragraphs)
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
# Should split into multiple messages
|
|
assert len(result) > 1
|
|
# Each chunk should be under max_length
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|
|
|
|
def test_split_on_paragraph_boundaries(self):
|
|
"""Text splits on paragraph boundaries (double newlines)."""
|
|
formatter = ResponseFormatter()
|
|
# Create text with clear paragraph breaks
|
|
paragraph1 = "A" * 1100 + "\n\n"
|
|
paragraph2 = "B" * 1100
|
|
text = paragraph1 + paragraph2
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
# Should split into 2 chunks at paragraph boundary
|
|
assert len(result) == 2
|
|
assert "A" in result[0] and "B" not in result[0]
|
|
assert "B" in result[1] and "A" not in result[1]
|
|
|
|
def test_split_on_sentence_boundaries(self):
|
|
"""Text splits on sentence boundaries when no paragraph breaks."""
|
|
formatter = ResponseFormatter()
|
|
# Create long sentences
|
|
sentence1 = "This is the first sentence. " * 40
|
|
sentence2 = "This is the second sentence. " * 40
|
|
text = sentence1 + sentence2
|
|
|
|
result = formatter.format_response(text, max_length=1500)
|
|
|
|
# Should split into multiple messages
|
|
assert len(result) >= 2
|
|
# Each chunk should be under max_length
|
|
for chunk in result:
|
|
assert len(chunk) <= 1500
|
|
|
|
def test_split_on_word_boundaries(self):
|
|
"""Text splits on word boundaries when no sentence breaks."""
|
|
formatter = ResponseFormatter()
|
|
# Create text with unique words to detect mid-word splits
|
|
text = " ".join([f"testword{i}" for i in range(400)]) # ~4000 chars
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
# Should split into chunks
|
|
assert len(result) >= 2
|
|
# All chunks should be under max length
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|
|
|
|
def test_very_long_single_line(self):
|
|
"""Single line longer than max_length is force-split."""
|
|
formatter = ResponseFormatter()
|
|
# Create one continuous line with no spaces
|
|
text = "a" * 3000
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
# Should split into 2 chunks
|
|
assert len(result) == 2
|
|
assert len(result[0]) == 2000
|
|
assert len(result[1]) == 1000
|
|
|
|
|
|
class TestCodeBlockPreservation:
|
|
"""Test code block handling and preservation."""
|
|
|
|
def test_single_code_block_preserved(self):
|
|
"""Response with single code block preserves it."""
|
|
formatter = ResponseFormatter()
|
|
text = "Here's the code:\n\n```python\ndef hello():\n return 'world'\n```\n\nThat's it!"
|
|
|
|
result = formatter.format_response(text)
|
|
|
|
# Should keep code block intact
|
|
assert len(result) == 1
|
|
assert "```python" in result[0]
|
|
assert "def hello():" in result[0]
|
|
assert "```" in result[0]
|
|
|
|
def test_multiple_code_blocks_preserved(self):
|
|
"""Response with multiple code blocks preserves all."""
|
|
formatter = ResponseFormatter()
|
|
text = """First block:
|
|
```python
|
|
def func1():
|
|
pass
|
|
```
|
|
|
|
Second block:
|
|
```javascript
|
|
function func2() {}
|
|
```
|
|
|
|
Done!"""
|
|
|
|
result = formatter.format_response(text)
|
|
|
|
# Should preserve both code blocks
|
|
full_text = " ".join(result)
|
|
assert "```python" in full_text
|
|
assert "```javascript" in full_text
|
|
assert full_text.count("```") >= 4 # 2 opening + 2 closing
|
|
|
|
def test_code_block_at_chunk_boundary(self):
|
|
"""Code block at chunk boundary is properly split and closed."""
|
|
formatter = ResponseFormatter()
|
|
# Create text with code block that causes splitting
|
|
prefix = "A" * 1000 + "\n\n"
|
|
code_block = "```python\n" + ("print('test')\n" * 100) + "```"
|
|
text = prefix + code_block
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
# Should split into multiple chunks
|
|
assert len(result) >= 2
|
|
# Each chunk should have valid markdown
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|
|
|
|
def test_large_code_block_split_correctly(self):
|
|
"""Code block larger than 2000 chars splits with markers."""
|
|
formatter = ResponseFormatter()
|
|
# Create huge code block
|
|
code_lines = "\n".join([f"line_{i} = {i}" for i in range(200)])
|
|
text = f"```python\n{code_lines}\n```"
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
# Should split into multiple chunks
|
|
assert len(result) >= 2
|
|
# Each chunk should have code block markers
|
|
for chunk in result:
|
|
assert chunk.startswith("```python")
|
|
assert chunk.endswith("```")
|
|
assert len(chunk) <= 2000
|
|
|
|
def test_code_block_without_language(self):
|
|
"""Code blocks without language identifier are handled."""
|
|
formatter = ResponseFormatter()
|
|
text = "Code:\n\n```\nsome code here\nmore code\n```\n\nDone."
|
|
|
|
result = formatter.format_response(text)
|
|
|
|
assert len(result) == 1
|
|
assert "```" in result[0]
|
|
assert "some code here" in result[0]
|
|
|
|
|
|
class TestMixedContent:
|
|
"""Test responses with mixed markdown and code."""
|
|
|
|
def test_mixed_markdown_preserved(self):
|
|
"""Response with bold, italic, lists is preserved."""
|
|
formatter = ResponseFormatter()
|
|
text = """**Bold text** and *italic text*
|
|
|
|
- List item 1
|
|
- List item 2
|
|
- List item 3
|
|
|
|
Regular text after list."""
|
|
|
|
result = formatter.format_response(text)
|
|
|
|
assert len(result) == 1
|
|
assert "**Bold text**" in result[0]
|
|
assert "*italic text*" in result[0]
|
|
assert "- List item 1" in result[0]
|
|
|
|
def test_multiple_paragraphs_chunked_correctly(self):
|
|
"""Response with multiple paragraphs splits on paragraph boundaries."""
|
|
formatter = ResponseFormatter()
|
|
# Create multiple substantial paragraphs
|
|
paragraphs = []
|
|
for i in range(5):
|
|
paragraphs.append(f"Paragraph {i+1}. " + ("Content. " * 50))
|
|
|
|
text = "\n\n".join(paragraphs)
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
# Should split into multiple messages
|
|
assert len(result) >= 2
|
|
# Verify content distribution
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|
|
|
|
|
|
class TestCodeBlockSplitting:
|
|
"""Test code block splitting behavior with split_on_code_blocks flag."""
|
|
|
|
def test_split_on_code_blocks_false(self):
|
|
"""With split_on_code_blocks=False, uses simple splitting."""
|
|
formatter = ResponseFormatter()
|
|
text = "Text before\n\n```python\ndef func():\n pass\n```\n\nText after" * 50
|
|
|
|
result = formatter.format_response(text, max_length=2000, split_on_code_blocks=False)
|
|
|
|
# Should split into chunks
|
|
assert len(result) >= 2
|
|
# May split code blocks (not preserving them)
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|
|
|
|
def test_split_on_code_blocks_true_preserves(self):
|
|
"""With split_on_code_blocks=True, preserves code block integrity."""
|
|
formatter = ResponseFormatter()
|
|
code = "```python\ndef hello():\n return 'world'\n```"
|
|
text = "Intro text\n\n" + code + "\n\nOutro text"
|
|
|
|
result = formatter.format_response(text, max_length=2000, split_on_code_blocks=True)
|
|
|
|
# Code block should be intact in one of the chunks
|
|
full_text = "".join(result)
|
|
assert "```python\ndef hello():\n return 'world'\n```" in full_text
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Test edge cases and error handling."""
|
|
|
|
def test_single_code_block_exactly_max_length(self):
|
|
"""Code block exactly at max_length is handled."""
|
|
formatter = ResponseFormatter()
|
|
# Create code block that's exactly 2000 chars including markers
|
|
code_content = "x" * (2000 - len("```python\n\n```"))
|
|
text = f"```python\n{code_content}\n```"
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
assert len(result) == 1
|
|
assert len(result[0]) <= 2000
|
|
|
|
def test_consecutive_code_blocks(self):
|
|
"""Multiple consecutive code blocks are preserved."""
|
|
formatter = ResponseFormatter()
|
|
text = """```python
|
|
code1 = 1
|
|
```
|
|
```javascript
|
|
code2 = 2
|
|
```
|
|
```bash
|
|
code3 = 3
|
|
```"""
|
|
|
|
result = formatter.format_response(text)
|
|
|
|
full_text = " ".join(result)
|
|
assert "```python" in full_text
|
|
assert "```javascript" in full_text
|
|
assert "```bash" in full_text
|
|
|
|
def test_very_long_single_word(self):
|
|
"""Single word longer than max_length is force-split."""
|
|
formatter = ResponseFormatter()
|
|
text = "a" * 2500 # Single "word" with no spaces
|
|
|
|
result = formatter.format_response(text, max_length=2000)
|
|
|
|
assert len(result) == 2
|
|
assert len(result[0]) == 2000
|
|
assert len(result[1]) == 500
|
|
|
|
def test_custom_max_length(self):
|
|
"""Custom max_length parameter is respected."""
|
|
formatter = ResponseFormatter()
|
|
text = "word " * 300 # ~1500 chars
|
|
|
|
result = formatter.format_response(text, max_length=500)
|
|
|
|
# Should split into multiple chunks
|
|
assert len(result) >= 3
|
|
for chunk in result:
|
|
assert len(chunk) <= 500
|
|
|
|
|
|
class TestHelperMethods:
|
|
"""Test existing helper methods still work."""
|
|
|
|
def test_format_code_block_with_language(self):
|
|
"""format_code_block() creates proper code block."""
|
|
formatter = ResponseFormatter()
|
|
|
|
result = formatter.format_code_block("print('test')", "python")
|
|
|
|
assert result == "```python\nprint('test')\n```"
|
|
|
|
def test_format_code_block_without_language(self):
|
|
"""format_code_block() works without language."""
|
|
formatter = ResponseFormatter()
|
|
|
|
result = formatter.format_code_block("some code")
|
|
|
|
assert result == "```\nsome code\n```"
|
|
|
|
def test_format_error(self):
|
|
"""format_error() creates proper error message."""
|
|
formatter = ResponseFormatter()
|
|
|
|
result = formatter.format_error("Something went wrong")
|
|
|
|
assert ":warning:" in result
|
|
assert "Error:" in result
|
|
assert "Something went wrong" in result
|
|
assert "```" in result
|
|
|
|
def test_chunk_response_basic(self):
|
|
"""chunk_response() splits on line boundaries."""
|
|
formatter = ResponseFormatter()
|
|
text = "line1\nline2\nline3\n" + ("x" * 2000)
|
|
|
|
result = formatter.chunk_response(text, max_length=2000)
|
|
|
|
assert len(result) >= 2
|
|
for chunk in result:
|
|
assert len(chunk) <= 2000
|