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
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:
@@ -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/
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Test suite for the Coding Agent
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user