Files
local_swarm/tests/test_tool_parsing.py
T
sleepy 6ab726b46c refactor(api): extract formatting, parsing, and handlers from routes
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
2026-02-25 12:53:27 +01:00

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)