diff --git a/src/local_coding_slm/safety.py b/src/local_coding_slm/safety.py index 638ee8f..192119b 100644 --- a/src/local_coding_slm/safety.py +++ b/src/local_coding_slm/safety.py @@ -347,7 +347,8 @@ def git_is_ignored(root: Path, relpath: str) -> bool | None: return None -def git_tracked_text_files(root: Path) -> list[tuple[str, str]]: +def git_tracked_text_files(root: Path) -> list[tuple[str, str]] | None: + """Return tracked text, or None when git cannot list the tree.""" try: proc = subprocess.run( ["git", "ls-files", "-z"], @@ -356,9 +357,9 @@ def git_tracked_text_files(root: Path) -> list[tuple[str, str]]: capture_output=True, ) except OSError: - return [] + return None if proc.returncode != 0: - return [] + return None out: list[tuple[str, str]] = [] for rel in proc.stdout.split(b"\0"): if not rel: @@ -391,6 +392,18 @@ def git_tracked_text_files(root: Path) -> list[tuple[str, str]]: return out +def _committed_ip_result( + repo: Path, + tracked: Iterable[tuple[str, str]] | None, +) -> CheckResult: + if tracked is not None: + return classify_tracked_ipv4(list(tracked)) + files = git_tracked_text_files(repo) + if files is None: + return CheckResult("committed_ips", "fail", "could not list tracked files") + return classify_tracked_ipv4(files) + + def _listen_check( url: str, *, @@ -439,8 +452,7 @@ def run_checks( ignored = env_ignored if env_ignored is not None else git_is_ignored(repo, ".env") results.append(classify_env_ignored(ignored)) - files = list(tracked) if tracked is not None else git_tracked_text_files(repo) - results.append(classify_tracked_ipv4(files)) + results.append(_committed_ip_result(repo, tracked)) return results diff --git a/tests/test_safety.py b/tests/test_safety.py index 589f71a..e9eab30 100644 --- a/tests/test_safety.py +++ b/tests/test_safety.py @@ -214,6 +214,33 @@ def test_safe_deployment_no_fail(self) -> None: ) self.assertEqual(worst_status(results), "pass") + def test_git_ls_files_failure_fails_closed(self) -> None: + """A failed git listing is not an empty tree with no addresses.""" + import subprocess + from pathlib import Path + + safe = { + "environ": { + "OLLAMA_BASE_URL": "http://127.0.0.1:11434", + "OLLAMA_FAST_MODEL": "qwen3.5:9b", + "OLLAMA_STRONG_MODEL": "devstral-small-2", + }, + "listen_rows": [("127.0.0.1", 11434)], + "env_ignored": True, + "env_example_text": "OLLAMA_BASE_URL=http://127.0.0.1:11434\n", + "root": Path("/tmp/not-a-repo"), + } + failed = subprocess.CompletedProcess(["git", "ls-files", "-z"], 1, b"", b"fatal") + with patch("local_coding_slm.safety.subprocess.run", return_value=failed): + results = run_checks(**safe) + ips = next(item for item in results if item.name == "committed_ips") + self.assertEqual(ips.status, "fail") + self.assertIn("could not list", ips.message) + with patch("local_coding_slm.safety.subprocess.run", side_effect=OSError("git missing")): + raised = run_checks(**safe) + missing = next(item for item in raised if item.name == "committed_ips") + self.assertEqual(missing.status, "fail") + def test_unofficial_library_tag_fails_run_checks(self) -> None: results = run_checks( environ={