Private
Public Access
0
0
Files
manual_slop/tests/test_test_sandbox.py
T

194 lines
7.2 KiB
Python

"""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 <project_root>/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 <project_root>/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)"