diff --git a/scripts/release_control/staged_commit_shape_guard.py b/scripts/release_control/staged_commit_shape_guard.py index bb5211cc63..d2614c7a90 100644 --- a/scripts/release_control/staged_commit_shape_guard.py +++ b/scripts/release_control/staged_commit_shape_guard.py @@ -58,8 +58,7 @@ def load_head_repo_json(path: str) -> dict | None: def load_staged_status_payload() -> dict: - # Internal governance files are not git-tracked; always read from filesystem. - return load_repo_json(STATUS_PATH, staged=False) + return load_repo_json(STATUS_PATH, staged=True, strict_staged=True) def load_head_status_payload() -> dict | None: @@ -67,8 +66,7 @@ def load_head_status_payload() -> dict | None: def load_staged_registry_payload() -> dict: - # Internal governance files are not git-tracked; always read from filesystem. - return load_repo_json(REGISTRY_PATH, staged=False) + return load_repo_json(REGISTRY_PATH, staged=True, strict_staged=True) def _is_runtime_surface_path(path: str) -> bool: diff --git a/scripts/release_control/staged_commit_shape_guard_test.py b/scripts/release_control/staged_commit_shape_guard_test.py index 3bf53b070b..7036f42b86 100644 --- a/scripts/release_control/staged_commit_shape_guard_test.py +++ b/scripts/release_control/staged_commit_shape_guard_test.py @@ -1,19 +1,84 @@ import io +import json import os +import subprocess +import tempfile import unittest +from pathlib import Path from contextlib import redirect_stderr, redirect_stdout from unittest.mock import patch +from repo_file_io import strip_local_git_env from staged_commit_shape_guard import ( + REGISTRY_PATH, + STATUS_PATH, CONTRACT_NEUTRAL_OVERRIDE_ENV, advanced_lane_ids, format_combined_errors, lane_progress_shape_errors, + load_staged_registry_payload, + load_staged_status_payload, main, staged_promotion_proof_errors, ) +class StagedGovernanceInputTest(unittest.TestCase): + def git(self, repo_root: Path, *args: str) -> None: + env = os.environ.copy() + strip_local_git_env(env) + subprocess.run( + ["git", *args], + cwd=repo_root, + env=env, + check=True, + capture_output=True, + text=True, + ) + + def test_staged_inputs_ignore_unstaged_edits_and_missing_worktree_files(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + repo_root = Path(tmpdir) + self.git(repo_root, "init", "--initial-branch=main") + loaders = ( + (STATUS_PATH, load_staged_status_payload), + (REGISTRY_PATH, load_staged_registry_payload), + ) + expected = {"fixture_source": "index"} + for rel, _ in loaders: + path = repo_root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(expected) + "\n", encoding="utf-8") + self.git(repo_root, "add", "--", *(rel for rel, _ in loaders)) + + with patch("repo_file_io.REPO_ROOT", repo_root): + for rel, loader in loaders: + path = repo_root / rel + for content in ('{"fixture_source": "working-tree"}', "invalid json"): + with self.subTest(path=rel, unstaged_content=content): + path.write_text(content + "\n", encoding="utf-8") + self.assertEqual(loader(), expected) + with self.subTest(path=rel, worktree_missing=True): + path.unlink() + self.assertEqual(loader(), expected) + + def test_missing_index_inputs_fail_closed_with_worktree_files_present(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + repo_root = Path(tmpdir) + self.git(repo_root, "init", "--initial-branch=main") + with patch("repo_file_io.REPO_ROOT", repo_root): + for rel, loader in ( + (STATUS_PATH, load_staged_status_payload), + (REGISTRY_PATH, load_staged_registry_payload), + ): + with self.subTest(path=rel): + path = repo_root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('{"fixture_source": "working-tree"}\n', encoding="utf-8") + with self.assertRaises(FileNotFoundError): + loader() + + class StagedCommitShapeGuardTest(unittest.TestCase): def test_advanced_lane_ids_detects_score_or_completion_progress(self) -> None: previous = {