Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions code_puppy/tools/file_operations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# file_operations.py

import math
import os
import shutil
import subprocess
Expand Down Expand Up @@ -193,10 +194,12 @@ def _list_files(
break

if not rg_path and recursive:
# Only need ripgrep for recursive listings
error_msg = "Error: ripgrep (rg) not found. Please install ripgrep to use this tool."
return ListFileOutput(content=error_msg, error=error_msg)

# Fall back to non-recursive listing when ripgrep is not available
output_lines.append(
"Warning: ripgrep (rg) not found. Falling back to non-recursive listing. "
"Install ripgrep for full recursive support."
)
recursive = False
# Only use ripgrep for recursive listings
if recursive:
# Build command for ripgrep --files
Expand Down Expand Up @@ -512,8 +515,8 @@ def _read_file(
for char in content
)

# Simple approximation: ~4 characters per token
num_tokens = len(content) // 4
# Token estimation consistent with BaseAgent (~2.5 characters per token)
num_tokens = max(1, math.floor(len(content) / 2.5))
if num_tokens > 10000:
return ReadFileOutput(
content=None,
Expand Down
5 changes: 3 additions & 2 deletions tests/tools/test_file_operations_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,9 @@ def test_list_files_ripgrep_not_found_recursive(self, tmp_path):
):
result = _list_files(None, str(tmp_path), recursive=True)

assert result.error is not None
assert "ripgrep" in result.error.lower() or "rg" in result.error.lower()
# Fallback behavior: warning in content, no hard error, files still listed
assert result.content is not None
assert result.error is None or "falling back" in (result.content or "").lower()

def test_list_files_non_recursive_without_ripgrep(self, tmp_path):
"""Test non-recursive listing works without ripgrep."""
Expand Down
6 changes: 3 additions & 3 deletions tests/tools/test_file_operations_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def test_read_file_line_range_out_of_bounds(self, tmp_path):

assert result.error is None
assert result.content == "" # Should return empty string
assert result.num_tokens == 0
assert result.num_tokens == 1

def test_read_file_line_range_negative_start(self, tmp_path):
"""Test reading with negative start line is rejected."""
Expand Down Expand Up @@ -124,7 +124,7 @@ def test_read_file_empty_file(self, tmp_path):

assert result.error is None
assert result.content == ""
assert result.num_tokens == 0
assert result.num_tokens == 1

# ==================== LIST FILES TESTS ====================

Expand Down Expand Up @@ -430,7 +430,7 @@ def test_read_large_file_with_token_limit(self, tmp_path):
"""Test that large files are handled and tokens are counted."""
test_file = tmp_path / "large.txt"
# Create file with 500 lines
lines = [f"Line {i}: " + ("x" * 50) for i in range(500)]
lines = [f"Line {i}: " + ("x" * 30) for i in range(400)]
test_file.write_text("\n".join(lines))

result = _read_file(None, str(test_file))
Expand Down
51 changes: 51 additions & 0 deletions tests/tools/test_list_files_ripgrep_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Regression test for ripgrep fallback in _list_files.

When ripgrep is not installed, _list_files should fall back to
non-recursive os.listdir instead of returning an error.
"""

import os
import tempfile
from unittest.mock import patch

from code_puppy.tools.file_operations import _list_files


class TestListFilesRipgrepFallback:
"""_list_files should gracefully handle missing ripgrep."""

def test_falls_back_when_ripgrep_not_found(self):
"""
When ripgrep is not installed, _list_files should return
a non-recursive listing instead of an error.
"""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = os.path.join(tmpdir, "test.py")
with open(test_file, "w") as f:
f.write("print('hello')")

with patch("shutil.which", return_value=None):
result = _list_files(None, tmpdir, recursive=True)

# Should not return a hard error
assert result.content is not None
assert (
"not found" not in (result.content or "").lower()
or "falling back" in (result.content or "").lower()
)
# Should still return file listing
assert "test.py" in result.content

def test_returns_files_without_ripgrep(self):
"""
Files in the directory should be listed even without ripgrep.
"""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = os.path.join(tmpdir, "myfile.py")
with open(test_file, "w") as f:
f.write("x = 1")

with patch("shutil.which", return_value=None):
result = _list_files(None, tmpdir, recursive=True)

assert "myfile.py" in result.content
Loading