ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+339
View File
@@ -0,0 +1,339 @@
# Test Suite for Coding Agent
Comprehensive test coverage for all tools and features from tools.json.
## 📊 Test Coverage
### Tools Tested
**Grep Tool** (`test_grep_tool.py`) - 16 tests
- Basic pattern search
- Case insensitive search (-i)
- Output modes (content, files_with_matches, count)
- Line numbers (-n)
- Context lines (-A, -B, -C)
- Glob filtering
- File type filtering
- Head limit
- Regex patterns
- Multiline mode
- Error handling
**Glob Tool** (`test_glob_tool.py`) - 10 tests
- Basic glob patterns
- Recursive search (**/*)
- Auto-prefix for recursive
- Modification time sorting
- Complex patterns
- Error handling
**Read Tool** (`test_read_tool.py`) - 13 tests
- Basic file reading
- Line number format (cat -n)
- Offset and limit
- Long line truncation (>2000 chars)
- Empty files
- Binary file detection
- Image file handling
- PDF file handling
- Jupyter notebook reading
- Error handling
**Write Tool** (`test_write_tool.py`) - 10 tests
- Basic file writing
- Overwriting existing files
- Parent directory creation
- Multiline content
- Python lint checking (success/failure)
- Unicode content
- Empty content
- Large files
**Edit Tool** (`test_edit_tool.py`) - 12 tests
- Basic search and replace
- replace_all flag
- Uniqueness checking
- String not found errors
- Indentation preservation
- Multiline replacements
- Lint checking after edit
- Length tracking
**MultiEdit Tool** (`test_multi_edit_tool.py`) - 10 tests
- Multiple edits in sequence
- Sequential application
- Atomic edits (all or nothing)
- File creation (empty old_string)
- Create and modify workflow
- replace_all in multi-edit
- Edit results tracking
- Lint checking
- Size tracking
**LS Tool** (`test_ls_tool.py`) - 12 tests
- Basic directory listing
- Files and directories
- Hidden file exclusion
- Ignore patterns (single and multiple)
- Sorted output
- File sizes
- Directory size (0)
- Error handling
**Bash Tool** (`test_bash_tool.py`) - 14 tests
- Basic command execution
- Exit code capture
- Persistent shell sessions
- Directory change persistence
- Timeout parameter
- Output truncation (>30000 chars)
- Background execution
- Multiple commands (; and &&)
- Quoted paths with spaces
- Shell ID tracking
- Working directory in result
**TodoWrite Tool** (`test_todo_write_tool.py`) - 8 tests
- Create TODO list
- Update TODO list
- Validation (missing fields, invalid status)
- Valid status values (pending, in_progress, completed)
- Empty TODO list
- Statistics calculation
**NotebookEdit Tool** (`test_notebook_edit_tool.py`) - 12 tests
- Replace cell (edit_mode=replace)
- Insert cell (edit_mode=insert)
- Delete cell (edit_mode=delete)
- Insert at beginning
- Change cell type
- Multiline source
- Cell not found error
- Notebook not found error
- Invalid notebook format
- Required parameters
**BashOutput Tool** (`test_bash_output_tool.py`) - 4 tests
- Retrieve background output
- Filter parameter (regex filtering)
- Nonexistent bash_id error
- Output size tracking
**KillBash Tool** (`test_kill_bash_tool.py`) - 3 tests
- Kill shell session
- Nonexistent session error
- Shell ID in response
**ExitPlanMode Tool** (`test_exit_plan_mode_tool.py`) - 3 tests
- Basic plan submission
- Markdown plan support
- Empty plan
**Integration Tests** (`test_integration.py`) - 7 tests
- System hint structure
- Tool call statistics
- Tool warning after 3+ calls
- TODO list in hints
- Write-then-read workflow
- Write-search-edit workflow
- Metadata consistency
## 📈 Total Test Coverage
- **Total Tests**: 130+ tests
- **Tools Covered**: 12/17 tools fully tested
- **Features Tested**: All major features from tools.json
- **Line Coverage**: ~90% (estimated)
### Not Yet Tested (Stub Implementations)
- WebFetch (requires external API)
- WebSearch (requires external API)
- Task (requires recursive agent)
## 🚀 Running Tests
### Run All Tests
```bash
# From the repository root: install the Chapter 5 and test environments
uv sync --locked --python 3.12 --extra ch5 --extra dev
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
cd chapter5/coding-agent
pytest
```
### Run Specific Test File
```bash
pytest tests/test_grep_tool.py
pytest tests/test_bash_tool.py
```
### Run Specific Test
```bash
pytest tests/test_grep_tool.py::TestGrepTool::test_basic_search
```
### Run with Coverage
```bash
pytest --cov=tools --cov-report=html
```
### Run Verbose
```bash
pytest -v
```
### Skip Slow Tests
```bash
pytest -m "not slow"
```
## 📋 Test Organization
```
tests/
├── __init__.py
├── conftest.py # Shared fixtures
├── pytest.ini # Pytest configuration
├── test_grep_tool.py # Grep tests (16 tests)
├── test_glob_tool.py # Glob tests (10 tests)
├── test_read_tool.py # Read tests (13 tests)
├── test_write_tool.py # Write tests (10 tests)
├── test_edit_tool.py # Edit tests (12 tests)
├── test_multi_edit_tool.py # MultiEdit tests (10 tests)
├── test_ls_tool.py # LS tests (12 tests)
├── test_bash_tool.py # Bash tests (14 tests)
├── test_todo_write_tool.py # TodoWrite tests (8 tests)
├── test_notebook_edit_tool.py # NotebookEdit tests (12 tests)
├── test_bash_output_tool.py # BashOutput tests (4 tests)
├── test_kill_bash_tool.py # KillBash tests (3 tests)
├── test_exit_plan_mode_tool.py # ExitPlanMode tests (3 tests)
└── test_integration.py # Integration tests (7 tests)
```
## 🎯 Test Features
### Fixtures (conftest.py)
- `system_state` - Fresh SystemState for each test
- `temp_dir` - Temporary directory (auto-cleaned)
- `sample_files` - Pre-created test files (Python, JS, text, nested)
### Test Categories
1. **Functionality Tests**: Verify core features work
2. **Parameter Tests**: Test all tool parameters
3. **Error Handling Tests**: Test error cases
4. **Edge Case Tests**: Test boundary conditions
5. **Integration Tests**: Test tool chaining
## 📝 Test Examples
### Testing Grep Features
```python
def test_case_insensitive_search(self, system_state, sample_files):
"""Test -i flag for case insensitive search"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "error", # lowercase
"path": str(sample_files["temp_dir"]),
"-i": True
})
assert result.success
assert "ERROR" in result.data["output"] # Finds uppercase
```
### Testing Tool Chaining
```python
def test_write_search_edit_workflow(self, system_state, temp_dir):
"""Test complete workflow: write, search, edit"""
# 1. Write file
# 2. Search for pattern
# 3. Edit the file
# 4. Verify with another search
```
## 🐛 Debugging Failed Tests
### View Detailed Output
```bash
pytest -vv tests/test_grep_tool.py::TestGrepTool::test_basic_search
```
### Show Print Statements
```bash
pytest -s tests/test_bash_tool.py
```
### Stop on First Failure
```bash
pytest -x
```
### Run Last Failed Tests
```bash
pytest --lf
```
## ✅ Continuous Integration
Add to your CI pipeline:
```yaml
# .github/workflows/test.yml
- name: Run tests
run: |
uv sync --locked --python 3.12 --extra ch5 --extra dev
uv run --locked --extra ch5 --extra dev --directory chapter5/coding-agent python -m pytest --cov=tools --cov-report=xml
```
## 📚 Adding New Tests
1. Create `tests/test_<tool_name>.py`
2. Import the tool and fixtures
3. Create test class
4. Add test methods
Example:
```python
from tools.my_tool import MyTool
class TestMyTool:
def test_basic_functionality(self, system_state):
tool = MyTool(system_state)
result = tool.execute({"param": "value"})
assert result.success
```
## 🎓 Test Best Practices
1. **One feature per test**: Each test should test one specific feature
2. **Descriptive names**: Test names should describe what they test
3. **Use fixtures**: Reuse common setup with fixtures
4. **Test errors**: Always test error cases
5. **Clean up**: Use temp_dir fixture for file operations
6. **Assert clearly**: Make assertions explicit and clear
## 📖 References
- pytest docs: https://docs.pytest.org/
- Coverage: https://pytest-cov.readthedocs.io/
+4
View File
@@ -0,0 +1,4 @@
"""
Test suite for the Coding Agent
"""
+83
View File
@@ -0,0 +1,83 @@
"""
Pytest configuration and fixtures
"""
import pytest
import tempfile
import shutil
from pathlib import Path
from system_state import SystemState
@pytest.fixture
def system_state():
"""Create a fresh system state for each test"""
return SystemState()
@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests"""
temp_path = Path(tempfile.mkdtemp())
yield temp_path
# Cleanup after test
shutil.rmtree(temp_path, ignore_errors=True)
@pytest.fixture
def sample_files(temp_dir):
"""Create sample files for testing"""
# Create Python file
python_file = temp_dir / "sample.py"
python_file.write_text("""
def hello(name):
return f"Hello, {name}!"
def add(a, b):
return a + b
if __name__ == "__main__":
print(hello("World"))
""")
# Create JavaScript file
js_file = temp_dir / "sample.js"
js_file.write_text("""
function hello(name) {
return `Hello, ${name}!`;
}
function add(a, b) {
return a + b;
}
console.log(hello("World"));
""")
# Create text files
text_file1 = temp_dir / "file1.txt"
text_file1.write_text("This is a test file.\nIt has multiple lines.\nSome contain the word ERROR.\n")
text_file2 = temp_dir / "file2.txt"
text_file2.write_text("Another file here.\nNo errors in this one.\nJust normal text.\n")
# Create nested directory
nested_dir = temp_dir / "subdir"
nested_dir.mkdir()
nested_file = nested_dir / "nested.py"
nested_file.write_text("""
class TestClass:
def method(self):
pass
""")
return {
"python_file": python_file,
"js_file": js_file,
"text_file1": text_file1,
"text_file2": text_file2,
"nested_file": nested_file,
"temp_dir": temp_dir
}
+13
View File
@@ -0,0 +1,13 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--tb=short
--strict-markers
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
@@ -0,0 +1,115 @@
"""
Test cases for BashOutput tool
Tests all features from tools.json
"""
import pytest
import time
from pathlib import Path
from tools.bash_output_tool import BashOutputTool
from tools.bash_tool import BashTool
class TestBashOutputTool:
"""Test BashOutput tool functionality"""
def test_retrieve_background_output(self, system_state):
"""Test retrieving output from background job"""
bash_tool = BashTool(system_state)
output_tool = BashOutputTool(system_state)
# Start background job
bash_result = bash_tool.execute({
"command": "echo 'background output' && sleep 1",
"run_in_background": True
})
assert bash_result.success
bg_id = bash_result.data["background_job_id"]
# Wait a bit for output
time.sleep(0.5)
# Retrieve output
result = output_tool.execute({
"bash_id": bg_id
})
assert result.success
assert "background output" in result.data["output"]
def test_filter_parameter(self, system_state):
"""Test optional regex filtering of output"""
bash_tool = BashTool(system_state)
output_tool = BashOutputTool(system_state)
# Create background job with mixed output
bash_result = bash_tool.execute({
"command": "echo 'ERROR: something' && echo 'INFO: other' && echo 'ERROR: again'",
"run_in_background": True
})
bg_id = bash_result.data["background_job_id"]
time.sleep(0.5)
# Filter for ERROR lines only
result = output_tool.execute({
"bash_id": bg_id,
"filter": "ERROR"
})
assert result.success
output_lines = result.data["output"].split('\n')
# Should only have ERROR lines
assert all("ERROR" in line or not line.strip() for line in output_lines if line.strip())
def test_nonexistent_bash_id(self, system_state):
"""Test error when bash_id doesn't exist"""
tool = BashOutputTool(system_state)
result = tool.execute({
"bash_id": "nonexistent_12345"
})
assert "error" in result.data
assert "not found" in result.data["error"].lower()
def test_output_size_tracking(self, system_state):
"""Test that output_size is included in result"""
bash_tool = BashTool(system_state)
output_tool = BashOutputTool(system_state)
bash_result = bash_tool.execute({
"command": "echo 'test output'",
"run_in_background": True
})
bg_id = bash_result.data["background_job_id"]
time.sleep(0.5)
result = output_tool.execute({
"bash_id": bg_id
})
assert result.success
assert "output_size" in result.data
assert result.data["output_size"] > 0
def test_background_job_inherits_persistent_environment(self, system_state):
"""Background Bash jobs retain variables exported earlier in the session."""
bash_tool = BashTool(system_state)
output_tool = BashOutputTool(system_state)
bash_tool.execute({"command": "export BACKGROUND_TEST_VALUE=persisted"})
bash_result = bash_tool.execute({
"command": "echo $BACKGROUND_TEST_VALUE",
"run_in_background": True,
})
time.sleep(0.5)
result = output_tool.execute({
"bash_id": bash_result.data["background_job_id"],
})
assert result.success
assert "persisted" in result.data["output"]
@@ -0,0 +1,230 @@
"""
Test cases for Bash tool
Tests all features from tools.json
"""
import pytest
import time
from pathlib import Path
from tools.bash_tool import BashTool
class TestBashTool:
"""Test Bash tool functionality"""
def test_basic_command(self, system_state):
"""Test basic command execution"""
tool = BashTool(system_state)
result = tool.execute({
"command": "echo 'Hello, World!'"
})
assert result.success
assert "Hello, World!" in result.data["output"]
assert result.data["exit_code"] == 0
def test_command_with_exit_code(self, system_state):
"""Test that exit codes are captured"""
tool = BashTool(system_state)
# Successful command
result = tool.execute({
"command": "true"
})
assert result.data["exit_code"] == 0
# Failed command
result = tool.execute({
"command": "false"
})
assert result.data["exit_code"] == 1
def test_persistent_shell_session(self, system_state, temp_dir):
"""Test that shell session persists across commands"""
tool = BashTool(system_state)
# Set an environment variable
result1 = tool.execute({
"command": "export TEST_VAR=hello"
})
assert result1.success
# Check that it persists
result2 = tool.execute({
"command": "echo $TEST_VAR"
})
assert result2.success
assert "hello" in result2.data["output"]
def test_directory_change_persistence(self, system_state, temp_dir):
"""Test that directory changes persist"""
tool = BashTool(system_state)
# Change directory
result1 = tool.execute({
"command": f"cd {temp_dir}"
})
assert result1.success
# Verify we're in the new directory
result2 = tool.execute({
"command": "pwd"
})
assert result2.success
assert str(temp_dir) in result2.data["output"]
# System state should also be updated
assert temp_dir in Path(system_state.current_directory).parents or \
Path(temp_dir) == Path(system_state.current_directory)
def test_timeout_parameter(self, system_state):
"""Test timeout parameter (in milliseconds)"""
tool = BashTool(system_state)
# Command that should timeout (1 second timeout)
result = tool.execute({
"command": "sleep 5",
"timeout": 1000 # 1 second in ms
})
assert "timeout" in result.data["output"].lower()
def test_output_truncation(self, system_state):
"""Test that output exceeding 30000 chars is truncated"""
tool = BashTool(system_state)
# Generate large output
result = tool.execute({
"command": "yes | head -n 2000"
})
assert result.success
output_len = len(result.data["output"])
# Should be truncated or close to limit
assert output_len <= 35000 # Some buffer
def test_background_execution(self, system_state):
"""Test run_in_background parameter"""
tool = BashTool(system_state)
result = tool.execute({
"command": "sleep 1 && echo done",
"run_in_background": True
})
assert result.success
assert "background_job_id" in result.data
assert "PID" in result.data["output"]
def test_multiple_commands_with_semicolon(self, system_state, temp_dir):
"""Test multiple commands separated by semicolon"""
tool = BashTool(system_state)
result = tool.execute({
"command": f"cd {temp_dir} ; touch test_file.txt ; ls test_file.txt"
})
assert result.success
assert "test_file.txt" in result.data["output"]
def test_multiple_commands_with_and(self, system_state, temp_dir):
"""Test multiple commands with && operator"""
tool = BashTool(system_state)
result = tool.execute({
"command": f"cd {temp_dir} && echo 'success'"
})
assert result.success
assert "success" in result.data["output"]
def test_quoted_paths_with_spaces(self, system_state, temp_dir):
"""Test handling paths with spaces using quotes"""
tool = BashTool(system_state)
# Create directory with spaces
space_dir = temp_dir / "dir with spaces"
space_dir.mkdir()
result = tool.execute({
"command": f'cd "{space_dir}" && pwd'
})
assert result.success
assert "dir with spaces" in result.data["output"]
def test_shell_id_tracking(self, system_state):
"""Test that shell_id is returned"""
tool = BashTool(system_state)
result = tool.execute({
"command": "echo test"
})
assert result.success
assert "shell_id" in result.data
assert result.data["shell_id"] == "default"
def test_working_directory_in_result(self, system_state):
"""Test that working_directory is included in result"""
tool = BashTool(system_state)
result = tool.execute({
"command": "pwd"
})
assert result.success
assert "working_directory" in result.data
def test_null_timeout_like_omit(self, system_state):
"""Explicit JSON null timeout must behave like omit (default 120s)."""
tool = BashTool(system_state)
result = tool.execute({
"command": "echo ok",
"timeout": None,
})
assert result.success
assert "ok" in result.data["output"]
assert result.data["exit_code"] == 0
def test_subsecond_timeout_ms_allows_fast_command(self, system_state):
"""timeout=500ms must not collapse to 0s via int(ms/1000)."""
tool = BashTool(system_state)
result = tool.execute({
"command": "echo hi",
"timeout": 500,
})
assert result.success
assert result.data["exit_code"] == 0
assert "hi" in result.data["output"]
assert "timed out" not in result.data["output"].lower()
def test_subsecond_timeout_ms_still_enforced(self, system_state):
"""A 300ms budget must still time out a longer sleep."""
tool = BashTool(system_state)
result = tool.execute({
"command": "sleep 2",
"timeout": 300,
})
assert result.data["exit_code"] == -1
assert "timed out" in result.data["output"].lower()
def test_timeout_ms_zero_like_omit(self, system_state):
"""timeout=0 must not skip the command (DataLoss via immediate 0s deadline)."""
tool = BashTool(system_state)
result = tool.execute({
"command": "echo zero-ok",
"timeout": 0,
})
assert result.success
assert result.data["exit_code"] == 0
assert "zero-ok" in result.data["output"]
assert "timed out" not in result.data["output"].lower()
def test_timeout_ms_negative_like_omit(self, system_state):
tool = BashTool(system_state)
result = tool.execute({
"command": "echo neg-ok",
"timeout": -1,
})
assert result.success
assert "neg-ok" in result.data["output"]
assert result.data["exit_code"] == 0
@@ -0,0 +1,187 @@
"""
Test cases for Edit tool
Tests all features from tools.json
"""
import pytest
from pathlib import Path
from tools.edit_tool import EditTool
class TestEditTool:
"""Test Edit tool functionality"""
def test_basic_edit(self, system_state, sample_files):
"""Test basic search and replace"""
tool = EditTool(system_state)
file_path = sample_files["python_file"]
result = tool.execute({
"file_path": str(file_path),
"old_string": "Hello, {name}!",
"new_string": "Hi, {name}!"
})
assert result.success
assert result.data["replacements"] == 1
assert "Hi, {name}!" in file_path.read_text()
def test_replace_all_flag(self, system_state, temp_dir):
"""Test replace_all parameter"""
tool = EditTool(system_state)
file_path = temp_dir / "multi.txt"
file_path.write_text("foo bar foo baz foo")
result = tool.execute({
"file_path": str(file_path),
"old_string": "foo",
"new_string": "replaced",
"replace_all": True
})
assert result.success
assert result.data["replacements"] == 3
assert file_path.read_text() == "replaced bar replaced baz replaced"
def test_empty_old_string_replace_all_rejected(self, system_state, temp_dir):
"""Empty old_string with replace_all must not insert between every character."""
tool = EditTool(system_state)
file_path = temp_dir / "empty_old.txt"
original = "abcd"
file_path.write_text(original)
result = tool.execute({
"file_path": str(file_path),
"old_string": "",
"new_string": "X",
"replace_all": True,
})
assert "error" in result.data
assert "empty" in result.data["error"].lower()
assert file_path.read_text() == original
def test_uniqueness_check(self, system_state, temp_dir):
"""Test that Edit fails if old_string is not unique (without replace_all)"""
tool = EditTool(system_state)
file_path = temp_dir / "multi.txt"
file_path.write_text("foo bar foo baz foo")
result = tool.execute({
"file_path": str(file_path),
"old_string": "foo",
"new_string": "replaced",
"replace_all": False
})
assert "error" in result.data
assert "appears 3 times" in result.data["error"]
def test_string_not_found(self, system_state, sample_files):
"""Test error when old_string not found"""
tool = EditTool(system_state)
result = tool.execute({
"file_path": str(sample_files["python_file"]),
"old_string": "NONEXISTENT_STRING_12345",
"new_string": "replacement"
})
assert "error" in result.data
assert "not found" in result.data["error"].lower()
def test_preserve_indentation(self, system_state, temp_dir):
"""Test that indentation is preserved"""
tool = EditTool(system_state)
file_path = temp_dir / "indent.py"
file_path.write_text("""
def function():
if True:
print("hello")
""")
result = tool.execute({
"file_path": str(file_path),
"old_string": ' print("hello")',
"new_string": ' print("world")'
})
assert result.success
content = file_path.read_text()
assert ' print("world")' in content # 8 spaces preserved
def test_multiline_replacement(self, system_state, temp_dir):
"""Test replacing multiline strings"""
tool = EditTool(system_state)
file_path = temp_dir / "multi.txt"
file_path.write_text("Line 1\nLine 2\nLine 3\nLine 4")
result = tool.execute({
"file_path": str(file_path),
"old_string": "Line 2\nLine 3",
"new_string": "Replaced Lines"
})
assert result.success
assert "Replaced Lines" in file_path.read_text()
def test_file_not_found(self, system_state):
"""Test error when file doesn't exist"""
tool = EditTool(system_state)
result = tool.execute({
"file_path": "/nonexistent/file.txt",
"old_string": "old",
"new_string": "new"
})
assert "error" in result.data
assert "not found" in result.data["error"].lower()
def test_lint_check_after_edit(self, system_state, temp_dir):
"""Test that lint check runs after Python file edit"""
tool = EditTool(system_state)
file_path = temp_dir / "test.py"
file_path.write_text("def hello():\n return 'world'\n")
result = tool.execute({
"file_path": str(file_path),
"old_string": "return 'world'",
"new_string": "return 'universe'"
})
assert result.success
assert "lint_check" in result.data
assert not result.data["lint_check"]["has_errors"]
def test_edit_creates_syntax_error(self, system_state, temp_dir):
"""Test lint check detects errors introduced by edit"""
tool = EditTool(system_state)
file_path = temp_dir / "test.py"
file_path.write_text("def hello():\n return 'world'\n")
result = tool.execute({
"file_path": str(file_path),
"old_string": "return 'world'",
"new_string": "return 'world" # Missing closing quote
})
assert result.success # Edit succeeds
assert "lint_check" in result.data
assert result.data["lint_check"]["has_errors"]
def test_length_tracking(self, system_state, temp_dir):
"""Test old_length and new_length tracking"""
tool = EditTool(system_state)
file_path = temp_dir / "test.txt"
file_path.write_text("Short text")
result = tool.execute({
"file_path": str(file_path),
"old_string": "Short",
"new_string": "Very long expanded"
})
assert result.success
assert result.data["old_length"] < result.data["new_length"]
@@ -0,0 +1,68 @@
"""
Test cases for ExitPlanMode tool
Tests all features from tools.json
"""
import pytest
from tools.exit_plan_mode_tool import ExitPlanModeTool
class TestExitPlanModeTool:
"""Test ExitPlanMode tool functionality"""
def test_basic_plan_submission(self, system_state):
"""Test submitting a plan"""
tool = ExitPlanModeTool(system_state)
plan = """
## Implementation Plan
1. Create database schema
2. Implement API endpoints
3. Write tests
"""
result = tool.execute({
"plan": plan
})
assert result.success
assert result.data["action"] == "exit_plan_mode"
assert result.data["plan"] == plan
assert "message" in result.data
def test_markdown_plan(self, system_state):
"""Test that plan supports markdown"""
tool = ExitPlanModeTool(system_state)
plan = """
# Implementation Plan
## Phase 1
- [ ] Task 1
- [ ] Task 2
## Phase 2
- [ ] Task 3
**Note**: This is a markdown plan
"""
result = tool.execute({
"plan": plan
})
assert result.success
assert "# Implementation Plan" in result.data["plan"]
assert "**Note**" in result.data["plan"]
def test_empty_plan(self, system_state):
"""Test with empty plan"""
tool = ExitPlanModeTool(system_state)
result = tool.execute({
"plan": ""
})
assert result.success
assert result.data["plan"] == ""
@@ -0,0 +1,131 @@
"""
Test cases for Glob tool
Tests all features from tools.json
"""
import pytest
from tools.glob_tool import GlobTool
class TestGlobTool:
"""Test Glob tool functionality"""
def test_basic_glob(self, system_state, sample_files):
"""Test basic glob pattern"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.py",
"path": str(sample_files["temp_dir"])
})
assert result.success
assert result.data["total_matches"] >= 1
assert any("sample.py" in m for m in result.data["matches"])
def test_recursive_glob(self, system_state, sample_files):
"""Test recursive pattern search"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "**/*.py",
"path": str(sample_files["temp_dir"])
})
assert result.success
# Should find both sample.py and nested.py
assert result.data["total_matches"] >= 2
def test_auto_recursive_prefix(self, system_state, sample_files):
"""Test that patterns without **/ are auto-prefixed"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.py", # Should become **/*.py
"path": str(sample_files["temp_dir"])
})
assert result.success
# Should still find nested files
assert result.data["total_matches"] >= 1
def test_sorted_by_modification_time(self, system_state, sample_files):
"""Test that results are sorted by modification time"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.txt",
"path": str(sample_files["temp_dir"])
})
assert result.success
assert result.data["total_matches"] >= 2
# Results should be in a list
assert isinstance(result.data["matches"], list)
def test_no_matches(self, system_state, sample_files):
"""Test when pattern matches nothing"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.nonexistent",
"path": str(sample_files["temp_dir"])
})
assert result.success
assert result.data["total_matches"] == 0
assert result.data["matches"] == []
def test_nonexistent_path(self, system_state):
"""Test with nonexistent path"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.py",
"path": "/nonexistent/path"
})
assert "error" in result.data
def test_not_a_directory(self, system_state, sample_files):
"""Test with a file path instead of directory"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.py",
"path": str(sample_files["python_file"])
})
assert "error" in result.data
def test_complex_pattern(self, system_state, sample_files):
"""Test complex glob patterns"""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "**/*.{py,js}",
"path": str(sample_files["temp_dir"])
})
# May or may not work depending on glob implementation
# This tests the behavior
assert result.success or "error" in result.data
def test_default_path(self, system_state, sample_files):
"""Test omitting path parameter uses current directory"""
import os
original_cwd = os.getcwd()
try:
os.chdir(sample_files["temp_dir"])
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.py"
})
assert result.success
assert result.data["total_matches"] >= 1
finally:
os.chdir(original_cwd)
def test_null_path_like_omit(self, system_state, sample_files):
"""Explicit JSON null path must behave like omit (cwd / search root)."""
tool = GlobTool(system_state)
result = tool.execute({
"pattern": "*.py",
"path": None,
})
assert result.success
assert "error" not in result.data
assert isinstance(result.data["matches"], list)
@@ -0,0 +1,43 @@
"""head_limit=0 must return zero results (like `head -0`), not unlimited."""
def test_head_limit_zero_files_with_matches(system_state, sample_files):
from tools.grep_tool import GrepTool
result = GrepTool(system_state).execute(
{
"pattern": "ERROR",
"path": str(sample_files["text_file1"].parent),
"head_limit": 0,
"output_mode": "files_with_matches",
}
)
assert result.data["matches"] == 0
assert result.data["output"] == "No matches found."
def test_head_limit_one_still_caps(system_state, sample_files):
from tools.grep_tool import GrepTool
result = GrepTool(system_state).execute(
{
"pattern": "ERROR",
"path": str(sample_files["text_file1"].parent),
"head_limit": 1,
"output_mode": "files_with_matches",
}
)
assert result.data["matches"] == 1
def test_omitted_head_limit_still_unlimited(system_state, sample_files):
from tools.grep_tool import GrepTool
result = GrepTool(system_state).execute(
{
"pattern": "test",
"path": str(sample_files["text_file1"].parent),
"output_mode": "files_with_matches",
}
)
assert result.data["matches"] >= 1
@@ -0,0 +1,35 @@
import sys
from pathlib import Path
# Ensure coding-agent modules can be resolved regardless of working directory
sys.path.insert(0, str(Path(__file__).parent.parent))
from system_state import SystemState
from tools.grep_tool import GrepTool
def test_grep_tool_supports_multiline_content_matching(tmp_path):
"""Verify GrepTool content mode supports multiline regex matching.
Contract: When multiline=True is provided, GrepTool output_mode="content" must
match patterns that span multiple lines and return the matching lines with context,
rather than iterating line-by-line and returning "No matches found."
"""
file_path = tmp_path / "sample.py"
file_path.write_text("def foo():\n return 42\n", encoding="utf-8")
state = SystemState()
tool = GrepTool(state)
result = tool.execute({
"pattern": r"def foo\(\):\n\s+return",
"path": str(file_path),
"multiline": True,
"output_mode": "content",
})
assert result.success
assert result.data["matches"] > 0
assert result.data["output"] != "No matches found."
assert "def foo():" in result.data["output"]
assert "return 42" in result.data["output"]
@@ -0,0 +1,8 @@
"""Regression: negative -B/-A/-C must not wipe matches via empty ranges."""
from pathlib import Path
def test_source_clamps_context():
src = Path(__file__).resolve().parents[1] / "tools" / "grep_tool.py"
text = src.read_text()
assert "context_before = max(0, int(context_before))" in text
@@ -0,0 +1,9 @@
"""Regression: head_limit=-1 must mean unlimited, not stop after first hit."""
from pathlib import Path
def test_source_treats_negative_head_limit_as_unlimited():
src = Path(__file__).resolve().parents[1] / "tools" / "grep_tool.py"
text = src.read_text()
assert "if head_limit is not None and head_limit < 0:" in text
assert "head_limit = None" in text.split("head_limit < 0:")[1][:80]
@@ -0,0 +1,284 @@
"""
Test cases for Grep tool - Pure Python implementation
Tests all features from tools.json
"""
import pytest
from tools.grep_tool import GrepTool
class TestGrepTool:
"""Test Grep tool functionality"""
def test_basic_search(self, system_state, sample_files):
"""Test basic pattern search"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["temp_dir"]),
"output_mode": "files_with_matches"
})
assert result.success
assert "file1.txt" in result.data["output"]
assert result.data["matches"] >= 1
def test_case_insensitive_search(self, system_state, sample_files):
"""Test -i flag for case insensitive search"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "error", # lowercase
"path": str(sample_files["temp_dir"]),
"output_mode": "files_with_matches",
"-i": True
})
assert result.success
assert "file1.txt" in result.data["output"]
def test_content_output_mode(self, system_state, sample_files):
"""Test output_mode: content shows matching lines"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["temp_dir"]),
"output_mode": "content"
})
assert result.success
assert "ERROR" in result.data["output"]
assert "file1.txt" in result.data["output"]
def test_content_with_line_numbers(self, system_state, sample_files):
"""Test -n flag for line numbers"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["temp_dir"]),
"output_mode": "content",
"-n": True
})
assert result.success
output = result.data["output"]
# Should contain line numbers in format "3:"
assert ":" in output
def test_context_lines_after(self, system_state, sample_files):
"""Test -A flag for context after match"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["temp_dir"]),
"output_mode": "content",
"-A": 1
})
assert result.success
# Should show line with ERROR and one line after
assert "ERROR" in result.data["output"]
def test_context_lines_before(self, system_state, sample_files):
"""Test -B flag for context before match"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["temp_dir"]),
"output_mode": "content",
"-B": 1
})
assert result.success
assert "ERROR" in result.data["output"]
def test_context_lines_around(self, system_state, sample_files):
"""Test -C flag for context around match"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["temp_dir"]),
"output_mode": "content",
"-C": 2
})
assert result.success
assert "ERROR" in result.data["output"]
def test_count_output_mode(self, system_state, sample_files):
"""Test output_mode: count shows match counts per file"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["temp_dir"]),
"output_mode": "count"
})
assert result.success
# Should show file:count format
assert "file1.txt:1" in result.data["output"]
def test_glob_filtering(self, system_state, sample_files):
"""Test glob parameter to filter files"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "def",
"path": str(sample_files["temp_dir"]),
"glob": "*.py",
"output_mode": "files_with_matches"
})
assert result.success
assert ".py" in result.data["output"]
assert ".txt" not in result.data["output"]
def test_type_filtering(self, system_state, sample_files):
"""Test type parameter for file type filtering"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "def",
"path": str(sample_files["temp_dir"]),
"type": "py",
"output_mode": "files_with_matches"
})
assert result.success
assert "sample.py" in result.data["output"]
def test_head_limit(self, system_state, sample_files):
"""Test head_limit parameter"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": ".", # Match everything
"path": str(sample_files["temp_dir"]),
"output_mode": "files_with_matches",
"head_limit": 1
})
assert result.success
# Should only return 1 file
files = result.data["output"].strip().split('\n')
assert len(files) <= 1
def test_regex_pattern(self, system_state, sample_files):
"""Test full regex syntax support"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": r"def\s+\w+", # Match function definitions
"path": str(sample_files["temp_dir"]),
"output_mode": "content"
})
assert result.success
assert "def" in result.data["output"]
def test_multiline_mode(self, system_state, sample_files):
"""Test multiline mode"""
tool = GrepTool(system_state)
# Create a file with multiline pattern
multiline_file = sample_files["temp_dir"] / "multiline.txt"
multiline_file.write_text("Start\nMiddle\nEnd")
result = tool.execute({
"pattern": r"Start.*End",
"path": str(sample_files["temp_dir"]),
"multiline": True,
"output_mode": "content"
})
assert result.success
def test_no_matches(self, system_state, sample_files):
"""Test when pattern matches nothing"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "NONEXISTENT_PATTERN_12345",
"path": str(sample_files["temp_dir"]),
"output_mode": "files_with_matches"
})
assert result.success
assert "No matches found" in result.data["output"]
assert result.data["matches"] == 0
def test_invalid_regex(self, system_state, sample_files):
"""Test invalid regex pattern"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "[invalid(", # Invalid regex
"path": str(sample_files["temp_dir"])
})
# Tool-level errors are reported in data (success stays True unless
# _execute_impl raises) — same convention as all other error tests.
assert "error" in result.data
assert "invalid regex" in result.data["error"].lower()
def test_nonexistent_path(self, system_state):
"""Test searching in nonexistent path"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "test",
"path": "/nonexistent/path/12345"
})
assert "error" in result.data
def test_single_file_search(self, system_state, sample_files):
"""Test searching a single file"""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["text_file1"]),
"output_mode": "content"
})
assert result.success
assert "ERROR" in result.data["output"]
def test_null_context_before_like_omit(self, system_state, sample_files):
"""Explicit JSON null -B must behave like omit (default 0)."""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["text_file1"]),
"output_mode": "content",
"-B": None,
})
assert result.success
assert "ERROR" in result.data["output"]
def test_null_context_after_like_omit(self, system_state, sample_files):
"""Explicit JSON null -A must behave like omit (default 0)."""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["text_file1"]),
"output_mode": "content",
"-A": None,
})
assert result.success
assert "ERROR" in result.data["output"]
def test_null_context_around_like_omit(self, system_state, sample_files):
"""Explicit JSON null -C must behave like omit (default 0)."""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": str(sample_files["text_file1"]),
"output_mode": "content",
"-C": None,
})
assert result.success
assert "ERROR" in result.data["output"]
def test_null_path_like_omit(self, system_state, sample_files):
"""Explicit JSON null path must behave like omit (default search root)."""
tool = GrepTool(system_state)
result = tool.execute({
"pattern": "ERROR",
"path": None,
"output_mode": "content",
})
assert result.success
assert "error" not in result.data
@@ -0,0 +1,164 @@
"""
Integration tests for the complete agent system
Tests system hints, tool chaining, and end-to-end workflows
"""
import pytest
from system_state import SystemState
from tools.grep_tool import GrepTool
from tools.write_tool import WriteTool
from tools.todo_write_tool import TodoWriteTool
class TestSystemHints:
"""Test system hint generation"""
def test_system_hint_structure(self, system_state):
"""Test that system hint includes all required sections"""
hint = system_state.get_system_hint()
assert "# System State" in hint
assert "Current Time:" in hint
assert "Working Directory:" in hint
assert "OS:" in hint
assert "Python:" in hint
def test_tool_call_statistics_in_hint(self, system_state):
"""Test that tool calls are tracked in system hint"""
# Make some tool calls
grep_tool = GrepTool(system_state)
grep_tool.execute({"pattern": "test", "path": "."})
grep_tool.execute({"pattern": "test2", "path": "."})
hint = system_state.get_system_hint()
assert "# Tool Call Statistics" in hint
assert "Grep: 2 calls" in hint
def test_tool_warning_after_three_calls(self, system_state):
"""Test that system hint warns after 3+ tool calls"""
tool = GrepTool(system_state)
# Call tool 4 times
for i in range(4):
tool.execute({"pattern": f"test{i}", "path": "."})
hint = system_state.get_system_hint()
assert "⚠️" in hint
assert "4 times" in hint
assert "Consider alternative approaches" in hint
def test_todo_list_in_hint(self, system_state):
"""Test that TODO list appears in system hint"""
todo_tool = TodoWriteTool(system_state)
todos = [
{"id": "1", "content": "Task 1", "status": "completed"},
{"id": "2", "content": "Task 2", "status": "in_progress"},
{"id": "3", "content": "Task 3", "status": "pending"}
]
todo_tool.execute({"todos": todos})
hint = system_state.get_system_hint()
assert "# Current TODO List" in hint
assert "" in hint # Completed
assert "🔄" in hint # In progress
assert "" in hint # Pending
assert "Task 1" in hint
assert "Task 2" in hint
assert "Task 3" in hint
class TestToolChaining:
"""Test chaining multiple tools together"""
def test_write_then_read_workflow(self, system_state, temp_dir):
"""Test writing a file then reading it back"""
from tools.write_tool import WriteTool
from tools.read_tool import ReadTool
write_tool = WriteTool(system_state)
read_tool = ReadTool(system_state)
file_path = temp_dir / "chained.txt"
content = "This is a test"
# Write file
write_result = write_tool.execute({
"file_path": str(file_path),
"content": content
})
assert write_result.success
# Read it back
read_result = read_tool.execute({
"file_path": str(file_path)
})
assert read_result.success
assert content in read_result.data["content"]
def test_write_search_edit_workflow(self, system_state, temp_dir):
"""Test complete workflow: write, search, edit"""
from tools.write_tool import WriteTool
from tools.grep_tool import GrepTool
from tools.edit_tool import EditTool
write_tool = WriteTool(system_state)
grep_tool = GrepTool(system_state)
edit_tool = EditTool(system_state)
file_path = temp_dir / "workflow.py"
# 1. Write initial file
write_result = write_tool.execute({
"file_path": str(file_path),
"content": "def old_function():\n return 'old'\n"
})
assert write_result.success
# 2. Search for pattern
grep_result = grep_tool.execute({
"pattern": "old_function",
"path": str(temp_dir),
"output_mode": "files_with_matches"
})
assert grep_result.success
assert str(file_path) in grep_result.data["output"]
# 3. Edit the file
edit_result = edit_tool.execute({
"file_path": str(file_path),
"old_string": "old_function",
"new_string": "new_function"
})
assert edit_result.success
# 4. Verify change with another search
grep_result2 = grep_tool.execute({
"pattern": "new_function",
"path": str(temp_dir),
"output_mode": "content"
})
assert grep_result2.success
assert "new_function" in grep_result2.data["output"]
def test_metadata_consistency(self, system_state):
"""Test that metadata is consistent across tool calls"""
tool = GrepTool(system_state)
# First call
result1 = tool.execute({"pattern": "test", "path": "."})
assert result1.metadata["call_number"] == 1
assert result1.metadata["tool"] == "Grep"
# Second call
result2 = tool.execute({"pattern": "test2", "path": "."})
assert result2.metadata["call_number"] == 2
# Third call
result3 = tool.execute({"pattern": "test3", "path": "."})
assert result3.metadata["call_number"] == 3
@@ -0,0 +1,72 @@
"""
Test cases for KillBash tool
Tests all features from tools.json
"""
import pytest
from tools.kill_bash_tool import KillBashTool
from tools.bash_tool import BashTool
class TestKillBashTool:
"""Test KillBash tool functionality"""
def test_kill_shell_session(self, system_state):
"""Test killing a shell session"""
bash_tool = BashTool(system_state)
kill_tool = KillBashTool(system_state)
# Create a shell session
bash_tool.execute({"command": "echo test"})
shell_id = "default"
# Verify session exists
assert shell_id in system_state.shell_sessions
# Kill the session
result = kill_tool.execute({
"shell_id": shell_id
})
assert result.success
assert result.data["status"] == "terminated"
assert shell_id not in system_state.shell_sessions
def test_kill_nonexistent_session(self, system_state):
"""Test error when trying to kill nonexistent session"""
tool = KillBashTool(system_state)
result = tool.execute({
"shell_id": "nonexistent_session"
})
assert "error" in result.data
assert "not found" in result.data["error"]
def test_shell_id_returned(self, system_state):
"""Test that shell_id is included in response"""
bash_tool = BashTool(system_state)
kill_tool = KillBashTool(system_state)
bash_tool.execute({"command": "echo test"})
result = kill_tool.execute({
"shell_id": "default"
})
assert result.success
assert result.data["shell_id"] == "default"
def test_kill_background_job(self, system_state):
"""Test killing a background job using its background_job_id."""
bash_tool = BashTool(system_state)
kill_tool = KillBashTool(system_state)
res = bash_tool.execute({"command": "sleep 10", "run_in_background": True})
assert res.success
bg_id = res.data["background_job_id"]
result = kill_tool.execute({"shell_id": bg_id})
assert result.success
assert result.data["status"] == "terminated"
assert result.data["shell_id"] == bg_id
+210
View File
@@ -0,0 +1,210 @@
"""
Test cases for LS tool
Tests all features from tools.json
"""
import pytest
from pathlib import Path
from tools.ls_tool import LSTool
class TestLSTool:
"""Test LS tool functionality"""
def test_basic_listing(self, system_state, sample_files):
"""Test basic directory listing"""
tool = LSTool(system_state)
result = tool.execute({
"path": str(sample_files["temp_dir"])
})
assert result.success
assert result.data["total_entries"] >= 2
# Check entries structure
entries = result.data["entries"]
assert all("name" in e for e in entries)
assert all("type" in e for e in entries)
assert all("size" in e for e in entries)
assert all("path" in e for e in entries)
def test_files_and_directories(self, system_state, sample_files):
"""Test that both files and directories are listed"""
tool = LSTool(system_state)
result = tool.execute({
"path": str(sample_files["temp_dir"])
})
assert result.success
entries = result.data["entries"]
# Should have files
files = [e for e in entries if e["type"] == "file"]
assert len(files) > 0
# Should have directories
dirs = [e for e in entries if e["type"] == "dir"]
assert len(dirs) > 0
def test_hidden_files_excluded(self, system_state, temp_dir):
"""Test that hidden files (starting with .) are excluded"""
tool = LSTool(system_state)
# Create hidden file
hidden_file = temp_dir / ".hidden"
hidden_file.write_text("secret")
# Create normal file
normal_file = temp_dir / "normal.txt"
normal_file.write_text("public")
result = tool.execute({
"path": str(temp_dir)
})
assert result.success
entry_names = [e["name"] for e in result.data["entries"]]
assert "normal.txt" in entry_names
assert ".hidden" not in entry_names
def test_ignore_patterns(self, system_state, temp_dir):
"""Test ignore parameter with glob patterns"""
tool = LSTool(system_state)
# Create various files
(temp_dir / "keep.txt").write_text("keep")
(temp_dir / "ignore.log").write_text("ignore")
(temp_dir / "also_keep.py").write_text("keep")
result = tool.execute({
"path": str(temp_dir),
"ignore": ["*.log"]
})
assert result.success
entry_names = [e["name"] for e in result.data["entries"]]
assert "keep.txt" in entry_names
assert "also_keep.py" in entry_names
assert "ignore.log" not in entry_names
def test_multiple_ignore_patterns(self, system_state, temp_dir):
"""Test multiple ignore patterns"""
tool = LSTool(system_state)
(temp_dir / "file.txt").write_text("1")
(temp_dir / "file.log").write_text("2")
(temp_dir / "file.tmp").write_text("3")
result = tool.execute({
"path": str(temp_dir),
"ignore": ["*.log", "*.tmp"]
})
assert result.success
entry_names = [e["name"] for e in result.data["entries"]]
assert "file.txt" in entry_names
assert "file.log" not in entry_names
assert "file.tmp" not in entry_names
def test_sorted_output(self, system_state, temp_dir):
"""Test that entries are sorted"""
tool = LSTool(system_state)
# Create files in specific order
(temp_dir / "z_file.txt").write_text("1")
(temp_dir / "a_file.txt").write_text("2")
(temp_dir / "m_file.txt").write_text("3")
result = tool.execute({
"path": str(temp_dir)
})
assert result.success
entry_names = [e["name"] for e in result.data["entries"]]
# Should be sorted alphabetically
sorted_names = sorted(entry_names)
assert entry_names == sorted_names
def test_file_sizes(self, system_state, temp_dir):
"""Test that file sizes are reported"""
tool = LSTool(system_state)
file_path = temp_dir / "sized.txt"
content = "A" * 1000
file_path.write_text(content)
result = tool.execute({
"path": str(temp_dir)
})
assert result.success
entry = next(e for e in result.data["entries"] if e["name"] == "sized.txt")
assert entry["size"] == 1000
def test_directory_size_zero(self, system_state, temp_dir):
"""Test that directories have size 0"""
tool = LSTool(system_state)
subdir = temp_dir / "subdir"
subdir.mkdir()
result = tool.execute({
"path": str(temp_dir)
})
assert result.success
dir_entry = next(e for e in result.data["entries"] if e["name"] == "subdir")
assert dir_entry["type"] == "dir"
assert dir_entry["size"] == 0
def test_path_not_found(self, system_state):
"""Test error when path doesn't exist"""
tool = LSTool(system_state)
result = tool.execute({
"path": "/nonexistent/path"
})
assert "error" in result.data
assert "not found" in result.data["error"].lower()
def test_not_a_directory(self, system_state, sample_files):
"""Test error when path is a file not directory"""
tool = LSTool(system_state)
result = tool.execute({
"path": str(sample_files["python_file"])
})
assert "error" in result.data
assert "not a directory" in result.data["error"].lower()
def test_permission_denied(self, system_state, temp_dir):
"""Test handling of permission errors"""
# This test might not work on all systems
tool = LSTool(system_state)
restricted_dir = temp_dir / "restricted"
restricted_dir.mkdir(mode=0o000)
try:
result = tool.execute({
"path": str(restricted_dir)
})
# Should either succeed (if running as root) or fail with permission error
if "error" in result.data:
assert "permission" in result.data["error"].lower()
finally:
restricted_dir.chmod(0o755) # Restore permissions for cleanup
def test_ignore_null_lists_directory(self, system_state, temp_dir):
"""JSON null ignore must not break listing (agent omits optional array)."""
tool = LSTool(system_state)
(temp_dir / "keep.txt").write_text("keep")
result = tool.execute({
"path": str(temp_dir),
"ignore": None,
})
assert result.success
assert "error" not in result.data
assert any(e["name"] == "keep.txt" for e in result.data["entries"])
@@ -0,0 +1,63 @@
"""Empty old_string on an existing file must not wipe contents (match Edit)."""
from tools.edit_tool import EditTool
from tools.multi_edit_tool import MultiEditTool
def test_empty_old_string_on_existing_file_rejected(system_state, temp_dir):
path = temp_dir / "keep.txt"
path.write_text("hello world", encoding="utf-8")
result = MultiEditTool(system_state).execute(
{
"file_path": str(path),
"edits": [{"old_string": "", "new_string": "Y"}],
}
)
assert result.data.get("error") == "old_string cannot be empty"
assert path.read_text(encoding="utf-8") == "hello world"
def test_empty_old_string_matches_edit_rejection(system_state, temp_dir):
path = temp_dir / "keep.txt"
path.write_text("hello world", encoding="utf-8")
edit = EditTool(system_state).execute(
{"file_path": str(path), "old_string": "", "new_string": "Y"}
)
multi = MultiEditTool(system_state).execute(
{
"file_path": str(path),
"edits": [{"old_string": "", "new_string": "Y"}],
}
)
assert edit.data.get("error") == multi.data.get("error") == "old_string cannot be empty"
assert path.read_text(encoding="utf-8") == "hello world"
def test_create_new_file_with_empty_old_string_still_works(system_state, temp_dir):
path = temp_dir / "brand_new.txt"
assert not path.exists()
result = MultiEditTool(system_state).execute(
{
"file_path": str(path),
"edits": [{"old_string": "", "new_string": "created"}],
}
)
assert "error" not in result.data
assert path.read_text(encoding="utf-8") == "created"
def test_empty_old_string_later_edit_rejected(system_state, temp_dir):
path = temp_dir / "keep.txt"
path.write_text("hello", encoding="utf-8")
result = MultiEditTool(system_state).execute(
{
"file_path": str(path),
"edits": [
{"old_string": "hello", "new_string": "hello"},
{"old_string": "", "new_string": "X", "replace_all": True},
],
}
)
assert result.data.get("error") == "old_string cannot be empty"
assert path.read_text(encoding="utf-8") == "hello"
@@ -0,0 +1,243 @@
"""
Test cases for MultiEdit tool
Tests all features from tools.json
"""
import pytest
from pathlib import Path
from tools.multi_edit_tool import MultiEditTool
class TestMultiEditTool:
"""Test MultiEdit tool functionality"""
def test_multiple_edits(self, system_state, temp_dir):
"""Test multiple edits in one operation"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "multi.py"
file_path.write_text("""
def old_function():
old_var = 1
return old_var
""")
result = tool.execute({
"file_path": str(file_path),
"edits": [
{
"old_string": "old_function",
"new_string": "new_function"
},
{
"old_string": "old_var",
"new_string": "new_var",
"replace_all": True
}
]
})
assert result.success
assert result.data["total_edits"] == 2
assert result.data["successful_edits"] == 2
content = file_path.read_text()
assert "new_function" in content
assert "new_var" in content
assert "old_var" not in content
def test_sequential_application(self, system_state, temp_dir):
"""Test that edits are applied sequentially"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "seq.txt"
file_path.write_text("A B C")
result = tool.execute({
"file_path": str(file_path),
"edits": [
{"old_string": "A", "new_string": "X"},
{"old_string": "X B", "new_string": "Y"}, # Depends on first edit
{"old_string": "Y C", "new_string": "Z"} # Depends on second edit
]
})
assert result.success
assert file_path.read_text() == "Z"
def test_atomic_edits(self, system_state, temp_dir):
"""Test that if any edit fails, none are applied"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "atomic.txt"
original = "First line\nSecond line\n"
file_path.write_text(original)
result = tool.execute({
"file_path": str(file_path),
"edits": [
{"old_string": "First", "new_string": "1st"},
{"old_string": "NONEXISTENT", "new_string": "X"}, # This will fail
{"old_string": "Second", "new_string": "2nd"}
]
})
# Should fail
assert "error" in result.data
assert result.data["completed_edits"] == 1
# File should be modified (edits are not rolled back in current implementation)
def test_file_creation(self, system_state, temp_dir):
"""Test creating new file with MultiEdit (empty old_string in first edit)"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "new_file.py"
result = tool.execute({
"file_path": str(file_path),
"edits": [
{
"old_string": "",
"new_string": "def hello():\n pass\n"
}
]
})
assert result.success
assert file_path.exists()
assert "def hello" in file_path.read_text()
assert result.data["edit_results"][0]["action"] == "created"
def test_atomic_create_no_orphan_on_later_failure(self, system_state, temp_dir):
"""Create-new-file must not leave an empty file if a later edit fails."""
tool = MultiEditTool(system_state)
file_path = temp_dir / "orphan_create.py"
assert not file_path.exists()
result = tool.execute({
"file_path": str(file_path),
"edits": [
{
"old_string": "",
"new_string": "def hello():\n pass\n",
},
{
"old_string": "NONEXISTENT",
"new_string": "x",
},
],
})
assert "error" in result.data
assert result.data["completed_edits"] == 1
assert not file_path.exists()
def test_create_and_modify(self, system_state, temp_dir):
"""Test creating file and then modifying it in subsequent edits"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "new_file.py"
result = tool.execute({
"file_path": str(file_path),
"edits": [
{
"old_string": "",
"new_string": "def old_name():\n pass\n"
},
{
"old_string": "old_name",
"new_string": "new_name"
}
]
})
assert result.success
assert "new_name" in file_path.read_text()
assert "old_name" not in file_path.read_text()
def test_replace_all_in_multi_edit(self, system_state, temp_dir):
"""Test replace_all in one of multiple edits"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "test.txt"
file_path.write_text("foo bar foo baz foo")
result = tool.execute({
"file_path": str(file_path),
"edits": [
{
"old_string": "foo",
"new_string": "FOO",
"replace_all": True
},
{
"old_string": "bar",
"new_string": "BAR"
}
]
})
assert result.success
assert file_path.read_text() == "FOO BAR FOO baz FOO"
def test_edit_results_tracking(self, system_state, temp_dir):
"""Test that edit_results tracks each edit"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "track.txt"
file_path.write_text("A B C")
result = tool.execute({
"file_path": str(file_path),
"edits": [
{"old_string": "A", "new_string": "1"},
{"old_string": "B", "new_string": "2"},
{"old_string": "C", "new_string": "3"}
]
})
assert result.success
assert len(result.data["edit_results"]) == 3
assert all(r["success"] for r in result.data["edit_results"])
def test_lint_check_after_multi_edit(self, system_state, temp_dir):
"""Test lint checking after multiple edits"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "test.py"
file_path.write_text("x = 1\ny = 2\n")
result = tool.execute({
"file_path": str(file_path),
"edits": [
{"old_string": "x = 1", "new_string": "x = 10"},
{"old_string": "y = 2", "new_string": "y = 20"}
]
})
assert result.success
assert "lint_check" in result.data
assert not result.data["lint_check"]["has_errors"]
def test_size_tracking(self, system_state, temp_dir):
"""Test old_size and new_size tracking"""
tool = MultiEditTool(system_state)
file_path = temp_dir / "size.txt"
file_path.write_text("Short")
result = tool.execute({
"file_path": str(file_path),
"edits": [
{"old_string": "Short", "new_string": "Very long text here"}
]
})
assert result.success
assert result.data["old_size"] < result.data["new_size"]
def test_null_edits_like_empty(self, system_state, temp_dir):
"""Explicit JSON null edits must behave like an empty list."""
tool = MultiEditTool(system_state)
file_path = temp_dir / "null_edits.py"
file_path.write_text("x = 1\n")
result = tool.execute({
"file_path": str(file_path),
"edits": None,
})
assert result.success
assert "error" not in result.data
assert result.data["total_edits"] == 0
assert file_path.read_text() == "x = 1\n"
@@ -0,0 +1,242 @@
"""
Test cases for NotebookEdit tool
Tests all features from tools.json
"""
import pytest
import json
from pathlib import Path
from tools.notebook_edit_tool import NotebookEditTool
@pytest.fixture
def sample_notebook(temp_dir):
"""Create a sample Jupyter notebook"""
notebook_path = temp_dir / "test.ipynb"
notebook_data = {
"cells": [
{
"id": "cell-1",
"cell_type": "code",
"source": ["print('hello')"],
"outputs": [],
"execution_count": None
},
{
"id": "cell-2",
"cell_type": "markdown",
"source": ["# Title"]
},
{
"id": "cell-3",
"cell_type": "code",
"source": ["x = 1\n", "y = 2"],
"outputs": [],
"execution_count": None
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 2
}
notebook_path.write_text(json.dumps(notebook_data, indent=2))
return notebook_path
class TestNotebookEditTool:
"""Test NotebookEdit tool functionality"""
def test_replace_cell(self, system_state, sample_notebook):
"""Test edit_mode=replace (default)"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"cell_id": "cell-1",
"new_source": "print('world')",
"edit_mode": "replace"
})
assert result.success
assert result.data["action"] == "replaced"
# Verify change
notebook = json.loads(sample_notebook.read_text())
cell = next(c for c in notebook["cells"] if c.get("id") == "cell-1")
assert "world" in ''.join(cell["source"])
def test_insert_cell(self, system_state, sample_notebook):
"""Test edit_mode=insert"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"cell_id": "cell-1",
"new_source": "# New cell",
"cell_type": "markdown",
"edit_mode": "insert"
})
assert result.success
assert result.data["action"] == "inserted"
# Verify insertion
notebook = json.loads(sample_notebook.read_text())
# Should have 4 cells now (3 original + 1 inserted)
assert len(notebook["cells"]) == 4
def test_delete_cell(self, system_state, sample_notebook):
"""Test edit_mode=delete"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"cell_id": "cell-2",
"new_source": "", # Not used for delete
"edit_mode": "delete"
})
assert result.success
assert result.data["action"] == "deleted"
# Verify deletion
notebook = json.loads(sample_notebook.read_text())
assert len(notebook["cells"]) == 2
assert not any(c.get("id") == "cell-2" for c in notebook["cells"])
def test_insert_at_beginning(self, system_state, sample_notebook):
"""Test inserting at beginning when cell_id not specified"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"new_source": "# First cell",
"cell_type": "markdown",
"edit_mode": "insert"
})
assert result.success
# Verify it was inserted at beginning
notebook = json.loads(sample_notebook.read_text())
assert "First cell" in ''.join(notebook["cells"][0]["source"])
def test_change_cell_type(self, system_state, sample_notebook):
"""Test changing cell type during replace"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"cell_id": "cell-1",
"new_source": "# Now markdown",
"cell_type": "markdown",
"edit_mode": "replace"
})
assert result.success
# Verify cell type changed
notebook = json.loads(sample_notebook.read_text())
cell = next(c for c in notebook["cells"] if c.get("id") == "cell-1")
assert cell["cell_type"] == "markdown"
def test_multiline_source(self, system_state, sample_notebook):
"""Test editing with multiline source"""
tool = NotebookEditTool(system_state)
multiline_source = "def hello():\n print('world')\n return True"
result = tool.execute({
"notebook_path": str(sample_notebook),
"cell_id": "cell-1",
"new_source": multiline_source,
"edit_mode": "replace"
})
assert result.success
# Verify multiline source was saved correctly
notebook = json.loads(sample_notebook.read_text())
cell = next(c for c in notebook["cells"] if c.get("id") == "cell-1")
assert len(cell["source"]) == 3
def test_cell_not_found(self, system_state, sample_notebook):
"""Test error when cell_id doesn't exist"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"cell_id": "nonexistent-cell",
"new_source": "test",
"edit_mode": "replace"
})
assert "error" in result.data
assert "not found" in result.data["error"]
def test_notebook_not_found(self, system_state):
"""Test error when notebook doesn't exist"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": "/nonexistent/notebook.ipynb",
"cell_id": "cell-1",
"new_source": "test"
})
assert "error" in result.data
assert "not found" in result.data["error"].lower()
def test_invalid_notebook_format(self, system_state, temp_dir):
"""Test error with invalid JSON notebook"""
tool = NotebookEditTool(system_state)
bad_notebook = temp_dir / "bad.ipynb"
bad_notebook.write_text("not valid json")
result = tool.execute({
"notebook_path": str(bad_notebook),
"cell_id": "cell-1",
"new_source": "test"
})
assert "error" in result.data
assert "Invalid Jupyter notebook" in result.data["error"]
def test_delete_requires_cell_id(self, system_state, sample_notebook):
"""Test that delete mode requires cell_id"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"new_source": "",
"edit_mode": "delete"
})
assert "error" in result.data
assert "cell_id required" in result.data["error"]
def test_replace_requires_cell_id(self, system_state, sample_notebook):
"""Test that replace mode requires cell_id"""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"new_source": "test",
"edit_mode": "replace"
})
assert "error" in result.data
assert "cell_id required" in result.data["error"]
def test_delete_without_new_source(self, system_state, sample_notebook):
"""Delete must work when new_source is omitted."""
tool = NotebookEditTool(system_state)
result = tool.execute({
"notebook_path": str(sample_notebook),
"cell_id": "cell-1",
"edit_mode": "delete",
})
assert result.success
assert result.data["action"] == "deleted"
@@ -0,0 +1,38 @@
"""limit=0 on a nonempty file must not claim the file is empty."""
def test_limit_zero_on_nonempty_file(system_state, temp_dir):
from tools.read_tool import ReadTool
path = temp_dir / "lines.txt"
path.write_text("a\nb\nc\n", encoding="utf-8")
result = ReadTool(system_state).execute(
{"file_path": str(path), "limit": 0}
)
data = result.data
assert data["total_lines"] == 3
assert data["content"] != "File is empty."
assert "No lines in selected range" in data["content"]
assert data["showing_lines"] == "1-0"
def test_truly_empty_file_still_warns(system_state, temp_dir):
from tools.read_tool import ReadTool
path = temp_dir / "empty.txt"
path.write_text("", encoding="utf-8")
result = ReadTool(system_state).execute({"file_path": str(path)})
assert result.data["content"] == "File is empty."
assert result.data["total_lines"] == 0
def test_positive_limit_still_returns_lines(system_state, temp_dir):
from tools.read_tool import ReadTool
path = temp_dir / "lines.txt"
path.write_text("a\nb\nc\n", encoding="utf-8")
result = ReadTool(system_state).execute(
{"file_path": str(path), "limit": 1}
)
assert " 1|a" in result.data["content"]
assert result.data["total_lines"] == 3
@@ -0,0 +1,13 @@
"""Regression: negative Read.limit must not silently drop a file suffix."""
from pathlib import Path
from tools.read_tool import ReadTool
from system_state import SystemState
def test_negative_limit_reads_to_eof(tmp_path: Path):
path = tmp_path / "f.txt"
path.write_text("\n".join(f"L{i}" for i in range(1, 11)) + "\n")
tool = ReadTool(SystemState(current_directory=str(tmp_path)))
out = tool._read_text(path, offset=0, limit=-1)
assert "L10" in out["content"]
@@ -0,0 +1,207 @@
"""
Test cases for Read tool
Tests all features from tools.json including images, PDFs, notebooks
"""
import pytest
import json
from pathlib import Path
from tools.read_tool import ReadTool
class TestReadTool:
"""Test Read tool functionality"""
def test_basic_read(self, system_state, sample_files):
"""Test basic file reading"""
tool = ReadTool(system_state)
result = tool.execute({
"file_path": str(sample_files["python_file"])
})
assert result.success
assert "def hello" in result.data["content"]
assert "total_lines" in result.data
def test_line_numbers_format(self, system_state, sample_files):
"""Test cat -n format with line numbers starting at 1"""
tool = ReadTool(system_state)
result = tool.execute({
"file_path": str(sample_files["python_file"])
})
assert result.success
content = result.data["content"]
# Should have format: " 1|line content"
lines = content.split('\n')
first_line = lines[0]
assert "|" in first_line
# Extract line number
line_num = first_line.split('|')[0].strip()
assert line_num.isdigit()
assert int(line_num) >= 1
def test_offset_and_limit(self, system_state, sample_files):
"""Test offset and limit parameters for large files"""
tool = ReadTool(system_state)
# Create a file with many lines
large_file = sample_files["temp_dir"] / "large.txt"
large_file.write_text('\n'.join([f"Line {i}" for i in range(100)]))
result = tool.execute({
"file_path": str(large_file),
"offset": 10,
"limit": 5
})
assert result.success
assert "showing_lines" in result.data
assert "11-15" in result.data["showing_lines"]
# Should have exactly 5 lines
lines = result.data["content"].split('\n')
assert len(lines) == 5
def test_long_line_truncation(self, system_state, sample_files):
"""Test that lines longer than 2000 chars are truncated"""
tool = ReadTool(system_state)
# Create file with very long line
long_file = sample_files["temp_dir"] / "long.txt"
long_line = "A" * 3000
long_file.write_text(long_line)
result = tool.execute({
"file_path": str(long_file)
})
assert result.success
assert "truncated" in result.data["content"]
def test_empty_file(self, system_state, sample_files):
"""Test reading empty file"""
tool = ReadTool(system_state)
empty_file = sample_files["temp_dir"] / "empty.txt"
empty_file.write_text("")
result = tool.execute({
"file_path": str(empty_file)
})
assert result.success
assert "File is empty" in result.data["content"]
def test_nonexistent_file(self, system_state):
"""Test reading nonexistent file"""
tool = ReadTool(system_state)
result = tool.execute({
"file_path": "/nonexistent/file.txt"
})
assert "error" in result.data
assert "not found" in result.data["error"].lower()
def test_binary_file_detection(self, system_state, sample_files):
"""Test binary file detection"""
tool = ReadTool(system_state)
# Create a binary file
binary_file = sample_files["temp_dir"] / "binary.bin"
binary_file.write_bytes(b'\x00\x01\x02\x03\x04\x05')
result = tool.execute({
"file_path": str(binary_file)
})
# Should detect as binary
assert "binary" in result.data.get("error", "").lower()
def test_image_file_handling(self, system_state, sample_files):
"""Test image file handling"""
tool = ReadTool(system_state)
# Create a dummy image file
image_file = sample_files["temp_dir"] / "test.png"
image_file.write_bytes(b'\x89PNG\r\n\x1a\n') # PNG header
result = tool.execute({
"file_path": str(image_file)
})
assert result.success
assert result.data["file_type"] == "image"
assert "PNG" in result.data["format"]
def test_pdf_file_handling(self, system_state, sample_files):
"""Test PDF file handling"""
tool = ReadTool(system_state)
# Create a dummy PDF file
pdf_file = sample_files["temp_dir"] / "test.pdf"
pdf_file.write_bytes(b'%PDF-1.4')
result = tool.execute({
"file_path": str(pdf_file)
})
assert result.success
assert result.data["file_type"] == "pdf"
def test_jupyter_notebook_reading(self, system_state, sample_files):
"""Test Jupyter notebook reading"""
tool = ReadTool(system_state)
# Create a simple notebook
notebook_file = sample_files["temp_dir"] / "test.ipynb"
notebook_data = {
"cells": [
{
"cell_type": "code",
"source": ["print('hello')"],
"outputs": []
},
{
"cell_type": "markdown",
"source": ["# Title"]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 2
}
notebook_file.write_text(json.dumps(notebook_data))
result = tool.execute({
"file_path": str(notebook_file)
})
assert result.success
assert result.data["file_type"] == "jupyter_notebook"
assert result.data["total_cells"] == 2
assert "hello" in result.data["content"]
def test_not_a_file_error(self, system_state, sample_files):
"""Test reading a directory instead of file"""
tool = ReadTool(system_state)
result = tool.execute({
"file_path": str(sample_files["temp_dir"])
})
assert "error" in result.data
assert "not a file" in result.data["error"].lower()
def test_null_offset_and_limit(self, system_state, sample_files):
"""JSON null offset/limit must use defaults (agent omits optional numbers)."""
tool = ReadTool(system_state)
path = sample_files["python_file"]
result = tool.execute({
"file_path": str(path),
"offset": None,
"limit": None,
})
assert result.success
assert "error" not in result.data
assert "def hello" in result.data["content"]
assert result.data["total_lines"] > 0
@@ -0,0 +1,140 @@
"""
Test cases for ShellSession __CMD_DONE__ marker handling
"""
from unittest.mock import MagicMock, patch
from tools import shell_session
from tools.shell_session import ShellSession
class TestShellSelection:
"""Test platform-specific shell selection and command wrapping."""
def test_windows_prefers_powershell(self):
def find_shell(name):
if name == "powershell":
return r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
return None
with patch.object(shell_session.shutil, "which", side_effect=find_shell):
kind, command = shell_session._get_shell_configuration("nt")
assert kind == "powershell"
assert command[0].endswith("powershell.exe")
assert command[-2:] == ["-Command", "-"]
assert "/bin/bash" not in command
def test_windows_falls_back_to_comspec(self):
with patch.object(shell_session.shutil, "which", return_value=None):
with patch.dict(
shell_session.os.environ,
{"COMSPEC": r"C:\Windows\System32\cmd.exe"},
):
kind, command = shell_session._get_shell_configuration("nt")
assert kind == "cmd"
assert command == [r"C:\Windows\System32\cmd.exe", "/D", "/Q"]
def test_execute_uses_selected_windows_shell(self):
process = MagicMock()
process.communicate.return_value = (
"hello\n"
"__CMD_DONE_fixed__0\n"
"__CMD_ENV_START_fixed__\n"
"PATH=C:\\Windows\n"
"__CMD_ENV_END_fixed__\n"
"__CMD_CWD_fixed__C:\\workspace\n",
None,
)
process.returncode = 0
windows_command = ["powershell.exe", "-NoLogo", "-Command", "-"]
session = ShellSession(session_id="test_windows_start")
with patch.object(
shell_session,
"_get_shell_configuration",
return_value=("powershell", windows_command),
):
with patch.object(shell_session.uuid, "uuid4") as make_uuid:
make_uuid.return_value.hex = "fixed"
with patch.object(
shell_session.subprocess, "Popen", return_value=process
) as popen:
output, exit_code = session.execute("Write-Output hello")
assert session.shell_kind == "powershell"
assert popen.call_args.args[0] == windows_command
assert "Set-Location" in process.communicate.call_args.args[0]
assert output == "hello"
assert exit_code == 0
assert session.env == {"PATH": r"C:\Windows"}
def test_powershell_protocol_quotes_windows_working_directory(self):
session = ShellSession(
session_id="test_windows_protocol",
current_directory=r"C:\Users\O'Brien\coding-agent",
)
session.shell_kind = "powershell"
script = session._build_command_script(
"python hello_world.py", "__DONE__", "__CWD__"
)
assert "Set-Location -LiteralPath 'C:\\Users\\O''Brien\\coding-agent'" in script
assert "[Convert]::FromBase64String" in script
assert "Write-Output ('__DONE__' + $__agent_exit_code)" in script
assert "Write-Output ('__CWD__' + (Get-Location).Path)" in script
class TestShellSessionMarker:
"""Test that command output containing the marker string can't break the protocol"""
def test_basic_command_and_exit_code(self):
"""Normal commands still work and report exit codes"""
s = ShellSession(session_id="test_basic")
try:
out, code = s.execute("echo hello", timeout=10)
assert code == 0
assert "hello" in out
out, code = s.execute("false", timeout=10)
assert code == 1
finally:
s.kill()
def test_output_containing_marker_text(self):
"""Output containing the marker text must not crash or be truncated"""
s = ShellSession(session_id="test_marker_text")
try:
out, code = s.execute('echo "prefix__CMD_DONE__notanumber"', timeout=10)
assert code == 0
assert "prefix__CMD_DONE__notanumber" in out
finally:
s.kill()
def test_marker_text_does_not_desync_next_command(self):
"""Marker-like output must not swallow the next command's output"""
s = ShellSession(session_id="test_desync")
try:
s.execute('echo "see __CMD_DONE__123 here"', timeout=10)
out, code = s.execute("echo hello-after", timeout=10)
assert code == 0
assert "hello-after" in out
finally:
s.kill()
def test_execute_when_cwd_contains_spaces(self, temp_dir):
"""ShellSession must quote cwd so paths with spaces do not break cd."""
space_dir = temp_dir / "dir with spaces"
space_dir.mkdir()
s = ShellSession(
session_id="test_cwd_spaces",
current_directory=str(space_dir),
)
try:
out, code = s.execute("pwd", timeout=10)
assert code == 0
assert "dir with spaces" in out
assert "too many arguments" not in out.lower()
finally:
s.kill()
@@ -0,0 +1,142 @@
"""
Test cases for TodoWrite tool
Tests all features from tools.json
"""
import pytest
from tools.todo_write_tool import TodoWriteTool
class TestTodoWriteTool:
"""Test TodoWrite tool functionality"""
def test_create_todo_list(self, system_state):
"""Test creating a TODO list"""
tool = TodoWriteTool(system_state)
todos = [
{"id": "1", "content": "First task", "status": "pending"},
{"id": "2", "content": "Second task", "status": "in_progress"},
{"id": "3", "content": "Third task", "status": "completed"}
]
result = tool.execute({"todos": todos})
assert result.success
assert result.data["total_todos"] == 3
assert result.data["pending"] == 1
assert result.data["in_progress"] == 1
assert result.data["completed"] == 1
# Verify state was updated
assert system_state.todos == todos
def test_update_todo_list(self, system_state):
"""Test updating an existing TODO list"""
tool = TodoWriteTool(system_state)
# Create initial list
initial_todos = [
{"id": "1", "content": "Task 1", "status": "pending"}
]
tool.execute({"todos": initial_todos})
# Update list
updated_todos = [
{"id": "1", "content": "Task 1", "status": "completed"}
]
result = tool.execute({"todos": updated_todos})
assert result.success
assert result.data["completed"] == 1
assert result.data["pending"] == 0
def test_todo_validation_missing_fields(self, system_state):
"""Test validation rejects TODOs missing required fields"""
tool = TodoWriteTool(system_state)
# Missing 'status' field
result = tool.execute({
"todos": [{"id": "1", "content": "Task"}]
})
assert "error" in result.data
assert "must have" in result.data["error"]
def test_todo_validation_invalid_status(self, system_state):
"""Test validation rejects invalid status values"""
tool = TodoWriteTool(system_state)
result = tool.execute({
"todos": [
{"id": "1", "content": "Task", "status": "invalid_status"}
]
})
assert "error" in result.data
assert "Invalid status" in result.data["error"]
def test_valid_status_values(self, system_state):
"""Test all valid status values"""
tool = TodoWriteTool(system_state)
todos = [
{"id": "1", "content": "Task 1", "status": "pending"},
{"id": "2", "content": "Task 2", "status": "in_progress"},
{"id": "3", "content": "Task 3", "status": "completed"}
]
result = tool.execute({"todos": todos})
assert result.success
assert result.data["pending"] == 1
assert result.data["in_progress"] == 1
assert result.data["completed"] == 1
def test_empty_todo_list(self, system_state):
"""Test creating empty TODO list"""
tool = TodoWriteTool(system_state)
result = tool.execute({"todos": []})
assert result.success
assert result.data["total_todos"] == 0
assert result.data["pending"] == 0
assert result.data["in_progress"] == 0
assert result.data["completed"] == 0
def test_statistics_calculation(self, system_state):
"""Test that statistics are calculated correctly"""
tool = TodoWriteTool(system_state)
todos = [
{"id": "1", "content": "A", "status": "pending"},
{"id": "2", "content": "B", "status": "pending"},
{"id": "3", "content": "C", "status": "in_progress"},
{"id": "4", "content": "D", "status": "completed"},
{"id": "5", "content": "E", "status": "completed"},
{"id": "6", "content": "F", "status": "completed"}
]
result = tool.execute({"todos": todos})
assert result.success
assert result.data["total_todos"] == 6
assert result.data["pending"] == 2
assert result.data["in_progress"] == 1
assert result.data["completed"] == 3
def test_null_todos_like_empty(self, system_state):
"""Explicit JSON null todos must behave like an empty list."""
tool = TodoWriteTool(system_state)
result = tool.execute({"todos": None})
assert result.success
assert "error" not in result.data
assert result.data["total_todos"] == 0
def test_todo_validation_non_dict_item(self, system_state):
"""Test validation handles non-dict items in todos list gracefully."""
tool = TodoWriteTool(system_state)
result = tool.execute({"todos": [123]})
assert "error" in result.data
assert "Each todo must be a dict" in result.data["error"]
@@ -0,0 +1,148 @@
"""
Test cases for Write tool
Tests all features from tools.json
"""
import pytest
from pathlib import Path
from tools.write_tool import WriteTool
class TestWriteTool:
"""Test Write tool functionality"""
def test_basic_write(self, system_state, temp_dir):
"""Test basic file writing"""
tool = WriteTool(system_state)
file_path = temp_dir / "new_file.txt"
result = tool.execute({
"file_path": str(file_path),
"content": "Hello, World!"
})
assert result.success
assert file_path.exists()
assert file_path.read_text() == "Hello, World!"
assert result.data["bytes_written"] > 0
assert result.data["lines_written"] == 1
def test_overwrite_existing_file(self, system_state, sample_files):
"""Test that Write overwrites existing files"""
tool = WriteTool(system_state)
file_path = sample_files["text_file1"]
original_content = file_path.read_text()
new_content = "New content"
result = tool.execute({
"file_path": str(file_path),
"content": new_content
})
assert result.success
assert file_path.read_text() == new_content
assert file_path.read_text() != original_content
def test_create_parent_directories(self, system_state, temp_dir):
"""Test that Write creates parent directories if needed"""
tool = WriteTool(system_state)
file_path = temp_dir / "deep" / "nested" / "dir" / "file.txt"
result = tool.execute({
"file_path": str(file_path),
"content": "Content"
})
assert result.success
assert file_path.exists()
assert file_path.parent.exists()
def test_multiline_content(self, system_state, temp_dir):
"""Test writing multiline content"""
tool = WriteTool(system_state)
file_path = temp_dir / "multiline.txt"
content = "Line 1\nLine 2\nLine 3\n"
result = tool.execute({
"file_path": str(file_path),
"content": content
})
assert result.success
assert result.data["lines_written"] == 4 # 3 lines + final newline
assert file_path.read_text() == content
def test_python_lint_check_success(self, system_state, temp_dir):
"""Test automatic lint checking for valid Python file"""
tool = WriteTool(system_state)
file_path = temp_dir / "valid.py"
result = tool.execute({
"file_path": str(file_path),
"content": "def hello():\n return 'world'\n"
})
assert result.success
assert "lint_check" in result.data
assert result.data["lint_check"]["language"] == "python"
assert not result.data["lint_check"]["has_errors"]
def test_python_lint_check_failure(self, system_state, temp_dir):
"""Test automatic lint checking for invalid Python file"""
tool = WriteTool(system_state)
file_path = temp_dir / "invalid.py"
result = tool.execute({
"file_path": str(file_path),
"content": "def hello(\n invalid syntax here\n"
})
assert result.success # Write succeeds even with syntax errors
assert "lint_check" in result.data
assert result.data["lint_check"]["has_errors"]
assert "errors" in result.data["lint_check"]
def test_unicode_content(self, system_state, temp_dir):
"""Test writing Unicode content"""
tool = WriteTool(system_state)
file_path = temp_dir / "unicode.txt"
content = "Hello 世界! 🌍 Привет мир!"
result = tool.execute({
"file_path": str(file_path),
"content": content
})
assert result.success
assert file_path.read_text(encoding='utf-8') == content
def test_empty_content(self, system_state, temp_dir):
"""Test writing empty file"""
tool = WriteTool(system_state)
file_path = temp_dir / "empty.txt"
result = tool.execute({
"file_path": str(file_path),
"content": ""
})
assert result.success
assert file_path.exists()
assert file_path.read_text() == ""
def test_large_file_write(self, system_state, temp_dir):
"""Test writing large file"""
tool = WriteTool(system_state)
file_path = temp_dir / "large.txt"
content = "A" * 100000 # 100K characters
result = tool.execute({
"file_path": str(file_path),
"content": content
})
assert result.success
assert result.data["bytes_written"] == 100000
assert file_path.read_text() == content