"""Tests for scripts/audit_test_sandbox_violations.py (Phase 2, FR4) and the Python audit guard in tests/conftest.py (Phase 3, FR1). """ from __future__ import annotations import os import re import subprocess import sys from pathlib import Path import pytest def test_audit_runs_without_error() -> None: """The audit script runs and exits cleanly.""" result = subprocess.run( [sys.executable, "scripts/audit_test_sandbox_violations.py"], capture_output=True, text=True, cwd=str(Path(__file__).resolve().parent.parent) ) assert result.returncode in (0, 1), f"Unexpected exit code: {result.returncode}" def test_audit_flags_toml_basename_pattern() -> None: """A test source line with Path('manual_slop.toml') is flagged by the pattern.""" pattern = re.compile(r'Path\(["\'](?:manual_slop|config|credentials|presets|personas|tool_presets|workspace_profiles|project|manualslop_layout|manual_slop_history)\.toml["\']') assert pattern.search('Path("manual_slop.toml").write_text("x")'), "Pattern should match" def test_audit_flags_project_root_path() -> None: """A test source line with Path('C:/projects/...') is flagged.""" pattern = re.compile(r'Path\(["\']C:[/\\]+projects') assert pattern.search('base_dir = Path("C:/projects/test")'), "Pattern should match" def test_audit_flags_tempfile_mkdtemp() -> None: """A test source line with bare tempfile.mkdtemp() is flagged.""" pattern = re.compile(r"tempfile\.mk(?:dt|st)emp\(") assert pattern.search('tmp = tempfile.mkdtemp()'), "Pattern should match" assert pattern.search('tmp = tempfile.mkstemp()'), "Pattern should match" def test_audit_flags_tests_artifacts_literal() -> None: """A test source line with Path('tests/artifacts/...') literal is flagged.""" pattern = re.compile(r'Path\(["\']tests/artifacts/') assert pattern.search('p = Path("tests/artifacts/some_file.txt")'), "Pattern should match" def test_audit_passes_clean_file() -> None: """A test source line using tmp_path passes the audit patterns.""" content = 'tmp_path.joinpath("foo.txt").write_text("x")\n' patterns = [ re.compile(r'Path\(["\'](?:manual_slop|config)\.toml["\']'), re.compile(r'Path\(["\']C:[/\\]+projects'), re.compile(r'Path\(["\']tests/artifacts/'), re.compile(r"tempfile\.mk(?:dt|st)emp\("), ] for p in patterns: assert not p.search(content), f"Pattern {p.pattern} should not match clean content" def test_audit_subprocess_clean_dir_exits_zero() -> None: """The audit returns 0 on a clean test directory.""" tmp_dir = Path("tests/artifacts/_audit_subprocess_clean") tmp_dir.mkdir(parents=True, exist_ok=True) good = tmp_dir / "test_good.py" good.write_text("def test_x(tmp_path): tmp_path.joinpath('f').write_text('x')\n", encoding="utf-8") try: result = subprocess.run( [sys.executable, "scripts/audit_test_sandbox_violations.py", "--tests-dir", str(tmp_dir), "--strict"], capture_output=True, text=True, ) assert result.returncode == 0, f"Expected exit 0, got {result.returncode}: {result.stdout}" finally: good.unlink(missing_ok=True) tmp_dir.rmdir() def test_audit_subprocess_bad_dir_exits_one() -> None: """The audit returns 1 on a directory with a bad pattern.""" tmp_dir = Path("tests/artifacts/_audit_subprocess_bad") tmp_dir.mkdir(parents=True, exist_ok=True) bad = tmp_dir / "test_bad.py" bad.write_text('Path("manual_slop.toml").write_text("x")\n', encoding="utf-8") try: result = subprocess.run( [sys.executable, "scripts/audit_test_sandbox_violations.py", "--tests-dir", str(tmp_dir), "--strict"], capture_output=True, text=True, ) assert result.returncode == 1, f"Expected exit 1, got {result.returncode}" finally: bad.unlink(missing_ok=True) tmp_dir.rmdir() def test_sandbox_blocks_writes_outside_tests_dir() -> None: """A write to /manual_slop.toml raises TEST_SANDBOX_VIOLATION.""" bad_path = Path(__file__).resolve().parent.parent / "manual_slop.toml" if bad_path.exists(): original = bad_path.read_bytes() existed = True else: existed = False original = b"" try: with pytest.raises(RuntimeError, match="TEST_SANDBOX_VIOLATION"): bad_path.write_text("corrupt", encoding="utf-8") finally: if existed: bad_path.write_bytes(original) elif bad_path.exists(): bad_path.unlink() def test_sandbox_allows_writes_inside_tests_dir(tmp_path) -> None: """A write to tmp_path (which lives under tests/artifacts/_pytest_tmp) succeeds.""" target = tmp_path / "foo.txt" target.write_text("ok", encoding="utf-8") assert target.read_text(encoding="utf-8") == "ok" def test_sandbox_allows_writes_inside_tests_artifacts() -> None: """A write to tests/artifacts/_sandbox_test_allows/foo.txt succeeds.""" p = Path("tests/artifacts/_sandbox_test_allows/foo.txt") p.parent.mkdir(parents=True, exist_ok=True) try: p.write_text("ok", encoding="utf-8") assert p.read_text(encoding="utf-8") == "ok" finally: p.unlink(missing_ok=True) if p.parent.exists(): p.parent.rmdir() def test_sandbox_does_not_block_reads() -> None: """A read of /pyproject.toml succeeds (reads are always allowed).""" pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml" content = pyproject.read_text(encoding="utf-8") assert "[tool.pytest.ini_options]" in content def test_sandbox_allows_pytest_cache_write() -> None: """Writes under .pytest_cache are allowed (pytest internal cache).""" cache_root = Path(__file__).resolve().parent.parent / ".pytest_cache" probe = cache_root / "_sandbox_probe.txt" cache_root.mkdir(parents=True, exist_ok=True) try: probe.write_text("ok", encoding="utf-8") assert probe.read_text(encoding="utf-8") == "ok" finally: probe.unlink(missing_ok=True) def test_config_override_via_cli_flag(tmp_path) -> None: """paths.set_config_override(path) makes get_config_path() return that path.""" from src import paths config_path = tmp_path / "my_config.toml" config_path.write_text("[ai]\nprovider='gemini'\n", encoding="utf-8") original = paths._CONFIG_OVERRIDE try: paths.set_config_override(config_path) assert paths.get_config_path() == config_path finally: paths.set_config_override(original) def test_paths_get_config_path_no_env_fallback(monkeypatch) -> None: """Without an override AND without SLOP_CONFIG, get_config_path returns default.""" monkeypatch.delenv("SLOP_CONFIG", raising=False) from src import paths original = paths._CONFIG_OVERRIDE try: paths.set_config_override(None) expected = Path(__file__).resolve().parent.parent / "config.toml" assert paths.get_config_path() == expected finally: paths.set_config_override(original) def test_sloppy_py_parses_config_flag() -> None: """sloppy.py has a --config argparse argument that calls set_config_override.""" import ast sloppy = Path(__file__).resolve().parent.parent / "sloppy.py" tree = ast.parse(sloppy.read_text(encoding="utf-8")) found_config_arg = False found_set_override_call = False for node in ast.walk(tree): if isinstance(node, ast.Constant) and node.value == "--config": found_config_arg = True if isinstance(node, ast.Call): func = node.func if isinstance(func, ast.Name) and func.id == "set_config_override": found_set_override_call = True assert found_config_arg, "sloppy.py must have a --config argparse argument" assert found_set_override_call, "sloppy.py must call paths.set_config_override(args.config)"