feat(reports): classify git fetch as a git ref mutation (closes #297)
Reviewer reports treated 'git fetch prgs master' as read-only diagnostics while claiming 'Git/worktree mutations: None'. Fetch edits no files but updates remote-tracking refs, so hiding it under diagnostics understates what the run mutated. Add git_ref_mutating_commands() classifying fetch, pull, push, merge, update-ref, remote update, branch -d/-D, and reset --hard as git ref mutations, and assess_git_ref_mutation_report() which fails a report when ref-updating commands ran but the report lacks a non-none 'Git ref mutations' entry, omits 'fetched <remote>/<branch>' for a targeted fetch, still claims 'Git/worktree mutations: None', or lists the command under read-only diagnostics. Tests cover fetch-only review, missing ledger entry, missing fetch target, contradictory none-claim, read-only misclassification, worktree creation (not a ref mutation), merge, cleanup branch deletion, no-mutation blocked handoff, and dict command-log entries. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Git ref mutations must be reported, never hidden as diagnostics (#297).
|
||||
|
||||
``git fetch`` updates remote-tracking refs even though it edits no files.
|
||||
Reports that ran fetch (or any ref-updating command) must carry a
|
||||
``Git ref mutations`` entry and may not claim ``Git/worktree mutations:
|
||||
None`` or classify the fetch as read-only diagnostics.
|
||||
"""
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
|
||||
|
||||
import review_proofs
|
||||
|
||||
|
||||
def _assess(report, commands):
|
||||
return review_proofs.assess_git_ref_mutation_report(
|
||||
report, command_log=commands)
|
||||
|
||||
|
||||
class TestGitRefMutationDetection(unittest.TestCase):
|
||||
def test_fetch_only_review_with_proper_ledger_passes(self):
|
||||
report = "\n".join([
|
||||
"Git ref mutations: fetched prgs/master",
|
||||
"Review decision: APPROVE",
|
||||
])
|
||||
res = _assess(report, ["git fetch prgs master"])
|
||||
self.assertTrue(res["proven"], res["reasons"])
|
||||
self.assertEqual(res["ref_mutating_commands"], ["git fetch prgs master"])
|
||||
|
||||
def test_fetch_without_ref_mutation_line_is_blocked(self):
|
||||
report = "Review decision: APPROVE\nMutations: None\n"
|
||||
res = _assess(report, ["git fetch prgs master"])
|
||||
self.assertFalse(res["proven"])
|
||||
self.assertTrue(
|
||||
any("git ref mutations" in r.lower() for r in res["reasons"]))
|
||||
|
||||
def test_fetch_with_remote_branch_requires_fetched_target_in_report(self):
|
||||
report = "Git ref mutations: some refs updated\n"
|
||||
res = _assess(report, ["git fetch prgs master"])
|
||||
self.assertFalse(res["proven"])
|
||||
self.assertTrue(
|
||||
any("prgs/master" in r for r in res["reasons"]))
|
||||
|
||||
def test_git_worktree_mutations_none_rejected_when_fetch_occurred(self):
|
||||
report = "\n".join([
|
||||
"Git ref mutations: fetched prgs/master",
|
||||
"Git/worktree mutations: None",
|
||||
])
|
||||
res = _assess(report, ["git fetch prgs master"])
|
||||
self.assertFalse(res["proven"])
|
||||
self.assertTrue(
|
||||
any("git/worktree mutations" in r.lower() for r in res["reasons"]))
|
||||
|
||||
def test_fetch_classified_as_read_only_diagnostics_rejected(self):
|
||||
report = "\n".join([
|
||||
"Git ref mutations: fetched prgs/master",
|
||||
"Read-only diagnostics: git fetch prgs master, git status",
|
||||
])
|
||||
res = _assess(report, ["git fetch prgs master"])
|
||||
self.assertFalse(res["proven"])
|
||||
self.assertTrue(
|
||||
any("read-only" in r.lower() for r in res["reasons"]))
|
||||
|
||||
def test_worktree_creation_is_not_a_ref_mutation(self):
|
||||
report = "Worktree mutations: created branches/review-pr9\n"
|
||||
res = _assess(report, ["git worktree add branches/review-pr9 prgs/master"])
|
||||
self.assertTrue(res["proven"], res["reasons"])
|
||||
self.assertEqual(res["ref_mutating_commands"], [])
|
||||
|
||||
def test_merge_command_counts_as_ref_mutation(self):
|
||||
report = "Review decision: APPROVE\n"
|
||||
res = _assess(report, ["git merge --no-ff feat/x"])
|
||||
self.assertFalse(res["proven"])
|
||||
self.assertIn("git merge --no-ff feat/x", res["ref_mutating_commands"])
|
||||
|
||||
def test_cleanup_branch_delete_counts_as_ref_mutation(self):
|
||||
report = "Cleanup mutations: deleted branch feat/x\n"
|
||||
res = _assess(report, ["git branch -d feat/x"])
|
||||
self.assertFalse(res["proven"])
|
||||
self.assertIn("git branch -d feat/x", res["ref_mutating_commands"])
|
||||
|
||||
def test_no_mutation_blocked_handoff_passes(self):
|
||||
report = "\n".join([
|
||||
"Git/worktree mutations: None",
|
||||
"Current status: blocked handoff, no mutations performed",
|
||||
])
|
||||
res = _assess(report, ["git status --porcelain", "git log --oneline -1"])
|
||||
self.assertTrue(res["proven"], res["reasons"])
|
||||
self.assertEqual(res["ref_mutating_commands"], [])
|
||||
|
||||
def test_command_log_dict_entries_supported(self):
|
||||
report = "Git ref mutations: fetched prgs/master\n"
|
||||
res = _assess(report, [{"command": "git fetch prgs master"}])
|
||||
self.assertTrue(res["proven"], res["reasons"])
|
||||
self.assertEqual(
|
||||
res["ref_mutating_commands"], ["git fetch prgs master"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user