|
7 | 7 |
|
8 | 8 | from langchain.agents.middleware.file_search import ( |
9 | 9 | FilesystemFileSearchMiddleware, |
| 10 | + _expand_include_patterns, |
| 11 | + _is_valid_include_pattern, |
| 12 | + _match_include_pattern, |
10 | 13 | ) |
11 | 14 |
|
12 | 15 |
|
@@ -259,3 +262,105 @@ def test_grep_path_traversal_protection(self, tmp_path: Path) -> None: |
259 | 262 |
|
260 | 263 | assert result == "No matches found" |
261 | 264 | assert "secret" not in result |
| 265 | + |
| 266 | + |
| 267 | +class TestExpandIncludePatterns: |
| 268 | + """Tests for _expand_include_patterns helper function.""" |
| 269 | + |
| 270 | + def test_expand_patterns_basic_brace_expansion(self) -> None: |
| 271 | + """Test basic brace expansion with multiple options.""" |
| 272 | + result = _expand_include_patterns("*.{py,txt}") |
| 273 | + assert result == ["*.py", "*.txt"] |
| 274 | + |
| 275 | + def test_expand_patterns_nested_braces(self) -> None: |
| 276 | + """Test nested brace expansion.""" |
| 277 | + result = _expand_include_patterns("test.{a,b}.{c,d}") |
| 278 | + assert result is not None |
| 279 | + assert len(result) == 4 |
| 280 | + assert "test.a.c" in result |
| 281 | + assert "test.b.d" in result |
| 282 | + |
| 283 | + @pytest.mark.parametrize( |
| 284 | + "pattern", |
| 285 | + [ |
| 286 | + "*.py}", # closing brace without opening |
| 287 | + "*.{}", # empty braces |
| 288 | + "*.{py", # unclosed brace |
| 289 | + ], |
| 290 | + ) |
| 291 | + def test_expand_patterns_invalid_braces(self, pattern: str) -> None: |
| 292 | + """Test patterns with invalid brace syntax return None.""" |
| 293 | + result = _expand_include_patterns(pattern) |
| 294 | + assert result is None |
| 295 | + |
| 296 | + |
| 297 | +class TestValidateIncludePattern: |
| 298 | + """Tests for _is_valid_include_pattern helper function.""" |
| 299 | + |
| 300 | + @pytest.mark.parametrize( |
| 301 | + "pattern", |
| 302 | + [ |
| 303 | + "", # empty pattern |
| 304 | + "*.py\x00", # null byte |
| 305 | + "*.py\n", # newline |
| 306 | + ], |
| 307 | + ) |
| 308 | + def test_validate_invalid_patterns(self, pattern: str) -> None: |
| 309 | + """Test that invalid patterns are rejected.""" |
| 310 | + assert not _is_valid_include_pattern(pattern) |
| 311 | + |
| 312 | + |
| 313 | +class TestMatchIncludePattern: |
| 314 | + """Tests for _match_include_pattern helper function.""" |
| 315 | + |
| 316 | + def test_match_pattern_with_braces(self) -> None: |
| 317 | + """Test matching with brace expansion.""" |
| 318 | + assert _match_include_pattern("test.py", "*.{py,txt}") |
| 319 | + assert _match_include_pattern("test.txt", "*.{py,txt}") |
| 320 | + assert not _match_include_pattern("test.md", "*.{py,txt}") |
| 321 | + |
| 322 | + def test_match_pattern_invalid_expansion(self) -> None: |
| 323 | + """Test matching with pattern that cannot be expanded returns False.""" |
| 324 | + assert not _match_include_pattern("test.py", "*.{}") |
| 325 | + |
| 326 | + |
| 327 | +class TestGrepEdgeCases: |
| 328 | + """Tests for edge cases in grep search.""" |
| 329 | + |
| 330 | + def test_grep_with_special_chars_in_pattern(self, tmp_path: Path) -> None: |
| 331 | + """Test grep with special characters in pattern.""" |
| 332 | + (tmp_path / "test.py").write_text("def test():\n pass\n", encoding="utf-8") |
| 333 | + |
| 334 | + middleware = FilesystemFileSearchMiddleware(root_path=str(tmp_path), use_ripgrep=False) |
| 335 | + |
| 336 | + result = middleware.grep_search.func(pattern="def.*:") |
| 337 | + |
| 338 | + assert "/test.py" in result |
| 339 | + |
| 340 | + def test_grep_case_insensitive(self, tmp_path: Path) -> None: |
| 341 | + """Test grep with case-insensitive search.""" |
| 342 | + (tmp_path / "test.py").write_text("HELLO world\n", encoding="utf-8") |
| 343 | + |
| 344 | + middleware = FilesystemFileSearchMiddleware(root_path=str(tmp_path), use_ripgrep=False) |
| 345 | + |
| 346 | + result = middleware.grep_search.func(pattern="(?i)hello") |
| 347 | + |
| 348 | + assert "/test.py" in result |
| 349 | + |
| 350 | + def test_grep_with_large_file_skipping(self, tmp_path: Path) -> None: |
| 351 | + """Test that grep skips files larger than max_file_size_mb.""" |
| 352 | + # Create a file larger than 1MB |
| 353 | + large_content = "x" * (2 * 1024 * 1024) # 2MB |
| 354 | + (tmp_path / "large.txt").write_text(large_content, encoding="utf-8") |
| 355 | + (tmp_path / "small.txt").write_text("x", encoding="utf-8") |
| 356 | + |
| 357 | + middleware = FilesystemFileSearchMiddleware( |
| 358 | + root_path=str(tmp_path), |
| 359 | + use_ripgrep=False, |
| 360 | + max_file_size_mb=1, # 1MB limit |
| 361 | + ) |
| 362 | + |
| 363 | + result = middleware.grep_search.func(pattern="x") |
| 364 | + |
| 365 | + # Large file should be skipped |
| 366 | + assert "/small.txt" in result |
0 commit comments