fix(rework): require verified completion comment receipt

This commit is contained in:
2026-10-07 10:14:47 +00:00
parent 470c7fb09b
commit e7bcfef532
3 changed files with 222 additions and 5 deletions
+70
View File
@@ -0,0 +1,70 @@
import hashlib, json, subprocess, sys, tempfile, unittest
from pathlib import Path
SCRIPT = Path(__file__).parents[2] / "agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py"
class ReworkCommentTests(unittest.TestCase):
def setUp(self):
self.t = tempfile.TemporaryDirectory(); self.d = Path(self.t.name)
self.data = {"owner":"acme","repository":"widget","issue":7,"pr":12,"review":99,
"reviewed_head":"a"*40,"final_head":"b"*40,"contract_sha256":"c"*64,
"result":"partial","findings":[{"id":"F-1","status":"resolved","classification":"valid_fix","changes":"src/main.py","verification":"pytest -q (0), pass","reason":"bounded fix"}],
"local_checks":"pytest -q (0), pass","runtime_browser":"해당 없음·미검증","commit_status":"success","actions_runs":"없음"}
self.write("input.json", self.data)
self.invoke("render", "--input", self.p("input.json"), "--comment", self.p("comment.md"), "--expected", self.p("expected.json"))
self.expected = json.loads(self.d.joinpath("expected.json").read_text())
def tearDown(self): self.t.cleanup()
def p(self,n): return str(self.d/n)
def write(self,n,x): self.d.joinpath(n).write_text(json.dumps(x,ensure_ascii=False) if not isinstance(x,str) else x)
def invoke(self,*args): return subprocess.run([sys.executable,str(SCRIPT),*args],text=True,capture_output=True)
def read(self,n): return json.loads(self.d.joinpath(n).read_text())
def readback(self, comments=None, head=None):
self.write("pr.json", {"object_kind":"pull_request","owner":"acme","repository":"widget","number":12,"state":"open","head":{"sha":head or self.data["final_head"]},"html_url":"https://gitea/acme/widget/pulls/12","metadata_binding_verified":True})
self.write("identity.json", {"id":42,"login":"bot","role":"gitea-impl-agent","authenticated":True,"stable_account_verified":True})
items=comments if comments is not None else [{"id":501,"html_url":"https://gitea/x/501","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z","body":self.expected["body"],"user":{"id":42,"login":"bot"}}]
self.write("comments.json", {"object_kind":"pull_request_top_level_comments","owner":"acme","repository":"widget","pr":12,"complete":True,"endpoint_binding_verified":True,"source_issue_unchanged":True,"review_collection_unchanged":True,"pr_metadata_unchanged":True,"page_count":1,"total_count":len(items),"provider_counts":{"top_level":len(items),"reviews":2,"inline":3,"aggregate_comments":len(items),"aggregate_components":["top_level"]},"comments":items})
def verify(self, action="create", baseline=0):
return self.invoke("verify","--expected",self.p("expected.json"),"--pr-readback",self.p("pr.json"),"--comments-readback",self.p("comments.json"),"--identity-readback",self.p("identity.json"),"--action",action,"--baseline-count",str(baseline),"--output",self.p("receipt.json"))
def test_render_digest_and_correlation(self):
self.assertEqual(hashlib.sha256(self.expected["marker_free_body"].encode()).hexdigest(), self.expected["result_sha256"])
parts=["acme","widget","7","12","99","a"*40,"b"*40,"c"*64]
self.assertEqual(hashlib.sha256("\0".join(parts).encode()).hexdigest(), self.expected["correlation"])
self.assertTrue(self.d.joinpath("comment.md").read_text().endswith("-->\n"))
def test_no_comment_and_marker_body_mismatch(self):
self.readback([]); self.assertNotEqual(self.verify().returncode,0)
self.readback([{"id":1,"body":self.expected["body"].replace("correlation=","correlation=x"),"user":{"id":42,"login":"bot"}}]); self.assertNotEqual(self.verify().returncode,0)
def test_wrong_head_and_author(self):
self.readback(head="d"*40); self.assertNotEqual(self.verify().returncode,0)
self.readback(); c=self.read("comments.json"); c["comments"][0]["user"]={"id":43,"login":"bot"}; self.write("comments.json",c); self.assertNotEqual(self.verify().returncode,0)
def test_digest_and_correlation_tampering_duplicate(self):
self.expected["result_sha256"]="0"*64; self.write("expected.json",self.expected); self.readback(); self.assertNotEqual(self.verify().returncode,0)
self.setUp_render_again(); self.expected["correlation"]="0"*64; self.write("expected.json",self.expected); self.readback(); self.assertNotEqual(self.verify().returncode,0)
self.setUp_render_again(); self.readback(); c=self.read("comments.json"); c["comments"].append(dict(c["comments"][0],id=502)); c["total_count"]=2; self.write("comments.json",c); self.assertNotEqual(self.verify("reuse",1).returncode,0)
def setUp_render_again(self):
self.invoke("render","--input",self.p("input.json"),"--comment",self.p("comment.md"),"--expected",self.p("expected.json")); self.expected=self.read("expected.json")
def test_create_reuse_cardinality(self):
self.readback(); self.assertEqual(self.verify("create",0).returncode,0); self.assertEqual(self.read("receipt.json")["status"],"verified")
original=self.read("comments.json")["comments"][0]
self.readback([original,{"id":2,"html_url":"u","created_at":"t","updated_at":"t","body":"other","user":{"id":42,"login":"bot"}}]); self.assertEqual(self.verify("reuse",2).returncode,0)
def test_exact_success_and_receipt_preserved(self):
self.readback(); self.assertEqual(self.verify().returncode,0)
old=self.d.joinpath("receipt.json").read_bytes(); self.readback(head="e"*40); self.assertNotEqual(self.verify().returncode,0); self.assertEqual(old,self.d.joinpath("receipt.json").read_bytes())
def test_unresolved_delivery_and_incomplete_or_wrong_scope_readback_fail(self):
bad=dict(self.data); bad["result"]="rework_delivered"; bad["findings"]=[dict(self.data["findings"][0],status="unresolved")]
self.write("bad-input.json",bad)
self.assertNotEqual(self.invoke("render","--input",self.p("bad-input.json"),"--comment",self.p("bad.md"),"--expected",self.p("bad.json")).returncode,0)
self.readback(); comments=self.read("comments.json"); comments["complete"]=False; self.write("comments.json",comments); self.assertNotEqual(self.verify().returncode,0)
self.readback(); pr=self.read("pr.json"); pr["repository"]="other"; self.write("pr.json",pr); self.assertNotEqual(self.verify().returncode,0)
def test_impl_role_and_provider_aggregate_binding_are_required(self):
self.readback(); identity=self.read("identity.json"); identity["role"]="other"; self.write("identity.json",identity); self.assertNotEqual(self.verify().returncode,0)
self.readback(); comments=self.read("comments.json"); comments["provider_counts"]["aggregate_comments"]+=1; self.write("comments.json",comments); self.assertNotEqual(self.verify().returncode,0)
if __name__ == "__main__": unittest.main()