|
4 | 4 | import sys |
5 | 5 | import pytest |
6 | 6 | from pathlib import Path |
| 7 | +from unittest.mock import patch |
7 | 8 |
|
8 | 9 | from jcodemunch_mcp.security import ( |
9 | 10 | validate_path, |
|
16 | 17 | should_exclude_file, |
17 | 18 | SECRET_PATTERNS, |
18 | 19 | BINARY_EXTENSIONS, |
| 20 | + DEFAULT_MAX_INDEX_FILES, |
| 21 | + MAX_INDEX_FILES_ENV_VAR, |
| 22 | + get_max_index_files, |
19 | 23 | ) |
20 | 24 |
|
21 | 25 |
|
@@ -221,6 +225,24 @@ def test_checks_can_be_disabled(self, tmp_path): |
221 | 225 | assert should_exclude_file(f, tmp_path, check_secrets=False) is None |
222 | 226 |
|
223 | 227 |
|
| 228 | +class TestMaxIndexFilesConfig: |
| 229 | + def test_defaults_when_env_is_unset(self): |
| 230 | + with patch.dict(os.environ, {}, clear=True): |
| 231 | + assert get_max_index_files() == DEFAULT_MAX_INDEX_FILES |
| 232 | + |
| 233 | + def test_reads_env_override(self): |
| 234 | + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "1234"}, clear=True): |
| 235 | + assert get_max_index_files() == 1234 |
| 236 | + |
| 237 | + def test_invalid_env_falls_back_to_default(self): |
| 238 | + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "invalid"}, clear=True): |
| 239 | + assert get_max_index_files() == DEFAULT_MAX_INDEX_FILES |
| 240 | + |
| 241 | + def test_non_positive_explicit_value_is_rejected(self): |
| 242 | + with pytest.raises(ValueError, match="positive integer"): |
| 243 | + get_max_index_files(0) |
| 244 | + |
| 245 | + |
224 | 246 | # --- Integration: discover_local_files with security --- |
225 | 247 |
|
226 | 248 | class TestDiscoverLocalFilesSecure: |
@@ -277,6 +299,31 @@ def test_extra_ignore_patterns(self, tmp_path): |
277 | 299 | assert "main.py" in names |
278 | 300 | assert "temp.py" not in names |
279 | 301 |
|
| 302 | + def test_respects_env_file_limit(self, tmp_path): |
| 303 | + """Environment override controls local folder file discovery limit.""" |
| 304 | + from jcodemunch_mcp.tools.index_folder import discover_local_files |
| 305 | + |
| 306 | + for i in range(10): |
| 307 | + (tmp_path / f"file{i}.py").write_text(f"x = {i}\n") |
| 308 | + |
| 309 | + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "3"}, clear=False): |
| 310 | + files, *_ = discover_local_files(tmp_path) |
| 311 | + |
| 312 | + assert len(files) == 3 |
| 313 | + |
| 314 | + def test_exact_env_file_limit_does_not_report_truncation(self, tmp_path): |
| 315 | + """Exact file-count matches should not be treated as truncation.""" |
| 316 | + from jcodemunch_mcp.tools.index_folder import discover_local_files |
| 317 | + |
| 318 | + for i in range(3): |
| 319 | + (tmp_path / f"file{i}.py").write_text(f"x = {i}\n") |
| 320 | + |
| 321 | + with patch.dict(os.environ, {MAX_INDEX_FILES_ENV_VAR: "3"}, clear=False): |
| 322 | + files, _, skip_counts = discover_local_files(tmp_path) |
| 323 | + |
| 324 | + assert len(files) == 3 |
| 325 | + assert skip_counts["file_limit"] == 0 |
| 326 | + |
280 | 327 | @pytest.mark.skipif(sys.platform == "win32", reason="Symlinks unreliable on Windows") |
281 | 328 | def test_symlinks_skipped_by_default(self, tmp_path): |
282 | 329 | """Symlinks are skipped when follow_symlinks=False.""" |
@@ -308,11 +355,12 @@ def test_secret_files_filtered_in_discovery(self): |
308 | 355 | {"path": "src/utils.py", "type": "blob", "size": 500}, |
309 | 356 | ] |
310 | 357 |
|
311 | | - files = discover_source_files(tree_entries) |
| 358 | + files, truncated = discover_source_files(tree_entries) |
312 | 359 | assert "src/main.py" in files |
313 | 360 | assert "src/utils.py" in files |
314 | 361 | assert ".env" not in files |
315 | 362 | assert "certs/server.pem" not in files |
| 363 | + assert truncated is False |
316 | 364 |
|
317 | 365 |
|
318 | 366 | # --- Encoding safety in index_store --- |
|
0 commit comments