import json import hashlib import sys import tempfile import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[0])) from validate_owner_decision_intake import BLOCKERS, validate ROOT = Path(__file__).resolve().parents[1] def complete_input(context_id): return {"schema": "owner-decision-intake.v1", "context_id": context_id, "decision_set_id": "test", "decisions": [{"blocker": blocker, "decision": "approve", "owner": "owner", "reviewer": "reviewer", "rationale": "reviewed", "evidence": [{"path": "context-lock.json", "sha256": hashlib.sha256(b'{"context_id": "ctx"}').hexdigest()}], "new_context_required": True} for blocker in BLOCKERS]} class OwnerDecisionIntakeTests(unittest.TestCase): def test_missing_input_fails_closed(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) report = validate(root, Path("owner-decisions.json"), Path("report.json")) self.assertEqual(report["status"], "blocked") self.assertEqual(report["checks"]["gate_refresh_allowed"]["status"], "blocked") self.assertEqual(report["checks"]["context_lock_unchanged"]["status"], "verified") def test_complete_shape_is_valid_but_does_not_refresh_gate(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) (root / "owner-decisions.json").write_text(json.dumps(complete_input("ctx"))) report = validate(root, Path("owner-decisions.json"), Path("report.json")) self.assertEqual(report["status"], "verified") self.assertEqual(report["checks"]["decision_approval"]["status"], "verified") self.assertEqual(report["checks"]["gate_refresh_allowed"]["status"], "blocked") def test_duplicate_blocker_is_rejected(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) value = complete_input("ctx") value["decisions"][-1]["blocker"] = BLOCKERS[0] (root / "owner-decisions.json").write_text(json.dumps(value)) report = validate(root, Path("owner-decisions.json"), Path("report.json")) self.assertEqual(report["status"], "blocked") self.assertIn(BLOCKERS[5], report["checks"]["six_blocker_identity"]["missing"]) def test_non_approve_decision_is_blocked(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) value = complete_input("ctx") value["decisions"][0]["decision"] = "defer" (root / "owner-decisions.json").write_text(json.dumps(value)) report = validate(root, Path("owner-decisions.json"), Path("report.json")) self.assertEqual(report["status"], "blocked") self.assertEqual(report["checks"]["decision_approval"]["status"], "blocked") def test_missing_decision_set_id_is_blocked(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) value = complete_input("ctx") del value["decision_set_id"] (root / "owner-decisions.json").write_text(json.dumps(value)) report = validate(root, Path("owner-decisions.json"), Path("report.json")) self.assertEqual(report["status"], "blocked") self.assertIn("decision_set_id_missing", report["errors"]) def test_invalid_evidence_path_and_hash_are_blocked(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) value = complete_input("ctx") value["decisions"][0]["evidence"] = [{"path": "../context-lock.json", "sha256": "z" * 64}] (root / "owner-decisions.json").write_text(json.dumps(value)) report = validate(root, Path("owner-decisions.json"), Path("report.json")) self.assertEqual(report["status"], "blocked") self.assertIn({"index": 0, "error": "evidence_path_escapes_repo"}, report["errors"]) if __name__ == "__main__": unittest.main()