fix: resolve conflicts for PR #378
Merge prgs/master and keep both review-merge state machine (#290) and native MCP preference (#270) imports, skill entries, and MCP tools.
This commit is contained in:
@@ -62,7 +62,24 @@ class TestPreflightWarnings(unittest.TestCase):
|
||||
self.assertIn("_encode_commit_payload.py", status["preflight_warnings"][0])
|
||||
|
||||
|
||||
# Issue-write tools are profile-gated (#69); gitea_lock_issue requires
|
||||
# gitea.issue.comment (see task_capability_map), so the gate must be
|
||||
# seeded exactly like tests/test_mcp_server.py::TestIssueLocking (#359).
|
||||
ISSUE_WRITE_ENV = {
|
||||
"GITEA_ALLOWED_OPERATIONS": (
|
||||
"gitea.issue.create,gitea.issue.close,gitea.issue.comment"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class TestIssueLockArtifactWarning(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._env_patcher = patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True)
|
||||
self._env_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self._env_patcher.stop()
|
||||
|
||||
@patch("mcp_server.api_get_all", return_value=[])
|
||||
@patch("mcp_server._auth", return_value="token x")
|
||||
@patch("mcp_server._resolve", return_value=("h", "o", "r"))
|
||||
@@ -72,6 +89,9 @@ class TestIssueLockArtifactWarning(unittest.TestCase):
|
||||
mock_state.return_value = {
|
||||
"current_branch": "master",
|
||||
"porcelain_status": "?? _emit_payload.py\n",
|
||||
"base_equivalent": True,
|
||||
"inspected_git_root": "/scratch/wt",
|
||||
"base_branch": "origin/master",
|
||||
}
|
||||
result = mcp_server.gitea_lock_issue(
|
||||
issue_number=261,
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Tests for already-landed final-report wording validator (#298).
|
||||
|
||||
An already-landed PR is reconciliation-only, never review/merge
|
||||
eligible, and a head SHA may not be called reviewed unless validation
|
||||
and diff review actually passed. Final reports and controller handoffs
|
||||
must use the reconciliation wording, not the legacy eligible/reviewed
|
||||
fields.
|
||||
"""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from review_proofs import assess_already_landed_report_wording # noqa: E402
|
||||
|
||||
|
||||
GOOD_ALREADY_LANDED = (
|
||||
"Controller Handoff\n"
|
||||
"- Oldest open PR requiring action: PR #278\n"
|
||||
"- Eligibility class: ALREADY_LANDED_RECONCILE_REQUIRED\n"
|
||||
"- Candidate head SHA: 2a544b7\n"
|
||||
"- Reviewed head SHA: none\n"
|
||||
"- Review worktree used: false\n"
|
||||
)
|
||||
|
||||
|
||||
class TestAlreadyLandedWording(unittest.TestCase):
|
||||
def test_correct_reconciliation_wording_passes(self):
|
||||
result = assess_already_landed_report_wording(
|
||||
GOOD_ALREADY_LANDED, already_landed=True
|
||||
)
|
||||
self.assertTrue(result["complete"])
|
||||
self.assertFalse(result["downgraded"])
|
||||
self.assertEqual(result["reasons"], [])
|
||||
|
||||
def test_oldest_eligible_wording_fails(self):
|
||||
report = GOOD_ALREADY_LANDED + "- oldest eligible PR: PR #278\n"
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=True
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("oldest eligible pr" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
def test_next_eligible_wording_fails(self):
|
||||
report = GOOD_ALREADY_LANDED + "- next eligible PR: PR #278\n"
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=True
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
def test_pinned_reviewed_head_fails(self):
|
||||
report = GOOD_ALREADY_LANDED + "- Pinned reviewed head: 2a544b7\n"
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=True
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("pinned reviewed head" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
def test_scratch_worktree_field_fails(self):
|
||||
report = GOOD_ALREADY_LANDED + "- Scratch worktree used: False\n"
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=True
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
def test_missing_eligibility_class_fails(self):
|
||||
report = (
|
||||
"Controller Handoff\n"
|
||||
"- Oldest open PR requiring action: PR #278\n"
|
||||
"- Candidate head SHA: 2a544b7\n"
|
||||
"- Reviewed head SHA: none\n"
|
||||
"- Review worktree used: false\n"
|
||||
)
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=True
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("eligibility class" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
def test_populated_reviewed_head_sha_fails(self):
|
||||
report = GOOD_ALREADY_LANDED.replace(
|
||||
"- Reviewed head SHA: none\n",
|
||||
"- Reviewed head SHA: 2a544b7\n",
|
||||
)
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=True
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("reviewed head" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
|
||||
class TestNonAlreadyLandedReports(unittest.TestCase):
|
||||
def test_normal_reviewed_report_passes(self):
|
||||
report = (
|
||||
"- Selected PR: 281\n"
|
||||
"- Pinned reviewed head: abc1234\n"
|
||||
"- Review decision: approve\n"
|
||||
)
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=False, review_validated=True
|
||||
)
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
def test_blocked_infra_report_with_pinned_head_fails(self):
|
||||
report = (
|
||||
"- Selected PR: 281\n"
|
||||
"- Pinned reviewed head: abc1234\n"
|
||||
"- Current status: blocked on infra_stop\n"
|
||||
)
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=False, review_validated=False
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("before validation" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
def test_validation_failed_report_with_reviewed_sha_fails(self):
|
||||
report = (
|
||||
"- Selected PR: 281\n"
|
||||
"- Reviewed head SHA: abc1234\n"
|
||||
"- Validation: full suite failed\n"
|
||||
)
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=False, review_validated=False
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
def test_validation_failed_report_with_reviewed_none_passes(self):
|
||||
report = (
|
||||
"- Selected PR: 281\n"
|
||||
"- Reviewed head SHA: none\n"
|
||||
"- Validation: full suite failed\n"
|
||||
)
|
||||
result = assess_already_landed_report_wording(
|
||||
report, already_landed=False, review_validated=False
|
||||
)
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Tests for issue claim heartbeat leases (#268)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import issue_claim_heartbeat as ich # noqa: E402
|
||||
|
||||
|
||||
class TestHeartbeatFormatting(unittest.TestCase):
|
||||
def test_format_and_parse_roundtrip(self):
|
||||
body = ich.format_heartbeat_body(
|
||||
kind="claim",
|
||||
issue_number=268,
|
||||
branch="feat/issue-268-heartbeat-leases",
|
||||
phase="claimed",
|
||||
profile="prgs-author",
|
||||
next_action="implement module",
|
||||
)
|
||||
parsed = ich.parse_heartbeat_comment(body)
|
||||
self.assertIsNotNone(parsed)
|
||||
assert parsed is not None
|
||||
self.assertEqual(parsed["issue_number"], 268)
|
||||
self.assertEqual(parsed["branch"], "feat/issue-268-heartbeat-leases")
|
||||
self.assertEqual(parsed["phase"], "claimed")
|
||||
|
||||
|
||||
class TestClaimClassification(unittest.TestCase):
|
||||
NOW = datetime(2026, 7, 7, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
def _comment(self, *, minutes_ago: int, kind: str = "progress") -> dict:
|
||||
ts = (self.NOW - timedelta(minutes=minutes_ago)).isoformat()
|
||||
body = ich.format_heartbeat_body(
|
||||
kind=kind,
|
||||
issue_number=268,
|
||||
branch="feat/issue-268-heartbeat-leases",
|
||||
phase="implementation",
|
||||
profile="prgs-author",
|
||||
)
|
||||
return {"id": 1, "body": body, "created_at": ts, "updated_at": ts}
|
||||
|
||||
def test_active_claim_within_lease(self):
|
||||
issue = {"number": 268, "title": "t", "labels": [{"name": "status:in-progress"}]}
|
||||
result = ich.classify_issue_claim(
|
||||
issue=issue,
|
||||
comments=[self._comment(minutes_ago=5)],
|
||||
now=self.NOW,
|
||||
)
|
||||
self.assertEqual(result["status"], "active")
|
||||
|
||||
def test_stale_claim_without_branch(self):
|
||||
issue = {"number": 268, "title": "t", "labels": [{"name": "status:in-progress"}]}
|
||||
result = ich.classify_issue_claim(
|
||||
issue=issue,
|
||||
comments=[self._comment(minutes_ago=45)],
|
||||
now=self.NOW,
|
||||
)
|
||||
self.assertEqual(result["status"], "stale")
|
||||
|
||||
def test_reclaimable_after_sixty_minutes_without_branch(self):
|
||||
issue = {"number": 268, "title": "t", "labels": [{"name": "status:in-progress"}]}
|
||||
result = ich.classify_issue_claim(
|
||||
issue=issue,
|
||||
comments=[self._comment(minutes_ago=90)],
|
||||
now=self.NOW,
|
||||
)
|
||||
self.assertEqual(result["status"], "reclaimable")
|
||||
|
||||
def test_awaiting_review_when_open_pr_exists(self):
|
||||
issue = {"number": 268, "title": "t", "labels": [{"name": "status:in-progress"}]}
|
||||
open_prs = [
|
||||
{
|
||||
"number": 400,
|
||||
"title": "feat",
|
||||
"body": "Closes #268",
|
||||
"head": {"ref": "feat/issue-268-heartbeat-leases"},
|
||||
}
|
||||
]
|
||||
result = ich.classify_issue_claim(
|
||||
issue=issue,
|
||||
comments=[self._comment(minutes_ago=120)],
|
||||
open_prs=open_prs,
|
||||
now=self.NOW,
|
||||
)
|
||||
self.assertEqual(result["status"], "awaiting_review")
|
||||
|
||||
def test_phantom_claim_without_heartbeat(self):
|
||||
issue = {"number": 268, "title": "t", "labels": [{"name": "status:in-progress"}]}
|
||||
result = ich.classify_issue_claim(
|
||||
issue=issue,
|
||||
comments=[{"id": 1, "body": "working on it"}],
|
||||
now=self.NOW,
|
||||
)
|
||||
self.assertEqual(result["status"], "phantom")
|
||||
|
||||
|
||||
class TestInventory(unittest.TestCase):
|
||||
def test_build_cleanup_plan_flags_phantom(self):
|
||||
inventory = {
|
||||
"entries": [
|
||||
{"issue_number": 1, "status": "phantom", "reasons": ["no heartbeat"]},
|
||||
{"issue_number": 2, "status": "active", "reasons": []},
|
||||
]
|
||||
}
|
||||
plan = ich.build_cleanup_plan(inventory)
|
||||
self.assertEqual(len(plan), 1)
|
||||
self.assertEqual(plan[0]["issue_number"], 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -172,6 +172,8 @@ class TestAllowedIssueWritesProceed(IssueWriteGateBase):
|
||||
return [{"id": 10, "name": "status:in-progress"}]
|
||||
if method == "POST" and "/issues/9/labels" in url:
|
||||
return [{"name": "status:in-progress"}]
|
||||
if method == "POST" and "/issues/9/comments" in url:
|
||||
return {"id": 501}
|
||||
if method == "GET" and url.endswith("/user"):
|
||||
return {"login": "author-user"}
|
||||
return {}
|
||||
|
||||
@@ -78,6 +78,12 @@ def test_reconcile_landed_workflow_contract():
|
||||
assert "canonical: true" in text
|
||||
assert "Do not review or merge normal PRs" in text
|
||||
assert "Already-landed proof" in text
|
||||
# Issue #302: partial reconciliation policy must stay documented.
|
||||
assert "## 12A. Partial reconciliation policy (#302)" in text
|
||||
assert "comment-then-stop" in text
|
||||
assert "PARTIAL_RECONCILE_COMMENT_THEN_STOP" in text
|
||||
assert "RECOVERY_HANDOFF_ONLY" in text
|
||||
assert "resolve_partial_reconciliation_plan" in text
|
||||
|
||||
|
||||
def test_create_issue_workflow_contract():
|
||||
@@ -136,6 +142,18 @@ def test_create_issue_final_report_verifier_exported():
|
||||
assert callable(assess_create_issue_final_report)
|
||||
|
||||
|
||||
def test_merge_simulation_verifier_exported():
|
||||
from review_proofs import assess_merge_simulation_report
|
||||
|
||||
assert callable(assess_merge_simulation_report)
|
||||
|
||||
|
||||
def test_validation_integrity_verifier_exported():
|
||||
from review_proofs import assess_validation_integrity_report
|
||||
|
||||
assert callable(assess_validation_integrity_report)
|
||||
|
||||
|
||||
def test_prior_blocker_skip_verifier_exported():
|
||||
from review_proofs import assess_prior_blocker_skip_proof
|
||||
|
||||
@@ -148,7 +166,31 @@ def test_non_mergeable_skip_verifier_exported():
|
||||
assert callable(assess_non_mergeable_skip_proof)
|
||||
|
||||
|
||||
def test_validation_worktree_edit_verifier_exported():
|
||||
from review_proofs import assess_validation_worktree_edit_report
|
||||
|
||||
assert callable(assess_validation_worktree_edit_report)
|
||||
|
||||
|
||||
def test_inventory_worktree_verifier_exported():
|
||||
from review_proofs import assess_inventory_worktree_report
|
||||
|
||||
assert callable(assess_inventory_worktree_report)
|
||||
|
||||
|
||||
def test_infra_stop_handoff_verifier_exported():
|
||||
from review_proofs import assess_infra_stop_handoff_report
|
||||
|
||||
assert callable(assess_infra_stop_handoff_report)
|
||||
|
||||
|
||||
def test_mutation_categories_verifier_exported():
|
||||
from review_proofs import assess_mutation_categories_report
|
||||
|
||||
assert callable(assess_mutation_categories_report)
|
||||
|
||||
|
||||
def test_worktree_ownership_verifier_exported():
|
||||
from review_proofs import assess_worktree_ownership_report
|
||||
|
||||
assert callable(assess_worktree_ownership_report)
|
||||
assert callable(assess_worktree_ownership_report)
|
||||
@@ -321,15 +321,17 @@ class TestMarkIssue(unittest.TestCase):
|
||||
@patch("mcp_server.api_request")
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
def test_start_adds_label(self, _auth, mock_api):
|
||||
# First call: get labels; second call: add label
|
||||
# labels, add label, post claim heartbeat comment
|
||||
mock_api.side_effect = [
|
||||
[{"id": 10, "name": "status:in-progress"}],
|
||||
[{"name": "status:in-progress"}],
|
||||
{"id": 99},
|
||||
]
|
||||
with patch.dict(os.environ, ISSUE_WRITE_ENV, clear=True):
|
||||
result = gitea_mark_issue(issue_number=5, action="start")
|
||||
self.assertTrue(result["success"])
|
||||
self.assertIn("claimed", result["message"])
|
||||
self.assertTrue(result.get("heartbeat_posted"))
|
||||
|
||||
@patch("mcp_server.api_request")
|
||||
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Tests for native MCP preference gate and shell circuit breaker (#270)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import native_mcp_preference as nmp # noqa: E402
|
||||
|
||||
|
||||
class TestShellHealthCircuitBreaker(unittest.TestCase):
|
||||
def setUp(self):
|
||||
nmp.clear_shell_health_for_tests()
|
||||
|
||||
def test_two_spawn_failures_hard_stop(self):
|
||||
nmp.record_shell_spawn_outcome(exit_code=-1, stdout="", stderr="")
|
||||
status = nmp.record_shell_spawn_outcome(exit_code=-1, stdout="", stderr="")
|
||||
self.assertTrue(status["hard_stopped"])
|
||||
self.assertFalse(status["shell_use_allowed"])
|
||||
self.assertEqual(status["consecutive_spawn_failures"], 2)
|
||||
|
||||
def test_success_resets_breaker(self):
|
||||
nmp.record_shell_spawn_outcome(exit_code=-1, stdout="", stderr="")
|
||||
status = nmp.record_shell_spawn_outcome(exit_code=0, stdout="ok", stderr="")
|
||||
self.assertFalse(status["hard_stopped"])
|
||||
self.assertEqual(status["consecutive_spawn_failures"], 0)
|
||||
|
||||
|
||||
class TestNativeMcpPreferenceGate(unittest.TestCase):
|
||||
def setUp(self):
|
||||
nmp.clear_shell_health_for_tests()
|
||||
|
||||
def test_mcp_available_blocks_local_script(self):
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="create_pr",
|
||||
command_or_detail="python create_pr.py --remote prgs --title T --head h",
|
||||
mcp_available=True,
|
||||
mcp_tool_visible=True,
|
||||
)
|
||||
self.assertTrue(result["block"])
|
||||
self.assertEqual(result["path_kind"], "shell_script")
|
||||
|
||||
def test_mcp_native_path_allowed(self):
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="comment_issue",
|
||||
path_kind="mcp_native",
|
||||
mcp_available=True,
|
||||
mcp_tool_visible=True,
|
||||
)
|
||||
self.assertFalse(result["block"])
|
||||
self.assertTrue(result["allowed"])
|
||||
|
||||
def test_mcp_unavailable_requires_terminal_report(self):
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="merge_pr",
|
||||
path_kind="shell_script",
|
||||
mcp_available=False,
|
||||
mcp_tool_visible=False,
|
||||
)
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(
|
||||
any("terminal recovery report" in r for r in result["reasons"])
|
||||
)
|
||||
self.assertIn(nmp.TERMINAL_REPORT_HEADING, result["terminal_report"])
|
||||
|
||||
def test_recovery_fallback_allowed_with_proof(self):
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="merge_pr",
|
||||
path_kind="shell_script",
|
||||
command_or_detail="Recovery mode: MCP tools unavailable in this session.",
|
||||
mcp_available=False,
|
||||
recovery_mode=True,
|
||||
recovery_proof_complete=True,
|
||||
)
|
||||
self.assertFalse(result["block"])
|
||||
|
||||
def test_cli_auth_divergence_blocked(self):
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="create_pr",
|
||||
command_or_detail="GITEA_MCP_PROFILE=prgs-reviewer python create_pr.py",
|
||||
mcp_available=True,
|
||||
active_profile="prgs-author",
|
||||
)
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(any("CLI auth divergence" in r for r in result["reasons"]))
|
||||
|
||||
def test_unsafe_helper_fallback_denied(self):
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="commit_files",
|
||||
command_or_detail="python _encode_payload.py",
|
||||
mcp_available=True,
|
||||
mcp_tool_visible=True,
|
||||
)
|
||||
self.assertTrue(result["block"])
|
||||
self.assertEqual(result["path_kind"], "unsafe_helper")
|
||||
|
||||
def test_reviewer_mcp_server_touch_blocked(self):
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="review_pr",
|
||||
command_or_detail="pkill -f gitea_mcp_server.py",
|
||||
mcp_available=True,
|
||||
role="reviewer",
|
||||
active_profile="prgs-reviewer",
|
||||
)
|
||||
self.assertTrue(result["block"])
|
||||
self.assertEqual(result["path_kind"], "mcp_server_touch")
|
||||
|
||||
def test_hard_stopped_shell_blocks_non_mcp_path(self):
|
||||
nmp.record_shell_spawn_outcome(exit_code=-1, stdout="", stderr="")
|
||||
nmp.record_shell_spawn_outcome(exit_code=-1, stdout="", stderr="")
|
||||
result = nmp.assess_gitea_operation_path(
|
||||
task="lock_issue",
|
||||
path_kind="shell_script",
|
||||
mcp_available=True,
|
||||
)
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(any("circuit breaker" in r for r in result["reasons"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Tests for the dedicated reconciliation controller-handoff schema (#303).
|
||||
|
||||
Already-landed PR reconciliation runs must emit the reconciliation
|
||||
schema, not stale author/reviewer handoff fields; observed git ref
|
||||
mutations (fetch) must be reported, and legacy fields fail validation.
|
||||
"""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from review_proofs import assess_reconciliation_handoff # noqa: E402
|
||||
|
||||
|
||||
def _good_handoff(**overrides):
|
||||
fields = {
|
||||
"Task": "Reconcile already-landed open PRs",
|
||||
"Repo": "prgs/Scaled-Tech-Consulting/Gitea-Tools",
|
||||
"Role/profile": "prgs-reconciler",
|
||||
"Identity": "jcwalker3",
|
||||
"Selected PR": "278",
|
||||
"PR live state": "open",
|
||||
"Candidate head SHA": "2a544b7",
|
||||
"Target branch": "master",
|
||||
"Target branch SHA": "fa0cb12",
|
||||
"Ancestor proof": "candidate head is ancestor of target branch SHA",
|
||||
"Linked issue": "263",
|
||||
"Linked issue live status": "open (fetched live this session)",
|
||||
"Eligibility class": "ALREADY_LANDED_RECONCILE_REQUIRED",
|
||||
"Capabilities proven": "gitea.pr.comment",
|
||||
"Missing capabilities": "gitea.pr.close",
|
||||
"PR comments posted": "1 (PR #278 reconciliation notice)",
|
||||
"Issue comments posted": "none",
|
||||
"PRs closed": "none",
|
||||
"Issues closed": "none",
|
||||
"Git ref mutations": "git fetch prgs master",
|
||||
"Worktree mutations": "none",
|
||||
"MCP/Gitea mutations": "PR comment on #278",
|
||||
"External-state mutations": "none",
|
||||
"Read-only diagnostics": "gitea_view_pr 278, gitea_view_issue 263",
|
||||
"Blocker": "PR close capability missing",
|
||||
"Safe next action": "operator closes PR #278 or grants close capability",
|
||||
"No review/merge confirmation": "no review or merge mutations performed",
|
||||
}
|
||||
fields.update(overrides)
|
||||
lines = ["Controller Handoff"]
|
||||
lines += [f"- {k}: {v}" for k, v in fields.items() if v is not None]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
class TestReconciliationSchemaPasses(unittest.TestCase):
|
||||
def test_blocked_reconciliation_passes(self):
|
||||
result = assess_reconciliation_handoff(
|
||||
_good_handoff(),
|
||||
observed_commands=["git fetch prgs master"],
|
||||
)
|
||||
self.assertTrue(result["complete"])
|
||||
self.assertEqual(result["reasons"], [])
|
||||
|
||||
def test_successful_pr_close_passes(self):
|
||||
report = _good_handoff(**{
|
||||
"Missing capabilities": "none",
|
||||
"PRs closed": "PR #278",
|
||||
"Blocker": "none",
|
||||
"Safe next action": "none; reconciliation complete",
|
||||
})
|
||||
result = assess_reconciliation_handoff(
|
||||
report, observed_commands=["git fetch prgs master"]
|
||||
)
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
def test_comment_only_reconciliation_passes(self):
|
||||
report = _good_handoff(**{
|
||||
"Git ref mutations": "none",
|
||||
})
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
def test_noop_already_closed_passes(self):
|
||||
report = _good_handoff(**{
|
||||
"PR live state": "closed",
|
||||
"PR comments posted": "none",
|
||||
"MCP/Gitea mutations": "none",
|
||||
"Git ref mutations": "none",
|
||||
"Blocker": "none",
|
||||
"Safe next action": "none; PR already closed",
|
||||
})
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
|
||||
class TestSchemaViolationsFail(unittest.TestCase):
|
||||
def test_missing_linked_issue_proof_fails(self):
|
||||
report = _good_handoff(**{"Linked issue live status": None})
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("linked issue live status" in r.lower()
|
||||
for r in result["reasons"])
|
||||
)
|
||||
|
||||
def test_wrong_eligibility_class_fails(self):
|
||||
report = _good_handoff(**{"Eligibility class": "REVIEW_ELIGIBLE"})
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
def test_missing_handoff_section_fails(self):
|
||||
result = assess_reconciliation_handoff(
|
||||
"no handoff here", observed_commands=[]
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
|
||||
class TestStaleFieldsFail(unittest.TestCase):
|
||||
def test_pr_number_opened_fails(self):
|
||||
report = _good_handoff() + "- PR number opened: 278\n"
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("pr number opened" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
def test_pinned_reviewed_head_fails(self):
|
||||
report = _good_handoff() + "- Pinned reviewed head: 2a544b7\n"
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
def test_scratch_worktree_used_fails(self):
|
||||
report = _good_handoff() + "- Scratch worktree used: False\n"
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
def test_workspace_mutations_fails(self):
|
||||
report = _good_handoff() + "- Workspace mutations: None\n"
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
def test_author_lock_fields_fail(self):
|
||||
report = (
|
||||
_good_handoff()
|
||||
+ "- Issue lock proof: locked before diff\n"
|
||||
+ "- Claim/comment status: claimed\n"
|
||||
)
|
||||
result = assess_reconciliation_handoff(report, observed_commands=[])
|
||||
self.assertFalse(result["complete"])
|
||||
|
||||
|
||||
class TestObservedFetchMustBeReported(unittest.TestCase):
|
||||
def test_fetch_with_ref_mutations_none_fails(self):
|
||||
report = _good_handoff(**{"Git ref mutations": "none"})
|
||||
result = assess_reconciliation_handoff(
|
||||
report, observed_commands=["git fetch prgs master"]
|
||||
)
|
||||
self.assertFalse(result["complete"])
|
||||
self.assertTrue(
|
||||
any("git ref mutation" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+369
-3
@@ -23,7 +23,12 @@ sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.par
|
||||
from review_proofs import ( # noqa: E402
|
||||
ISSUE_SELECTION_CONTINUATION_EXPLICIT,
|
||||
ISSUE_SELECTION_REPRESENTED_BY_OPEN_PR,
|
||||
assess_already_landed_report_state,
|
||||
assess_already_landed_review_gate,
|
||||
assess_author_pr_report,
|
||||
assess_partial_reconciliation_report,
|
||||
resolve_partial_reconciliation_plan,
|
||||
assess_validation_environment_proof,
|
||||
assess_full_suite_failure_approval_gate,
|
||||
assess_queue_ordering_proof,
|
||||
assess_reviewer_baseline_validation_proof,
|
||||
@@ -2315,10 +2320,108 @@ class TestCreateIssueFinalReport(unittest.TestCase):
|
||||
self.assertTrue(result["downgraded"])
|
||||
|
||||
|
||||
class TestQueueOrderingProof(unittest.TestCase):
|
||||
"""Issue #321: reviewer queue selection must prove ordering."""
|
||||
class TestValidationEnvironmentProof(unittest.TestCase):
|
||||
"""Issue #311: exact validation command and execution environment."""
|
||||
|
||||
ROOT = "/repo/Gitea-Tools/branches/review-pr-280"
|
||||
|
||||
def test_venv_pytest_with_full_report_passes(self):
|
||||
result = assess_validation_environment_proof(
|
||||
report_text=(
|
||||
"Validation command: venv/bin/pytest tests/ --ignore=branches\n"
|
||||
f"Working directory: {self.ROOT}\n"
|
||||
"Result: 1014 passed, 6 skipped"
|
||||
),
|
||||
)
|
||||
self.assertTrue(result["proven"])
|
||||
|
||||
def test_bare_pytest_without_path_proof_fails(self):
|
||||
result = assess_validation_environment_proof(
|
||||
report_text=(
|
||||
"Validation command: pytest\n"
|
||||
f"Working directory: {self.ROOT}\n"
|
||||
"1014 passed, 6 skipped"
|
||||
),
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
|
||||
def test_bare_pytest_with_path_and_version_proof_passes(self):
|
||||
result = assess_validation_environment_proof(
|
||||
report_text=(
|
||||
"Validation command: pytest tests/\n"
|
||||
f"Working directory: {self.ROOT}\n"
|
||||
"which pytest: /repo/Gitea-Tools/venv/bin/pytest\n"
|
||||
"pytest --version: pytest 8.3.4\n"
|
||||
"Resolves to the project venv: yes\n"
|
||||
"1014 passed, 6 skipped"
|
||||
),
|
||||
)
|
||||
self.assertTrue(result["proven"])
|
||||
|
||||
def test_vague_pytest_summary_without_command_fails(self):
|
||||
result = assess_validation_environment_proof(
|
||||
report_text=(
|
||||
f"Working directory: {self.ROOT}\n"
|
||||
"pytest executed inside the worktree: 1014 passed, 6 skipped"
|
||||
),
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
|
||||
def test_structured_environment_proof_passes(self):
|
||||
result = assess_validation_environment_proof(
|
||||
validation_environment={
|
||||
"command": "pytest tests/",
|
||||
"working_directory": self.ROOT,
|
||||
"which_pytest": "/repo/Gitea-Tools/venv/bin/pytest",
|
||||
"pytest_version": "pytest 8.3.4",
|
||||
"venv_resolved": True,
|
||||
"passed": 1014,
|
||||
"skipped": 6,
|
||||
},
|
||||
)
|
||||
self.assertTrue(result["proven"])
|
||||
|
||||
def test_missing_working_directory_fails(self):
|
||||
result = assess_validation_environment_proof(
|
||||
report_text=(
|
||||
"Validation command: venv/bin/pytest tests/\n"
|
||||
"1014 passed, 6 skipped"
|
||||
),
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
|
||||
def test_build_final_report_downgrades_missing_environment_proof(self):
|
||||
report = build_final_report(
|
||||
checkout_proof=_good_checkout(),
|
||||
inventory=_good_inventory(),
|
||||
validation=_good_validation(),
|
||||
contamination=_good_contamination(),
|
||||
identity_eligible=True,
|
||||
merge_performed=False,
|
||||
issue_status_verified=True,
|
||||
capability_evidence=_good_capability_evidence(),
|
||||
sweep=_good_sweep(),
|
||||
live_state=_good_live_state(),
|
||||
role_boundary=_good_role_boundary(),
|
||||
review_mutation=_good_review_mutation(),
|
||||
controller_handoff=_good_handoff(),
|
||||
capability_proof=_good_capability_proof(),
|
||||
sweep_proof=_good_secret_sweep(),
|
||||
worktree_proof={
|
||||
"worktree_path": "/repo/branches/review-feat-issue-224",
|
||||
"porcelain_status": "",
|
||||
"pr_scope_files": ["docs/wiki/Repositories.md"],
|
||||
"scratch_used": True,
|
||||
},
|
||||
report_text=(
|
||||
f"Working directory: {self.ROOT}\n"
|
||||
"pytest executed: 1014 passed, 6 skipped"
|
||||
),
|
||||
)
|
||||
self.assertNotEqual(report["grade"], "A")
|
||||
self.assertFalse(report["validation_environment_proven"])
|
||||
|
||||
|
||||
def test_next_eligible_claim_without_policy_fails(self):
|
||||
result = assess_queue_ordering_proof(
|
||||
report_text="Selected the next eligible PR: PR #286.",
|
||||
)
|
||||
@@ -2540,6 +2643,266 @@ class TestReviewerBaselineValidationProof(unittest.TestCase):
|
||||
self.assertFalse(report["baseline_validation_proven"])
|
||||
|
||||
|
||||
class TestPartialReconciliationPlan(unittest.TestCase):
|
||||
"""Issue #302: policy when PR-close capability is missing (Option B)."""
|
||||
|
||||
def _plan(self, **overrides):
|
||||
kwargs = {
|
||||
"ancestor_proven": True,
|
||||
"capabilities": {
|
||||
"close_pr": False,
|
||||
"comment_pr": True,
|
||||
"close_issue": True,
|
||||
"comment_issue": True,
|
||||
},
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
return resolve_partial_reconciliation_plan(**kwargs)
|
||||
|
||||
def test_close_capability_present_allows_full_reconcile(self):
|
||||
plan = self._plan(capabilities={
|
||||
"close_pr": True, "comment_pr": True,
|
||||
"close_issue": True, "comment_issue": True,
|
||||
})
|
||||
self.assertEqual(plan["outcome"], "FULL_RECONCILE_CLOSE_ALLOWED")
|
||||
self.assertIn("close_pr", plan["allowed_mutations"])
|
||||
|
||||
def test_close_missing_with_comment_posts_comment_then_stops(self):
|
||||
plan = self._plan()
|
||||
self.assertEqual(
|
||||
plan["outcome"], "PARTIAL_RECONCILE_COMMENT_THEN_STOP")
|
||||
self.assertEqual(plan["allowed_mutations"], ("comment_pr",))
|
||||
for field in (
|
||||
"PR head SHA",
|
||||
"target branch SHA",
|
||||
"ancestor proof",
|
||||
"linked issue status",
|
||||
"required missing capability",
|
||||
):
|
||||
self.assertIn(field, plan["required_comment_fields"])
|
||||
|
||||
def test_comment_capability_missing_produces_handoff_only(self):
|
||||
plan = self._plan(capabilities={
|
||||
"close_pr": False, "comment_pr": False,
|
||||
"close_issue": True, "comment_issue": True,
|
||||
})
|
||||
self.assertEqual(plan["outcome"], "RECOVERY_HANDOFF_ONLY")
|
||||
self.assertEqual(plan["allowed_mutations"], ())
|
||||
|
||||
def test_all_capabilities_missing_produces_handoff_only(self):
|
||||
plan = self._plan(capabilities={
|
||||
"close_pr": False, "comment_pr": False,
|
||||
"close_issue": False, "comment_issue": False,
|
||||
})
|
||||
self.assertEqual(plan["outcome"], "RECOVERY_HANDOFF_ONLY")
|
||||
self.assertEqual(plan["allowed_mutations"], ())
|
||||
|
||||
def test_unproven_ancestry_blocks_all_mutations(self):
|
||||
plan = self._plan(ancestor_proven=False, capabilities={
|
||||
"close_pr": True, "comment_pr": True,
|
||||
"close_issue": True, "comment_issue": True,
|
||||
})
|
||||
self.assertEqual(plan["outcome"], "GATE_NOT_PROVEN")
|
||||
self.assertEqual(plan["allowed_mutations"], ())
|
||||
|
||||
def test_missing_capability_dict_fails_closed(self):
|
||||
plan = self._plan(capabilities=None)
|
||||
self.assertEqual(plan["outcome"], "RECOVERY_HANDOFF_ONLY")
|
||||
self.assertEqual(plan["allowed_mutations"], ())
|
||||
|
||||
|
||||
class TestPartialReconciliationReport(unittest.TestCase):
|
||||
"""Issue #302: reports must explain why comments were or were not posted."""
|
||||
|
||||
def _plan(self, outcome):
|
||||
capabilities = {
|
||||
"FULL_RECONCILE_CLOSE_ALLOWED": {
|
||||
"close_pr": True, "comment_pr": True,
|
||||
"close_issue": True, "comment_issue": True,
|
||||
},
|
||||
"PARTIAL_RECONCILE_COMMENT_THEN_STOP": {
|
||||
"close_pr": False, "comment_pr": True,
|
||||
"close_issue": True, "comment_issue": True,
|
||||
},
|
||||
"RECOVERY_HANDOFF_ONLY": {
|
||||
"close_pr": False, "comment_pr": False,
|
||||
"close_issue": False, "comment_issue": False,
|
||||
},
|
||||
}[outcome]
|
||||
return resolve_partial_reconciliation_plan(
|
||||
ancestor_proven=True, capabilities=capabilities)
|
||||
|
||||
def test_partial_report_requires_comment_and_missing_capability(self):
|
||||
plan = self._plan("PARTIAL_RECONCILE_COMMENT_THEN_STOP")
|
||||
good = (
|
||||
"Reconciliation comment posted on PR #278 with ancestor proof. "
|
||||
"PR close skipped: close capability gitea.pr.close missing; "
|
||||
"stopped for authorized close."
|
||||
)
|
||||
result = assess_partial_reconciliation_report(good, plan=plan)
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
bad = "Stopped. Nothing done."
|
||||
result = assess_partial_reconciliation_report(bad, plan=plan)
|
||||
self.assertTrue(result["block"])
|
||||
|
||||
def test_handoff_only_report_requires_no_comment_explanation(self):
|
||||
plan = self._plan("RECOVERY_HANDOFF_ONLY")
|
||||
good = (
|
||||
"No reconciliation comment posted: comment capability missing. "
|
||||
"No Gitea mutation performed; recovery handoff produced."
|
||||
)
|
||||
result = assess_partial_reconciliation_report(good, plan=plan)
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
bad = "Recovery handoff produced."
|
||||
result = assess_partial_reconciliation_report(bad, plan=plan)
|
||||
self.assertTrue(result["block"])
|
||||
|
||||
def test_full_reconcile_report_requires_close_result(self):
|
||||
plan = self._plan("FULL_RECONCILE_CLOSE_ALLOWED")
|
||||
good = "PR close result: closed PR #278 after ancestor proof."
|
||||
result = assess_partial_reconciliation_report(good, plan=plan)
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
bad = "Reconciliation done."
|
||||
result = assess_partial_reconciliation_report(bad, plan=plan)
|
||||
self.assertTrue(result["block"])
|
||||
|
||||
|
||||
class TestAlreadyLandedReviewGate(unittest.TestCase):
|
||||
"""Issue #292: PR head already on target blocks approval and merge."""
|
||||
|
||||
def _gate(self, **overrides):
|
||||
kwargs = {
|
||||
"pr_number": 278,
|
||||
"candidate_head_sha": PINNED,
|
||||
"target_branch": "master",
|
||||
"target_branch_sha": OTHER,
|
||||
"head_is_ancestor_of_target": True,
|
||||
"live_head_sha": PINNED,
|
||||
}
|
||||
kwargs.update(overrides)
|
||||
return assess_already_landed_review_gate(**kwargs)
|
||||
|
||||
def test_already_landed_blocks_approval_and_merge(self):
|
||||
result = self._gate()
|
||||
self.assertEqual(result["state"], "ALREADY_LANDED_RECONCILE_REQUIRED")
|
||||
self.assertFalse(result["approve_allowed"])
|
||||
self.assertFalse(result["merge_api_allowed"])
|
||||
self.assertFalse(result["request_changes_allowed"])
|
||||
self.assertTrue(result["reconciliation_handoff_required"])
|
||||
|
||||
def test_already_landed_with_real_blocker_allows_request_changes(self):
|
||||
result = self._gate(real_blocker=True)
|
||||
self.assertEqual(result["state"], "ALREADY_LANDED_RECONCILE_REQUIRED")
|
||||
self.assertFalse(result["approve_allowed"])
|
||||
self.assertFalse(result["merge_api_allowed"])
|
||||
self.assertTrue(result["request_changes_allowed"])
|
||||
|
||||
def test_normal_candidate_passes_gate(self):
|
||||
result = self._gate(head_is_ancestor_of_target=False)
|
||||
self.assertEqual(result["state"], "NORMAL_REVIEW_CANDIDATE")
|
||||
self.assertTrue(result["approve_allowed"])
|
||||
self.assertTrue(result["merge_api_allowed"])
|
||||
self.assertFalse(result["reconciliation_handoff_required"])
|
||||
|
||||
def test_non_mergeable_normal_pr_still_passes_this_gate(self):
|
||||
# Mergeability/conflicts are separate gates; ancestry gate only
|
||||
# decides already-landed vs normal candidate.
|
||||
result = self._gate(
|
||||
head_is_ancestor_of_target=False, mergeable=False)
|
||||
self.assertEqual(result["state"], "NORMAL_REVIEW_CANDIDATE")
|
||||
self.assertTrue(result["request_changes_allowed"])
|
||||
|
||||
def test_unchecked_ancestry_blocks_review_mutations(self):
|
||||
result = self._gate(head_is_ancestor_of_target=None)
|
||||
self.assertEqual(result["state"], "GATE_NOT_PROVEN")
|
||||
self.assertFalse(result["approve_allowed"])
|
||||
self.assertFalse(result["merge_api_allowed"])
|
||||
|
||||
def test_changed_head_since_pin_blocks_until_recheck(self):
|
||||
result = self._gate(
|
||||
head_is_ancestor_of_target=False, live_head_sha=OTHER)
|
||||
self.assertEqual(result["state"], "GATE_NOT_PROVEN")
|
||||
self.assertFalse(result["approve_allowed"])
|
||||
self.assertTrue(any(
|
||||
"head" in reason.lower() for reason in result["reasons"]
|
||||
))
|
||||
|
||||
def test_invalid_shas_block_gate(self):
|
||||
result = self._gate(candidate_head_sha="deadbeef")
|
||||
self.assertEqual(result["state"], "GATE_NOT_PROVEN")
|
||||
self.assertFalse(result["approve_allowed"])
|
||||
|
||||
result = self._gate(target_branch_sha=None)
|
||||
self.assertEqual(result["state"], "GATE_NOT_PROVEN")
|
||||
|
||||
def test_already_landed_handoff_lists_required_fields(self):
|
||||
result = self._gate()
|
||||
for field in (
|
||||
"PR number/title",
|
||||
"candidate head SHA",
|
||||
"target branch",
|
||||
"target branch SHA",
|
||||
"ancestor proof",
|
||||
"linked issue status",
|
||||
"recommended reconciliation action",
|
||||
"capability proof for close/comment mutations",
|
||||
):
|
||||
self.assertIn(field, result["required_handoff_fields"])
|
||||
|
||||
|
||||
class TestAlreadyLandedReportState(unittest.TestCase):
|
||||
"""Issue #292: reports cannot say APPROVED after the gate fired."""
|
||||
|
||||
GOOD_REPORT = (
|
||||
"Eligibility class: ALREADY_LANDED_RECONCILE_REQUIRED\n"
|
||||
"Candidate head SHA: " + PINNED + "\n"
|
||||
"Review decision: none\n"
|
||||
"Merge result: none\n"
|
||||
)
|
||||
|
||||
def test_gate_fired_with_reconcile_state_passes(self):
|
||||
result = assess_already_landed_report_state(
|
||||
self.GOOD_REPORT, gate_fired=True)
|
||||
self.assertTrue(result["complete"])
|
||||
self.assertFalse(result["block"])
|
||||
|
||||
def test_gate_fired_with_approved_state_blocks(self):
|
||||
report = self.GOOD_REPORT.replace(
|
||||
"Review decision: none", "Review decision: APPROVED")
|
||||
result = assess_already_landed_report_state(report, gate_fired=True)
|
||||
self.assertTrue(result["block"])
|
||||
|
||||
def test_gate_fired_with_merged_state_blocks(self):
|
||||
report = self.GOOD_REPORT.replace(
|
||||
"Merge result: none", "Merge result: MERGED")
|
||||
result = assess_already_landed_report_state(report, gate_fired=True)
|
||||
self.assertTrue(result["block"])
|
||||
|
||||
def test_gate_fired_with_ready_to_merge_blocks(self):
|
||||
result = assess_already_landed_report_state(
|
||||
self.GOOD_REPORT + "Current status: READY_TO_MERGE\n",
|
||||
gate_fired=True)
|
||||
self.assertTrue(result["block"])
|
||||
|
||||
def test_gate_fired_without_reconcile_state_blocks(self):
|
||||
result = assess_already_landed_report_state(
|
||||
"Current status: review complete.\n", gate_fired=True)
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(any(
|
||||
"already_landed_reconcile_required" in reason.lower()
|
||||
for reason in result["reasons"]
|
||||
))
|
||||
|
||||
def test_gate_not_fired_reports_pass_untouched(self):
|
||||
result = assess_already_landed_report_state(
|
||||
"Review decision: APPROVED\nMerge result: MERGED\n",
|
||||
gate_fired=False)
|
||||
self.assertTrue(result["complete"])
|
||||
|
||||
|
||||
class TestFullSuiteFailureApprovalGate(unittest.TestCase):
|
||||
"""Issue #323: full-suite failure blocks approval without baseline proof."""
|
||||
|
||||
@@ -2707,3 +3070,6 @@ class TestFullSuiteFailureApprovalGate(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Tests for infra-stop repair handoff verifier (#289)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from capability_stop_terminal import TERMINAL_REPORT_HEADING
|
||||
from reviewer_infra_stop_handoff import assess_infra_stop_handoff_report # noqa: E402
|
||||
|
||||
|
||||
def _repair_handoff() -> str:
|
||||
return "\n".join([
|
||||
TERMINAL_REPORT_HEADING,
|
||||
"Repair handoff for infra_stop blocked reviewer workflow.",
|
||||
"CONTROL-CHECKOUT REPAIR MODE",
|
||||
"MCP process root: /Users/dev/Gitea-Tools",
|
||||
"Inspected git root: /Users/dev/Gitea-Tools",
|
||||
"Conflict marker path: gitea_mcp_server.py",
|
||||
"Merge/rebase control path: none",
|
||||
"Safe next repair action: resolve conflict markers and restart MCP",
|
||||
"infra_stop: true",
|
||||
])
|
||||
|
||||
|
||||
class TestInfraStopHandoff(unittest.TestCase):
|
||||
def test_repair_handoff_passes(self):
|
||||
result = assess_infra_stop_handoff_report(
|
||||
_repair_handoff(),
|
||||
stop_session={
|
||||
"infra_stop": True,
|
||||
"infra_stop_assessment": {
|
||||
"infra_stop": True,
|
||||
"conflict_file": "gitea_mcp_server.py",
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_next_pr_selection_blocks(self):
|
||||
report = "\n".join([
|
||||
TERMINAL_REPORT_HEADING,
|
||||
"Next eligible PR to review: PR #276",
|
||||
"Pinned review head SHA: abc123",
|
||||
])
|
||||
result = assess_infra_stop_handoff_report(
|
||||
report,
|
||||
stop_session={"infra_stop": True, "require_infra_diagnostics": False},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("queue" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_missing_repair_handoff_blocks(self):
|
||||
result = assess_infra_stop_handoff_report(
|
||||
"Validation result: pass\nReview summary for PR #276",
|
||||
stop_session={"infra_stop": True},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
|
||||
def test_main_checkout_diagnostics_without_label_blocks(self):
|
||||
report = "\n".join([
|
||||
TERMINAL_REPORT_HEADING,
|
||||
"Repair handoff",
|
||||
"Ran git status in main checkout for MCP diagnostics.",
|
||||
])
|
||||
result = assess_infra_stop_handoff_report(
|
||||
report,
|
||||
stop_session={
|
||||
"infra_stop": True,
|
||||
"main_checkout_diagnostics": True,
|
||||
"require_infra_diagnostics": False,
|
||||
},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("control-checkout" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_background_scheduling_blocks(self):
|
||||
report = "\n".join([
|
||||
TERMINAL_REPORT_HEADING,
|
||||
"Repair handoff",
|
||||
"Used schedule/manage_task while waiting for MCP recovery.",
|
||||
])
|
||||
result = assess_infra_stop_handoff_report(
|
||||
report,
|
||||
stop_session={"infra_stop": True, "require_infra_diagnostics": False},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("schedule" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_non_infra_stop_skips(self):
|
||||
result = assess_infra_stop_handoff_report(
|
||||
"Selected PR #1 for review",
|
||||
stop_session={"infra_stop": False},
|
||||
)
|
||||
self.assertTrue(result["proven"])
|
||||
|
||||
|
||||
class TestExport(unittest.TestCase):
|
||||
def test_review_proofs_reexport(self):
|
||||
from review_proofs import assess_infra_stop_handoff_report as exported
|
||||
|
||||
self.assertTrue(callable(exported))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Tests for reviewer inventory/worktree verifier (#293)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from reviewer_inventory_worktree import assess_inventory_worktree_report # noqa: E402
|
||||
|
||||
|
||||
def _good_handoff() -> str:
|
||||
return "\n".join([
|
||||
"## Controller Handoff",
|
||||
"- Selected PR: 280",
|
||||
"- Inventory pagination proof: is_final_page: true, has_more: false, total_count: 6",
|
||||
"- Review worktree used: true",
|
||||
"- Review worktree path: branches/review-pr280-feat-issue-275-claim",
|
||||
"- Review worktree inside branches: true",
|
||||
"- Review worktree HEAD state: detached",
|
||||
"- Main checkout branch: master",
|
||||
"- Current status: reviewed PR #280",
|
||||
])
|
||||
|
||||
|
||||
class TestInventoryPaginationProof(unittest.TestCase):
|
||||
def test_no_next_page_proof_passes(self):
|
||||
result = assess_inventory_worktree_report(_good_handoff())
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
self.assertTrue(result["pagination_proven"])
|
||||
|
||||
def test_rejects_partial_first_page_eligibility_claim(self):
|
||||
report = "\n".join([
|
||||
"Oldest eligible PR is #280.",
|
||||
"gitea_list_prs returned 10 open PRs on the first page.",
|
||||
"## Controller Handoff",
|
||||
"- Inventory pagination proof: first page only",
|
||||
])
|
||||
result = assess_inventory_worktree_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(any("eligible" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_rejects_default_page_size_assumption(self):
|
||||
report = (
|
||||
"Inventory exhaustive because fewer than the default Gitea page size "
|
||||
"were returned."
|
||||
)
|
||||
result = assess_inventory_worktree_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("page size" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_rejects_exactly_full_first_page_without_finality(self):
|
||||
report = "\n".join([
|
||||
"Next eligible PR: #290 based on complete inventory.",
|
||||
"gitea_list_prs returned 50 open PRs.",
|
||||
"## Controller Handoff",
|
||||
"- Inventory pagination proof: returned 50 open PRs",
|
||||
])
|
||||
result = assess_inventory_worktree_report(
|
||||
report,
|
||||
inventory_session={"requested_page_size": 50, "returned_page_size": 50},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("full first page" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_allows_exact_page_with_session_pagination_complete(self):
|
||||
report = "Oldest eligible PR: #12 after queue inventory."
|
||||
result = assess_inventory_worktree_report(
|
||||
report,
|
||||
inventory_session={"pagination_complete": True},
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
|
||||
class TestWorktreeConsistency(unittest.TestCase):
|
||||
def test_branches_worktree_requires_review_worktree_used_true(self):
|
||||
report = "\n".join([
|
||||
"## Controller Handoff",
|
||||
"- Inventory pagination proof: is_final_page: true",
|
||||
"- Review worktree used: false",
|
||||
"- Review worktree path: branches/review-pr280-feat",
|
||||
"- Scratch worktree used: false",
|
||||
])
|
||||
result = assess_inventory_worktree_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
joined = " ".join(result["reasons"]).lower()
|
||||
self.assertIn("review worktree used", joined)
|
||||
self.assertIn("scratch worktree", joined)
|
||||
|
||||
def test_rejects_contradictory_detached_and_branch_wording(self):
|
||||
report = "\n".join([
|
||||
"Checkout: Detached HEAD / Branch master",
|
||||
"## Controller Handoff",
|
||||
"- Inventory pagination proof: is_final_page: true",
|
||||
"- Review worktree used: true",
|
||||
"- Review worktree path: branches/review-pr280-feat",
|
||||
"- Review worktree HEAD state: detached",
|
||||
"- Main checkout branch: master",
|
||||
])
|
||||
result = assess_inventory_worktree_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(
|
||||
any("contradictory" in r.lower() for r in result["reasons"])
|
||||
)
|
||||
|
||||
def test_requires_head_state_when_review_worktree_used(self):
|
||||
report = "\n".join([
|
||||
"## Controller Handoff",
|
||||
"- Inventory pagination proof: pagination_complete: true",
|
||||
"- Review worktree used: true",
|
||||
"- Review worktree path: branches/review-pr280-feat",
|
||||
])
|
||||
result = assess_inventory_worktree_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("head state" in r.lower() for r in result["reasons"]))
|
||||
|
||||
|
||||
class TestExport(unittest.TestCase):
|
||||
def test_review_proofs_reexport(self):
|
||||
from review_proofs import assess_inventory_worktree_report as exported
|
||||
|
||||
self.assertTrue(callable(exported))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Tests for merge-simulation worktree mutation verifier (#317)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from reviewer_merge_simulation import ( # noqa: E402
|
||||
assess_merge_simulation_report,
|
||||
merge_simulation_commands,
|
||||
)
|
||||
|
||||
|
||||
def _good_simulation_report(**overrides) -> str:
|
||||
lines = [
|
||||
"Worktree/index mutations: merge simulation in branches/review-pr281",
|
||||
"Worktree path: branches/review-pr281",
|
||||
"Pre-simulation clean status: clean (git status --porcelain empty)",
|
||||
"Merge result: conflicts in review_proofs.py",
|
||||
"Abort command: git merge --abort",
|
||||
"Post-abort clean status: clean after abort (git status --porcelain empty)",
|
||||
]
|
||||
text = "\n".join(lines)
|
||||
for key, value in overrides.items():
|
||||
text = text.replace(f"{key}:", f"{key}: {value}")
|
||||
return text
|
||||
|
||||
|
||||
class TestMergeSimulationDetection(unittest.TestCase):
|
||||
def test_detects_no_commit_merge(self):
|
||||
commands = merge_simulation_commands([
|
||||
"git merge prgs/master --no-commit --no-ff",
|
||||
"git status",
|
||||
])
|
||||
self.assertEqual(commands, ["git merge prgs/master --no-commit --no-ff"])
|
||||
|
||||
def test_detects_merge_abort(self):
|
||||
commands = merge_simulation_commands([
|
||||
"git merge prgs/master --no-commit --no-ff",
|
||||
"git merge --abort",
|
||||
])
|
||||
self.assertIn("git merge --abort", commands)
|
||||
|
||||
|
||||
class TestMergeSimulationReport(unittest.TestCase):
|
||||
def test_no_simulation_passes(self):
|
||||
result = assess_merge_simulation_report(
|
||||
"Review complete. Worktree/index mutations: none",
|
||||
command_log=["git status --porcelain", "git diff prgs/master...HEAD"],
|
||||
)
|
||||
self.assertTrue(result["proven"])
|
||||
|
||||
def test_documented_simulation_passes(self):
|
||||
report = _good_simulation_report()
|
||||
result = assess_merge_simulation_report(
|
||||
report,
|
||||
command_log=[
|
||||
"git merge prgs/master --no-commit --no-ff",
|
||||
"git merge --abort",
|
||||
],
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_readonly_classification_blocks(self):
|
||||
report = "\n".join([
|
||||
"Read-only diagnostics: git merge prgs/master --no-commit --no-ff, git status",
|
||||
"Worktree/index mutations: none",
|
||||
])
|
||||
result = assess_merge_simulation_report(
|
||||
report,
|
||||
command_log=["git merge prgs/master --no-commit --no-ff"],
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(any("read-only" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_missing_worktree_field_blocks(self):
|
||||
report = "Merge simulation ran but only noted in prose."
|
||||
result = assess_merge_simulation_report(
|
||||
report,
|
||||
command_log=["git merge prgs/master --no-commit --no-ff"],
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("worktree/index mutations" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_conflict_simulation_requires_merge_result(self):
|
||||
report = _good_simulation_report().replace(
|
||||
"Merge result: conflicts in review_proofs.py",
|
||||
"",
|
||||
)
|
||||
result = assess_merge_simulation_report(
|
||||
report,
|
||||
command_log=["git merge prgs/master --no-commit --no-ff"],
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("merge result" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_aborted_simulation_requires_post_clean_proof(self):
|
||||
report = _good_simulation_report().replace(
|
||||
"Post-abort clean status: clean after abort (git status --porcelain empty)",
|
||||
"",
|
||||
)
|
||||
result = assess_merge_simulation_report(
|
||||
report,
|
||||
command_log=[
|
||||
"git merge prgs/master --no-commit --no-ff",
|
||||
"git merge --abort",
|
||||
],
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("post-abort" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_successful_simulation_without_abort_omits_abort_fields(self):
|
||||
report = "\n".join([
|
||||
"Worktree/index mutations: merge simulation completed cleanly",
|
||||
"Worktree path: branches/review-pr281",
|
||||
"Pre-simulation clean status: clean before simulation",
|
||||
"Merge result: clean fast-forward simulation",
|
||||
])
|
||||
result = assess_merge_simulation_report(
|
||||
report,
|
||||
command_log=["git merge prgs/master --no-commit --no-ff"],
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
|
||||
class TestExport(unittest.TestCase):
|
||||
def test_review_proofs_reexport(self):
|
||||
from review_proofs import assess_merge_simulation_report as exported
|
||||
|
||||
self.assertTrue(callable(exported))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Tests for precise mutation category verifier (#319)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from reviewer_mutation_categories import assess_mutation_categories_report # noqa: E402
|
||||
|
||||
|
||||
def _precise_handoff() -> str:
|
||||
return "\n".join([
|
||||
"Controller Handoff",
|
||||
"File edits by reviewer: none",
|
||||
"Worktree/index mutations: git worktree add branches/review-pr1",
|
||||
"Git ref mutations: git fetch prgs master",
|
||||
"MCP/Gitea mutations: gitea_view_pr",
|
||||
"Review mutations: gitea_review_pr request_changes",
|
||||
"Read-only diagnostics: git status, git diff",
|
||||
])
|
||||
|
||||
|
||||
class TestMutationCategories(unittest.TestCase):
|
||||
def test_precise_categories_pass(self):
|
||||
result = assess_mutation_categories_report(_precise_handoff())
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_legacy_workspace_mutations_blocks(self):
|
||||
report = "\n".join([
|
||||
"Controller Handoff",
|
||||
"Workspace mutations: none (no local file changes)",
|
||||
"File edits by reviewer: none",
|
||||
"Worktree/index mutations: none",
|
||||
"Git ref mutations: none",
|
||||
])
|
||||
result = assess_mutation_categories_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("workspace mutations" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_workspace_none_with_worktree_command_blocks(self):
|
||||
report = "\n".join([
|
||||
"Controller Handoff",
|
||||
"Workspace mutations: none (no local file changes)",
|
||||
"File edits by reviewer: none",
|
||||
"Worktree/index mutations: none",
|
||||
"Git ref mutations: none",
|
||||
])
|
||||
result = assess_mutation_categories_report(
|
||||
report,
|
||||
observed_commands=["git reset --hard FETCH_HEAD"],
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
|
||||
def test_file_edits_none_with_worktree_mutation_passes(self):
|
||||
report = "\n".join([
|
||||
"Controller Handoff",
|
||||
"File edits by reviewer: none",
|
||||
"Worktree/index mutations: git reset --hard FETCH_HEAD",
|
||||
"Git ref mutations: git fetch prgs",
|
||||
])
|
||||
result = assess_mutation_categories_report(
|
||||
report,
|
||||
observed_commands=["git reset --hard FETCH_HEAD", "git fetch prgs"],
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_missing_required_categories_blocks(self):
|
||||
report = "Controller Handoff\nFile edits by reviewer: none\n"
|
||||
result = assess_mutation_categories_report(report)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("missing" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_non_controller_handoff_skips(self):
|
||||
result = assess_mutation_categories_report(
|
||||
"Workspace mutations: none\n",
|
||||
)
|
||||
self.assertTrue(result["proven"])
|
||||
|
||||
|
||||
class TestExport(unittest.TestCase):
|
||||
def test_review_proofs_reexport(self):
|
||||
from review_proofs import assess_mutation_categories_report as exported
|
||||
|
||||
self.assertTrue(callable(exported))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Tests for PR-head vs diagnostic validation integrity verifier (#316)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from reviewer_validation_integrity import assess_validation_integrity_report # noqa: E402
|
||||
|
||||
|
||||
def _official_only_report() -> str:
|
||||
return "\n".join([
|
||||
"Official PR-head validation on unmodified worktree.",
|
||||
"Official validation command: pytest tests/ -q",
|
||||
"Official validation result: failed (3 failed)",
|
||||
"Validation integrity status: failed",
|
||||
"Worktree dirty after official validation: clean",
|
||||
"Review decision: request_changes",
|
||||
])
|
||||
|
||||
|
||||
def _diagnostic_report() -> str:
|
||||
return "\n".join([
|
||||
_official_only_report(),
|
||||
"Diagnostic local experiment — not PR-head validation",
|
||||
"Diagnostic edit path: tests/test_example.py",
|
||||
"File edits by reviewer: tests/test_example.py",
|
||||
"Diagnostic command: pytest tests/test_example.py -q",
|
||||
"Diagnostic result: passed after local fix",
|
||||
"Diagnostic results used only for suggested fix; blocker remains PR-head failure.",
|
||||
])
|
||||
|
||||
|
||||
class TestValidationIntegrity(unittest.TestCase):
|
||||
def test_official_only_passes(self):
|
||||
result = assess_validation_integrity_report(
|
||||
_official_only_report(),
|
||||
validation_session={
|
||||
"official_validation_ran": True,
|
||||
"official_result": "fail",
|
||||
"worktree_dirty_after_official": False,
|
||||
},
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_failing_pr_head_with_diagnostic_pass_passes(self):
|
||||
result = assess_validation_integrity_report(
|
||||
_diagnostic_report(),
|
||||
validation_session={
|
||||
"official_validation_ran": True,
|
||||
"official_result": "fail",
|
||||
"diagnostic_validation_ran": True,
|
||||
"diagnostic_result": "pass",
|
||||
"diagnostic_edits": ["tests/test_example.py"],
|
||||
"worktree_dirty_after_official": False,
|
||||
},
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_post_edit_tests_as_official_blocks(self):
|
||||
report = "\n".join([
|
||||
"Official validation result: passed after diagnostic edit",
|
||||
"pytest passed after local fix in validation worktree",
|
||||
])
|
||||
result = assess_validation_integrity_report(
|
||||
report,
|
||||
validation_session={
|
||||
"official_validation_ran": True,
|
||||
"diagnostic_edits": ["tests/test_example.py"],
|
||||
"diagnostic_validation_ran": True,
|
||||
},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(result["block"])
|
||||
|
||||
def test_missing_dirty_after_validation_blocks(self):
|
||||
report = _official_only_report().replace(
|
||||
"Worktree dirty after official validation: clean",
|
||||
"",
|
||||
)
|
||||
result = assess_validation_integrity_report(
|
||||
report,
|
||||
validation_session={"official_validation_ran": True, "official_result": "fail"},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("dirty" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_diagnostic_without_label_blocks(self):
|
||||
report = _diagnostic_report().replace(
|
||||
"Diagnostic local experiment — not PR-head validation",
|
||||
"",
|
||||
)
|
||||
result = assess_validation_integrity_report(
|
||||
report,
|
||||
validation_session={
|
||||
"official_validation_ran": True,
|
||||
"diagnostic_edits": ["tests/test_example.py"],
|
||||
"diagnostic_validation_ran": True,
|
||||
},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("diagnostic" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_contaminated_requires_integrity_status(self):
|
||||
report = "Validation worktree was edited before official validation completed."
|
||||
result = assess_validation_integrity_report(
|
||||
report,
|
||||
validation_session={"contaminated": True, "official_validation_ran": True},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("contaminated" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_action_log_edits_trigger_diagnostic_rules(self):
|
||||
result = assess_validation_integrity_report(
|
||||
_official_only_report(),
|
||||
validation_session={"official_validation_ran": True, "official_result": "fail"},
|
||||
action_log=[{"path": "tests/test_example.py", "kind": "file_edit", "performed": True}],
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
|
||||
|
||||
class TestExport(unittest.TestCase):
|
||||
def test_review_proofs_reexport(self):
|
||||
from review_proofs import assess_validation_integrity_report as exported
|
||||
|
||||
self.assertTrue(callable(exported))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Tests for PR validation worktree no-edit verifier (#315)."""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from reviewer_validation_worktree_edits import ( # noqa: E402
|
||||
assess_validation_worktree_edit_report,
|
||||
)
|
||||
|
||||
|
||||
def _read_only_report() -> str:
|
||||
return "\n".join([
|
||||
"Validation worktree path: branches/review-pr280-feat-issue-275-claim",
|
||||
"Official PR-head validation on unmodified worktree.",
|
||||
"Official validation result: failed (1 failed)",
|
||||
"File edits by reviewer: none",
|
||||
"Review decision: request_changes",
|
||||
])
|
||||
|
||||
|
||||
def _diagnostic_report() -> str:
|
||||
return "\n".join([
|
||||
"Validation worktree path: branches/review-pr280-feat-issue-275-claim",
|
||||
"Official PR-head validation on unmodified worktree.",
|
||||
"Official validation result: failed (1 failed)",
|
||||
"Review decision: request_changes",
|
||||
"Diagnostic local experiment — not PR-head validation",
|
||||
"Diagnostic scratch worktree path: branches/diagnostic-pr280-fix-test",
|
||||
"File edits by reviewer: tests/test_agent_temp_artifacts.py (diagnostic only)",
|
||||
"Diagnostic result: passed after local fix",
|
||||
])
|
||||
|
||||
|
||||
class TestValidationWorktreeEdits(unittest.TestCase):
|
||||
def test_read_only_review_passes(self):
|
||||
result = assess_validation_worktree_edit_report(
|
||||
_read_only_report(),
|
||||
validation_session={
|
||||
"validation_worktree_path": (
|
||||
"branches/review-pr280-feat-issue-275-claim"
|
||||
),
|
||||
},
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_validation_worktree_edit_blocks(self):
|
||||
result = assess_validation_worktree_edit_report(
|
||||
_read_only_report(),
|
||||
validation_session={
|
||||
"validation_worktree_path": (
|
||||
"branches/review-pr280-feat-issue-275-claim"
|
||||
),
|
||||
"validation_worktree_edits": ["tests/test_agent_temp_artifacts.py"],
|
||||
},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(result["block"])
|
||||
self.assertTrue(any("validation worktree" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_file_edits_none_contradiction_blocks(self):
|
||||
result = assess_validation_worktree_edit_report(
|
||||
_read_only_report(),
|
||||
validation_session={
|
||||
"validation_worktree_path": (
|
||||
"branches/review-pr280-feat-issue-275-claim"
|
||||
),
|
||||
},
|
||||
action_log=[
|
||||
{
|
||||
"path": "tests/test_agent_temp_artifacts.py",
|
||||
"kind": "file_edit",
|
||||
"performed": True,
|
||||
"worktree_path": "branches/review-pr280-feat-issue-275-claim",
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("file edits" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_diagnostic_scratch_with_reporting_passes(self):
|
||||
result = assess_validation_worktree_edit_report(
|
||||
_diagnostic_report(),
|
||||
validation_session={
|
||||
"validation_worktree_path": (
|
||||
"branches/review-pr280-feat-issue-275-claim"
|
||||
),
|
||||
"diagnostic_worktree_path": "branches/diagnostic-pr280-fix-test",
|
||||
"diagnostic_edits": ["tests/test_agent_temp_artifacts.py"],
|
||||
"diagnostic_experiment": True,
|
||||
},
|
||||
)
|
||||
self.assertTrue(result["proven"], result["reasons"])
|
||||
|
||||
def test_diagnostic_without_scratch_worktree_blocks(self):
|
||||
result = assess_validation_worktree_edit_report(
|
||||
"File edits by reviewer: tests/test_example.py",
|
||||
validation_session={
|
||||
"diagnostic_edits": ["tests/test_example.py"],
|
||||
"diagnostic_experiment": True,
|
||||
},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
self.assertTrue(any("diagnostic" in r.lower() for r in result["reasons"]))
|
||||
|
||||
def test_post_edit_official_validation_blocks(self):
|
||||
report = "\n".join([
|
||||
"Validation worktree path: branches/review-pr280-feat-issue-275-claim",
|
||||
"Official validation result: passed after local edit",
|
||||
"File edits by reviewer: tests/test_agent_temp_artifacts.py",
|
||||
])
|
||||
result = assess_validation_worktree_edit_report(
|
||||
report,
|
||||
validation_session={
|
||||
"validation_worktree_path": (
|
||||
"branches/review-pr280-feat-issue-275-claim"
|
||||
),
|
||||
"validation_worktree_edits": ["tests/test_agent_temp_artifacts.py"],
|
||||
"official_validation_after_edit": True,
|
||||
},
|
||||
)
|
||||
self.assertFalse(result["proven"])
|
||||
|
||||
|
||||
class TestExport(unittest.TestCase):
|
||||
def test_review_proofs_reexport(self):
|
||||
from review_proofs import assess_validation_worktree_edit_report as exported
|
||||
|
||||
self.assertTrue(callable(exported))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user