Not executable under the toolset a webhook run actually got: Hermes defaults those to web_search/web_extract/vision_analyze/clarify, with no shell. Worth revisiting now that the routes grant `terminal` explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa
121 lines
5.7 KiB
Python
121 lines
5.7 KiB
Python
"""Contract test for gitea-pr-review-filter.py.
|
|
|
|
Same discipline as gitea-pr-comment-filter-test.py: Hermes treats
|
|
"[SILENT]"/empty/nonzero-exit as ignore, a JSON object as a payload
|
|
replacement, and ANY OTHER stdout text as allow-with-script_output, so every
|
|
case asserts on the exact stdout, not just on the decision.
|
|
"""
|
|
import json, subprocess, sys, pathlib
|
|
|
|
SCRIPT = str(pathlib.Path(__file__).with_name("gitea-pr-review-filter.py"))
|
|
|
|
def payload(action="reviewed", reviewer="darman",
|
|
review_type="pull_request_review_comment", content="please fix the typo",
|
|
head="feature/x", state="open", number=7, repo="darman/homelab",
|
|
with_review=True, with_pr=True):
|
|
p = {"action": action, "number": number,
|
|
"repository": {"full_name": repo},
|
|
"sender": {"login": reviewer}}
|
|
if with_pr:
|
|
p["pull_request"] = {"title": "some PR", "state": state,
|
|
"html_url": "https://git.mgaction.town/darman/homelab/pulls/7",
|
|
"head": {"ref": head}}
|
|
if with_review:
|
|
p["review"] = {"type": review_type, "content": content}
|
|
return p
|
|
|
|
def run(p):
|
|
r = subprocess.run([sys.executable, SCRIPT], input=json.dumps(p),
|
|
capture_output=True, text=True)
|
|
return r.returncode, r.stdout, r.stderr
|
|
|
|
def classify(rc, out):
|
|
"""Replicate Hermes's own interpretation of the script result."""
|
|
if rc != 0 or out.strip() == "" or out.strip() == "[SILENT]":
|
|
return "IGNORED"
|
|
try:
|
|
v = json.loads(out)
|
|
return "ALLOWED" if isinstance(v, dict) else "ALLOWED(script_output)"
|
|
except ValueError:
|
|
return "ALLOWED(script_output)"
|
|
|
|
fails = []
|
|
def check(name, p, expect):
|
|
rc, out, err = run(p)
|
|
got = classify(rc, out)
|
|
ok = got == expect
|
|
print(f"{'PASS' if ok else 'FAIL'} {name:<54} {got}")
|
|
if not ok:
|
|
fails.append(name); print(f" expected {expect}; stdout={out!r} stderr={err.strip()!r}")
|
|
return out
|
|
|
|
# --- the loop guard ---
|
|
check("luna's own review is dropped (LOOP GUARD)", payload(reviewer="luna"), "IGNORED")
|
|
check("luna in different case is dropped", payload(reviewer="LUNA"), "IGNORED")
|
|
|
|
# --- review types this route subscribes to ---
|
|
check("comment review by a human is allowed", payload(), "ALLOWED")
|
|
check("changes-requested review is allowed",
|
|
payload(review_type="pull_request_review_rejected", content="needs work"), "ALLOWED")
|
|
check("approval is dropped (not subscribed)",
|
|
payload(review_type="pull_request_review_approved", content="lgtm"), "IGNORED")
|
|
check("unknown review type is dropped",
|
|
payload(review_type="pull_request_review_request"), "IGNORED")
|
|
check("missing review object is dropped", payload(with_review=False), "IGNORED")
|
|
|
|
# --- an EMPTY review body must still pass: the substance is in the line
|
|
# comments, which the payload does not carry at all ---
|
|
check("empty review body is ALLOWED (body is optional)", payload(content=""), "ALLOWED")
|
|
check("null review body is ALLOWED", payload(content=None), "ALLOWED")
|
|
|
|
# --- action handling ---
|
|
check("action=opened is dropped", payload(action="opened"), "IGNORED")
|
|
check("action=synchronized is dropped", payload(action="synchronized"), "IGNORED")
|
|
check("missing action is dropped", payload(action=""), "IGNORED")
|
|
|
|
# --- pull request state ---
|
|
check("review on a closed/merged PR is dropped", payload(state="closed"), "IGNORED")
|
|
check("missing pull_request is dropped", payload(with_pr=False), "IGNORED")
|
|
check("missing head.ref is dropped", payload(head=""), "IGNORED")
|
|
|
|
# --- incomplete payloads ---
|
|
check("missing repository.full_name is dropped", payload(repo=""), "IGNORED")
|
|
check("missing PR number is dropped", payload(number=None), "IGNORED")
|
|
|
|
# --- normalisation: every path the prompt template uses must resolve ---
|
|
out = check("allowed delivery is a JSON object", payload(content=None), "ALLOWED")
|
|
allowed = json.loads(out)
|
|
for path in [("number",), ("repository", "full_name"), ("sender", "login"),
|
|
("pull_request", "title"), ("pull_request", "html_url"),
|
|
("pull_request", "head", "ref"), ("review", "type"), ("review", "content")]:
|
|
cur, ok = allowed, True
|
|
for k in path:
|
|
if isinstance(cur, dict) and k in cur: cur = cur[k]
|
|
else: ok = False; break
|
|
label = ".".join(path)
|
|
print(f"{'PASS' if ok else 'FAIL'} {'prompt path survives: {' + label + '}':<54} {cur if ok else 'MISSING'}")
|
|
if not ok: fails.append(f"path-{label}")
|
|
|
|
# a null content must normalise to "" and never to the literal "None"
|
|
c = allowed.get("review", {}).get("content")
|
|
print(f"{'PASS' if c == '' else 'FAIL'} {'null review.content normalises to empty string':<54} {c!r}")
|
|
if c != "": fails.append("content-normalised")
|
|
|
|
# --- drop contract: nonzero exit + empty stdout + reason on stderr ---
|
|
rc, out, err = run(payload(reviewer="luna"))
|
|
print(f"{'PASS' if rc == 3 else 'FAIL'} {'drop exits 3 (not 0, so Hermes logs it)':<54} rc={rc}")
|
|
if rc != 3: fails.append("drop-exit-code")
|
|
print(f"{'PASS' if out == '' else 'FAIL'} {'drop writes nothing to stdout':<54} {out!r}")
|
|
if out != "": fails.append("drop-stdout-empty")
|
|
print(f"{'PASS' if 'luna' in err else 'FAIL'} {'drop names the rule on stderr':<54} {err.strip()[-46:]!r}")
|
|
if "luna" not in err: fails.append("stderr-reason")
|
|
|
|
# a crash must stay distinguishable from a deliberate drop
|
|
rc, out, err = run("not-a-dict")
|
|
print(f"{'PASS' if rc == 3 else 'FAIL'} {'malformed payload is a drop (3), not a crash':<54} rc={rc}")
|
|
if rc != 3: fails.append("malformed-exit-code")
|
|
|
|
print()
|
|
print("ALL PASSED" if not fails else "FAILURES: " + ", ".join(fails))
|
|
sys.exit(1 if fails else 0)
|