184 lines
15 KiB
Python
184 lines
15 KiB
Python
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", "--no-delegated-evidence", "--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","--no-delegated-evidence","--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","--no-delegated-evidence","--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)
|
|
|
|
def test_render_visible_identifier_lines_do_not_emit_issue_references(self):
|
|
body=self.expected["marker_free_body"]
|
|
self.assertIn("- Source Issue: 7",body)
|
|
self.assertIn("- Pull Request: 12",body)
|
|
self.assertIn("- Source Review: 99",body)
|
|
self.assertNotIn("- Source Issue: #7",body)
|
|
self.assertNotIn("- Pull Request: #12",body)
|
|
self.assertNotIn("- Source Review: #99",body)
|
|
|
|
def test_render_rejects_reference_hazards_in_every_safe_human_field(self):
|
|
hazards=("#123", "otherrepo#123", r"\\#123", "\\#123", "review #99")
|
|
fields=("changes","verification","reason","local_checks","runtime_browser")
|
|
# Exercise finding fields and global fields independently; references
|
|
# must not be emitted even when they resemble another repo or are escaped.
|
|
for field in fields:
|
|
for snippet in hazards:
|
|
with self.subTest(field=field,snippet=snippet):
|
|
data=json.loads(json.dumps(self.data))
|
|
if field in ("changes","verification","reason"):
|
|
data["findings"][0][field]="unsafe payload "+snippet
|
|
else:
|
|
data[field]="unsafe payload "+snippet
|
|
self.write("hazard.json",data)
|
|
result=self.invoke("render","--no-delegated-evidence","--input",self.p("hazard.json"),"--comment",self.p("hazard.md"),"--expected",self.p("hazard-expected.json"))
|
|
self.assertNotEqual(result.returncode,0)
|
|
|
|
def test_offline_verifier_rejects_reference_in_expected_body(self):
|
|
self.expected["marker_free_body"] += "\n- unsafe: otherrepo#123"
|
|
before_hash=self.expected["result_sha256"]
|
|
self.expected["result_sha256"]=hashlib.sha256(self.expected["marker_free_body"].encode()).hexdigest()
|
|
self.expected["marker"]=self.expected["marker"].replace(before_hash,self.expected["result_sha256"])
|
|
self.expected["body"]=self.expected["marker_free_body"]+self.expected["marker"]
|
|
self.write("expected.json",self.expected); self.readback()
|
|
self.assertNotEqual(self.verify().returncode,0)
|
|
|
|
def _reseal(self, value, body):
|
|
value=json.loads(json.dumps(value)); old=value["result_sha256"]
|
|
value["marker_free_body"]=body
|
|
value["result_sha256"]=hashlib.sha256(body.encode()).hexdigest()
|
|
value["marker"]=value["marker"].replace(old,value["result_sha256"])
|
|
value["body"]=body+value["marker"]
|
|
return value
|
|
|
|
def _legacy_expected(self):
|
|
old=json.loads(json.dumps(self.expected))
|
|
body=old["marker_free_body"]
|
|
body=body.replace("- Source Issue: #7","- Source Issue: 7").replace("- Pull Request: #12","- Pull Request: 12").replace("- Source Review: #99","- Source Review: 99")
|
|
# Preserve an explicitly legacy migration fixture; no other old text differs.
|
|
body=body.replace("- Source Issue: 7","- Source Issue: #7").replace("- Pull Request: 12","- Pull Request: #12").replace("- Source Review: 99","- Source Review: #99")
|
|
old["marker_free_body"]=body
|
|
old["result_sha256"]=hashlib.sha256(body.encode()).hexdigest()
|
|
old["marker"]=old["marker"].replace(self.expected["result_sha256"],old["result_sha256"])
|
|
old["body"]=body+old["marker"]
|
|
return old
|
|
|
|
def test_prepare_correction_returns_bounded_verified_legacy_migration_plan(self):
|
|
import importlib.util
|
|
spec=importlib.util.spec_from_file_location("rework_comment_correction_test",SCRIPT)
|
|
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
self.assertTrue(callable(getattr(module,"prepare_correction",None)), "approved correction API missing")
|
|
plan=module.prepare_correction(self._legacy_expected(),self.expected,comment_id=501,actor_id=42)
|
|
self.assertEqual(plan["schema"],"gitea-rework-correction-plan/v1")
|
|
self.assertEqual(plan["target"],self.expected["target"])
|
|
self.assertEqual(plan["comment_id"],501); self.assertEqual(plan["actor_id"],42)
|
|
self.assertEqual(plan["old_body"],self._legacy_expected()["body"])
|
|
self.assertEqual(plan["new_body"],self.expected["body"])
|
|
self.assertEqual(plan["old_body_sha256"],hashlib.sha256(plan["old_body"].encode()).hexdigest())
|
|
self.assertEqual(plan["new_body_sha256"],hashlib.sha256(plan["new_body"].encode()).hexdigest())
|
|
self.assertIs(plan["prior_handoff_verified"],False)
|
|
self.assertTrue(plan["immutable_source_required"])
|
|
|
|
def test_prepare_correction_rejects_tampered_or_unbounded_changes(self):
|
|
import importlib.util
|
|
spec=importlib.util.spec_from_file_location("rework_comment_correction_reject_test",SCRIPT)
|
|
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
|
|
self.assertTrue(callable(getattr(module,"prepare_correction",None)), "approved correction API missing")
|
|
old=self._legacy_expected()
|
|
cases=[]
|
|
bad=json.loads(json.dumps(old)); bad["result_sha256"]="0"*64; cases.append((bad,self.expected))
|
|
bad=json.loads(json.dumps(old)); bad["body"]=bad["body"].replace("#7","#8",1); cases.append((bad,self.expected))
|
|
bad=json.loads(json.dumps(old)); bad["marker_free_body"]=bad["marker_free_body"].replace("bounded fix","unsafe old text #123"); cases.append((bad,self.expected))
|
|
bad=json.loads(json.dumps(self.expected)); bad["marker_free_body"]=bad["marker_free_body"].replace("bounded fix","different extra text"); cases.append((old,bad))
|
|
bad=self._reseal(self.expected,self.expected["marker_free_body"].replace("bounded fix","different extra text")); cases.append((old,bad))
|
|
unsafe_old=self._reseal(old,old["marker_free_body"].replace("bounded fix","same unsafe #123"))
|
|
unsafe_new=self._reseal(self.expected,self.expected["marker_free_body"].replace("bounded fix","same unsafe #123")); cases.append((unsafe_old,unsafe_new))
|
|
bad=json.loads(json.dumps(self.expected)); bad["head"]="d"*40; cases.append((old,bad))
|
|
bad=json.loads(json.dumps(self.expected)); bad["correlation"]="e"*64; cases.append((old,bad))
|
|
for previous,new in cases:
|
|
with self.subTest(previous=previous["result_sha256"],new=new["correlation"]):
|
|
with self.assertRaises(module.Bad): module.prepare_correction(previous,new,comment_id=501,actor_id=42)
|
|
|
|
def test_prepare_correction_cli_emits_plan_not_approval_or_receipt(self):
|
|
self.write("previous.json",self._legacy_expected())
|
|
r=self.invoke("prepare-correction","--previous-expected",self.p("previous.json"),"--expected",self.p("expected.json"),"--comment-id","501","--actor-id","42","--output",self.p("plan.json"))
|
|
self.assertEqual(r.returncode,0,r.stderr)
|
|
plan=self.read("plan.json")
|
|
self.assertEqual(plan["schema"],"gitea-rework-correction-plan/v1")
|
|
self.assertIs(plan["approval_required"],True)
|
|
self.assertIs(plan["prior_handoff_verified"],False)
|
|
self.assertNotEqual(plan.get("status"),"verified")
|
|
self.assertEqual(self.d.joinpath("plan.json").stat().st_mode & 0o777,0o600)
|
|
|
|
def test_prepare_correction_cli_refuses_input_alias_and_preserves_file(self):
|
|
self.write("previous.json",self._legacy_expected()); before=self.d.joinpath("expected.json").read_bytes()
|
|
r=self.invoke("prepare-correction","--previous-expected",self.p("previous.json"),"--expected",self.p("expected.json"),"--comment-id","501","--actor-id","42","--output",self.p("expected.json"))
|
|
self.assertNotEqual(r.returncode,0)
|
|
self.assertEqual(self.d.joinpath("expected.json").read_bytes(),before)
|
|
|
|
def test_different_body_same_correlation_is_not_default_reuse(self):
|
|
old=self._legacy_expected()
|
|
self.readback([{"id":501,"html_url":"https://gitea/x/501","created_at":"2026-01-01T00:00:00Z","updated_at":"2026-01-01T00:00:00Z","body":old["body"],"user":{"id":42,"login":"bot"}}])
|
|
self.assertNotEqual(self.verify("reuse",1).returncode,0)
|
|
|
|
if __name__ == "__main__": unittest.main()
|