All checks were successful
Lint / lint (pull_request) Successful in 29s
Cherry-picks tools/path_guard.py and tests/test_path_guard.py from
burn/921-poka-yoke-hardcoded-paths (commit 5dcb905). All 21 tests pass:
- hardcoded /Users/<name>/ paths are rejected at runtime
- hardcoded /home/<name>/ paths are rejected at runtime
- ~/.hermes/... via expanduser() passes (safe, expanded at runtime)
- valid relative and /tmp/ absolute paths pass
- static scanner catches violations and respects # noqa: hardcoded-path-ok
- comments are skipped by scanner
- directory scanner skips test files and __pycache__
Refs #962
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
4.2 KiB
Python
128 lines
4.2 KiB
Python
"""Tests for tools/path_guard.py — poka-yoke hardcoded path detection."""
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tools.path_guard import (
|
|
PathGuardError,
|
|
scan_directory,
|
|
scan_file_for_violations,
|
|
validate_path,
|
|
validate_tool_paths,
|
|
)
|
|
|
|
|
|
class TestValidatePath:
|
|
"""Runtime path validation."""
|
|
|
|
def test_valid_relative_path(self):
|
|
assert validate_path("tools/file_tools.py") == "tools/file_tools.py"
|
|
|
|
def test_valid_absolute_path(self):
|
|
assert validate_path("/tmp/test.txt") == "/tmp/test.txt"
|
|
|
|
def test_valid_hermes_home(self):
|
|
assert validate_path(os.path.expanduser("~/.hermes/config.yaml")) is not None
|
|
|
|
def test_reject_users_hardcoded(self):
|
|
with pytest.raises(PathGuardError, match="/Users/"):
|
|
validate_path("/Users/someone_else/.hermes/config")
|
|
|
|
def test_reject_home_hardcoded(self):
|
|
with pytest.raises(PathGuardError, match="/home/"):
|
|
validate_path("/home/user/.hermes/config")
|
|
|
|
def test_empty_path(self):
|
|
assert validate_path("") == ""
|
|
assert validate_path(None) is None
|
|
|
|
def test_non_string(self):
|
|
assert validate_path(42) == 42
|
|
|
|
|
|
class TestValidateToolPaths:
|
|
"""Batch path validation."""
|
|
|
|
def test_all_valid(self):
|
|
paths = ["tools/file.py", "/tmp/x.txt", "relative/path.py"]
|
|
assert validate_tool_paths(paths) == paths
|
|
|
|
def test_mixed_invalid(self):
|
|
with pytest.raises(PathGuardError):
|
|
validate_tool_paths(["tools/file.py", "/Users/someone_else/secret.txt"])
|
|
|
|
def test_skips_non_strings(self):
|
|
assert validate_tool_paths([None, 42, "valid.py"]) == ["valid.py"]
|
|
|
|
|
|
class TestScanFileForViolations:
|
|
"""Static file scanning."""
|
|
|
|
def test_clean_file(self, tmp_path):
|
|
f = tmp_path / "clean.py"
|
|
f.write_text("import os\nHOME = os.environ['HOME']\n")
|
|
assert scan_file_for_violations(str(f)) == []
|
|
|
|
def test_hardcoded_users(self, tmp_path):
|
|
f = tmp_path / "bad.py"
|
|
f.write_text("CONFIG = '/Users/apayne/.hermes/config.yaml'\n")
|
|
violations = scan_file_for_violations(str(f))
|
|
assert len(violations) == 1
|
|
assert "/Users/<name>/" in violations[0][2]
|
|
|
|
def test_hardcoded_home(self, tmp_path):
|
|
f = tmp_path / "bad2.py"
|
|
f.write_text("PATH = '/home/deploy/.hermes/state.db'\n")
|
|
violations = scan_file_for_violations(str(f))
|
|
assert len(violations) == 1
|
|
assert "/home/<name>/" in violations[0][2]
|
|
|
|
def test_tilde_in_expanduser_ok(self, tmp_path):
|
|
f = tmp_path / "ok.py"
|
|
f.write_text("p = os.path.expanduser('~/.hermes/config')\n")
|
|
assert scan_file_for_violations(str(f)) == []
|
|
|
|
def test_tilde_in_display_ok(self, tmp_path):
|
|
f = tmp_path / "ok2.py"
|
|
f.write_text('print("~/config saved")\n')
|
|
assert scan_file_for_violations(str(f)) == []
|
|
|
|
def test_noqa_escape(self, tmp_path):
|
|
f = tmp_path / "noqa.py"
|
|
f.write_text("PATH = '/Users/apayne/test' # noqa: hardcoded-path-ok\n")
|
|
assert scan_file_for_violations(str(f)) == []
|
|
|
|
def test_comments_skipped(self, tmp_path):
|
|
f = tmp_path / "comment.py"
|
|
f.write_text("# PATH = '/Users/apayne/test'\n")
|
|
assert scan_file_for_violations(str(f)) == []
|
|
|
|
|
|
class TestScanDirectory:
|
|
"""Directory scanning."""
|
|
|
|
def test_clean_tree(self, tmp_path):
|
|
(tmp_path / "clean.py").write_text("import os\n")
|
|
(tmp_path / "sub").mkdir()
|
|
(tmp_path / "sub" / "also_clean.py").write_text("x = 1\n")
|
|
assert scan_directory(str(tmp_path)) == []
|
|
|
|
def test_finds_violations(self, tmp_path):
|
|
(tmp_path / "bad.py").write_text("P = '/Users/x/.hermes'\n")
|
|
results = scan_directory(str(tmp_path))
|
|
assert len(results) == 1
|
|
assert results[0][0].endswith("bad.py")
|
|
|
|
def test_skips_tests(self, tmp_path):
|
|
(tmp_path / "test_something.py").write_text("P = '/Users/x/.hermes'\n")
|
|
assert scan_directory(str(tmp_path)) == []
|
|
|
|
def test_skips_pycache(self, tmp_path):
|
|
cache = tmp_path / "__pycache__"
|
|
cache.mkdir()
|
|
(cache / "cached.py").write_text("P = '/Users/x/.hermes'\n")
|
|
assert scan_directory(str(tmp_path)) == []
|