"""Pure-function tests for parametrize-base-branch (v1.1 task 5).""" import importlib.util import json import sys from pathlib import Path from unittest.mock import patch spec = importlib.util.spec_from_file_location( "st", str(Path(__file__).resolve().parent.parent / "scripts" / "status.py") ) st = importlib.util.module_from_spec(spec) spec.loader.exec_module(st) class TestBaseBranch: def test_default_main(self): assert st._base_branch(None) == "main" assert st._base_branch({}) == "main" assert st._base_branch({"blast_radius": {}}) == "main" assert st._base_branch({"blast_radius": {"base_branch": None}}) == "main" def test_explicit_value(self): assert st._base_branch({"blast_radius": {"base_branch": "trunk"}}) == "trunk" assert st._base_branch({"blast_radius": {"base_branch": "develop"}}) == "develop" assert st._base_branch({"blast_radius": {"base_branch": "master"}}) == "master" def test_empty_string_with_warning(self): assert st._base_branch({"blast_radius": {"base_branch": ""}}) == "main" def test_non_string_with_warning(self): assert st._base_branch({"blast_radius": {"base_branch": 42}}) == "42" assert st._base_branch({"blast_radius": {"base_branch": True}}) == "True" class TestDriftGateBranch: """Verify _gate_worktree_drift uses the configured base_branch.""" def make_state(self, worktree_path="/tmp/wt"): return {"worktree_path": worktree_path} def make_cfg(self, base_branch="main", file_scope=None): fs = file_scope or ["src/", "tests/"] cfg = {"blast_radius": {"file_scope": list(fs)}} cfg["blast_radius"]["base_branch"] = base_branch return cfg @patch("subprocess.run") def test_uses_configured_branch(self, mock_run, tmp_path): mock_run.return_value.returncode = 0 mock_run.return_value.stdout = "" mock_run.return_value.stderr = "" cfg = self.make_cfg(base_branch="trunk") st._gate_worktree_drift({"worktree_path": str(tmp_path)}, cfg, None) call = mock_run.call_args assert call is not None args = call[0][0] assert "trunk...HEAD" in args, f"expected 'trunk...HEAD' in {args}" @patch("subprocess.run") def test_falls_back_to_main(self, mock_run, tmp_path): mock_run.return_value.returncode = 0 mock_run.return_value.stdout = "" mock_run.return_value.stderr = "" cfg = self.make_cfg(base_branch="main") st._gate_worktree_drift({"worktree_path": str(tmp_path)}, cfg, None) call = mock_run.call_args args = call[0][0] assert "main...HEAD" in args, f"expected 'main...HEAD' in {args}" @patch("subprocess.run") def test_bad_revision_skips_with_warning(self, mock_run, tmp_path): mock_run.return_value.returncode = 128 mock_run.return_value.stdout = "" mock_run.return_value.stderr = "fatal: bad revision 'not_a_branch'" rv = st._gate_worktree_drift( {"worktree_path": str(tmp_path)}, self.make_cfg(base_branch="not_a_branch"), None) assert rv is None @patch("subprocess.run") def test_drift_detected(self, mock_run, tmp_path): mock_run.return_value.returncode = 0 mock_run.return_value.stdout = "src/valid.py\nextraneous.txt\n" mock_run.return_value.stderr = "" rv = st._gate_worktree_drift( {"worktree_path": str(tmp_path)}, self.make_cfg(), None) assert rv is not None assert rv["halt_reason"] == "drift_detected" assert "extraneous.txt" in rv["out_of_scope_files"] @patch("subprocess.run") def test_drift_in_scope_ok(self, mock_run, tmp_path): mock_run.return_value.returncode = 0 mock_run.return_value.stdout = "src/valid.py\n" mock_run.return_value.stderr = "" rv = st._gate_worktree_drift( {"worktree_path": str(tmp_path)}, self.make_cfg(), None) assert rv is None def test_no_worktree_returns_none(self): assert st._gate_worktree_drift({}, {}, None) is None def test_missing_worktree_dir_returns_none(self): rv = st._gate_worktree_drift( {"worktree_path": "/nonexistent_path_xyz"}, self.make_cfg(), None) assert rv is None def test_empty_file_scope_returns_none(self, tmp_path): cfg = {"blast_radius": {"file_scope": [], "base_branch": "main"}} rv = st._gate_worktree_drift( {"worktree_path": str(tmp_path)}, cfg, None) assert rv is None def test_template_includes_base_branch(self): tpl_path = (Path(__file__).resolve().parent.parent / "templates" / "loops" / "self-improvement" / "loop.json") tpl_cfg = json.loads(tpl_path.read_text()) assert tpl_cfg["blast_radius"]["base_branch"] == "main"