6ab726b46c
Extracted large monolithic routes.py (1183 lines) into focused modules: - api/formatting.py: Message formatting and tool instructions - api/tool_parser.py: Tool call parsing from various formats - api/chat_handlers.py: Chat completion business logic - utils/token_counter.py: Centralized token counting utilities - utils/project_discovery.py: Shared project root discovery routes.py is now 252 lines (under 300 limit). All 35 tests pass. Eliminated code duplication for _discover_project_root. Refs previous review report findings on modularity
200 lines
6.0 KiB
Python
200 lines
6.0 KiB
Python
"""Unit tests for tool parsing functionality."""
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
|
|
from api.routes import parse_tool_calls
|
|
|
|
|
|
def test_parse_simple_tool():
|
|
"""Test parsing a single tool call."""
|
|
text = 'TOOL: read\nARGUMENTS: {"filePath": "test.txt"}'
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is not None
|
|
assert len(tools) == 1
|
|
assert tools[0]["function"]["name"] == "read"
|
|
assert tools[0]["function"]["arguments"] == '{"filePath": "test.txt"}'
|
|
|
|
|
|
def test_parse_no_tool():
|
|
"""Test parsing text without tool calls."""
|
|
text = "Just a regular response"
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is None
|
|
assert content == text
|
|
|
|
|
|
def test_parse_multiple_tools():
|
|
"""Test parsing multiple tool calls."""
|
|
text = '''TOOL: read
|
|
ARGUMENTS: {"filePath": "file1.txt"}
|
|
|
|
TOOL: write
|
|
ARGUMENTS: {"filePath": "file2.txt", "content": "hello"}'''
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is not None
|
|
assert len(tools) == 2
|
|
assert tools[0]["function"]["name"] == "read"
|
|
assert tools[1]["function"]["name"] == "write"
|
|
|
|
|
|
def test_parse_tool_with_content_before():
|
|
"""Test parsing when there's content before the tool call."""
|
|
text = '''I'll read that file for you.
|
|
|
|
TOOL: read
|
|
ARGUMENTS: {"filePath": "config.yaml"}'''
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is not None
|
|
assert len(tools) == 1
|
|
assert tools[0]["function"]["name"] == "read"
|
|
assert "I'll read that file for you." in content
|
|
|
|
|
|
def test_parse_bash_tool():
|
|
"""Test parsing bash tool call."""
|
|
text = 'TOOL: bash\nARGUMENTS: {"command": "ls -la"}'
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is not None
|
|
assert len(tools) == 1
|
|
assert tools[0]["function"]["name"] == "bash"
|
|
|
|
|
|
def test_parse_case_insensitive():
|
|
"""Test that TOOL:/ARGUMENTS: is case insensitive."""
|
|
text = 'tool: read\narguments: {"filePath": "test.txt"}'
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is not None
|
|
assert len(tools) == 1
|
|
assert tools[0]["function"]["name"] == "read"
|
|
|
|
|
|
def test_parse_invalid_json():
|
|
"""Test that invalid JSON is skipped gracefully."""
|
|
text = '''TOOL: read
|
|
ARGUMENTS: {invalid json}
|
|
|
|
TOOL: write
|
|
ARGUMENTS: {"filePath": "test.txt"}'''
|
|
content, tools = parse_tool_calls(text)
|
|
# Should skip the invalid one and parse the valid one
|
|
assert tools is not None
|
|
assert len(tools) == 1
|
|
assert tools[0]["function"]["name"] == "write"
|
|
|
|
|
|
def test_parse_empty_text():
|
|
"""Test parsing empty text."""
|
|
text = ""
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is None
|
|
assert content == ""
|
|
|
|
|
|
def test_parse_whitespace_only():
|
|
"""Test parsing whitespace-only text."""
|
|
text = " \n\t "
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is None
|
|
|
|
|
|
def test_parse_markdown_code_block():
|
|
"""Test parsing markdown code blocks as fallback (e.g., ```bash command```)."""
|
|
text = '''I'll help you create a project.
|
|
|
|
```bash
|
|
mkdir myapp
|
|
cd myapp
|
|
```
|
|
|
|
Now let's create a file.'''
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is not None
|
|
assert len(tools) == 1
|
|
assert tools[0]["function"]["name"] == "bash"
|
|
assert "mkdir myapp" in tools[0]["function"]["arguments"]
|
|
assert "cd myapp" in tools[0]["function"]["arguments"]
|
|
|
|
|
|
def test_parse_markdown_inline():
|
|
"""Test parsing inline bash commands in markdown."""
|
|
text = '''Here's what to do:
|
|
|
|
```bash
|
|
ls -la
|
|
```'''
|
|
content, tools = parse_tool_calls(text)
|
|
assert tools is not None
|
|
assert len(tools) == 1
|
|
assert tools[0]["function"]["name"] == "bash"
|
|
assert "ls -la" in tools[0]["function"]["arguments"]
|
|
|
|
|
|
def test_tool_instructions_content():
|
|
"""Test that tool instructions contain required sections (REVIEW-2026-02-24 Blocker #4)."""
|
|
from api.formatting import _load_tool_instructions
|
|
|
|
# Load instructions from config file
|
|
instructions = _load_tool_instructions()
|
|
|
|
# Verify key instruction components are present (minimal instructions)
|
|
assert "use tools" in instructions.lower(), "Instructions must mention tool usage"
|
|
assert "Format" in instructions or "format" in instructions.lower(), "Instructions must mention format"
|
|
assert "no explanations" in instructions.lower(), "Instructions must forbid explanations"
|
|
assert "no markdown" in instructions.lower(), "Instructions must forbid markdown"
|
|
|
|
|
|
def test_tool_instructions_token_count():
|
|
"""Test that tool instructions are within token budget (REVIEW-2026-02-24 Blocker #1)."""
|
|
from api.formatting import _load_tool_instructions
|
|
|
|
# Load instructions from config file
|
|
instructions = _load_tool_instructions()
|
|
|
|
# Token budget: 2000 hard limit
|
|
# Rough estimate: 4 chars = 1 token
|
|
char_count = len(instructions)
|
|
estimated_tokens = char_count // 4
|
|
|
|
assert estimated_tokens <= 2000, f"Instructions estimated at {estimated_tokens} tokens, must be under 2000"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run all tests
|
|
test_functions = [
|
|
test_parse_simple_tool,
|
|
test_parse_no_tool,
|
|
test_parse_multiple_tools,
|
|
test_parse_tool_with_content_before,
|
|
test_parse_bash_tool,
|
|
test_parse_case_insensitive,
|
|
test_parse_invalid_json,
|
|
test_parse_empty_text,
|
|
test_parse_whitespace_only,
|
|
test_parse_markdown_code_block,
|
|
test_parse_markdown_inline,
|
|
test_tool_instructions_content,
|
|
test_tool_instructions_token_count,
|
|
]
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
for test_func in test_functions:
|
|
try:
|
|
test_func()
|
|
print(f"✓ {test_func.__name__}")
|
|
passed += 1
|
|
except AssertionError as e:
|
|
print(f"✗ {test_func.__name__}: {e}")
|
|
failed += 1
|
|
except Exception as e:
|
|
print(f"✗ {test_func.__name__}: Exception - {e}")
|
|
failed += 1
|
|
|
|
print(f"\n{passed} passed, {failed} failed")
|
|
|
|
if failed > 0:
|
|
sys.exit(1)
|