Files
hermes-agent/tests/hermes_cli/test_kanban_pr_acceptance.py
ethernet 9f2ba1b74d merge origin/main (779 commits) into ethie/pm-clean
Branch semantics kept where main and PM disagree: update_cmd_deps.py,
constraints-termux.txt, the Electron update-api-check module and the
post-swap hand-off test stay deleted; the pending-fleet-restart catch-up
and the local_runtime tag/download ladder stay retired (PM owns engines).

Ported from main onto the branch's shape: profile_scoped_chore for the
auto-archive and plugin-update housekeeping chores, the local-runtime
cross-process boot lock and residency cap, the checkpoint tmp_pack sweep,
the cua daemon-liveness status probe, the remote-served Desktop update
flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways
(urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn
without [standard]), and the umask-scoping spawn test.

uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from
main switched to utf-8-sig (check-windows-footguns).
2026-09-21 00:58:39 -04:00

132 lines
6.5 KiB
Python

"""Two lifecycle invariants, using real SQLite and a local GitHub HTTP contract."""
import json
import os
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import pytest
from hermes_cli import kanban_db as kb
from hermes_cli.kanban_db_connect import connect
@pytest.fixture
def github(tmp_path, monkeypatch):
state = {"conclusion": "success", "head": "a" * 40, "reads": 0, "requests": []}
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
state["requests"].append(self.path)
sha = state["head"]
if self.path == "/graphql":
value = {"data": {"repository": {"pullRequest": {
"headRefOid": sha, "baseRefName": "main", "state": "OPEN",
"baseRef": {"branchProtectionRule": {"requiredStatusChecks": [
{"context": "required", "app": {"databaseId": 1}}]}}}}}}
elif "/rules/branches/" in self.path:
value = [[]]
elif "/check-runs" in self.path:
run = {"id": 42, "name": "required", "head_sha": sha,
"app": {"id": 1}, "status": "in_progress" if state["conclusion"] == "pending" else "completed", "conclusion": state["conclusion"],
"html_url": "https://github.com/acme/repo/actions/runs/42"}
if state.get("stale"):
run["head_sha"] = "b" * 40
runs = [] if state.get("missing") else [run]
value = [{"total_count": 100 + len(runs), "check_runs": [
{**run, "id": 1000 + i, "name": "optional", "conclusion": "skipped"}
for i in range(100)]}, {"total_count": 100 + len(runs), "check_runs": runs}]
if state.get("race"):
state["race"]()
if state.get("head_change"):
state["head"] = "b" * 40
elif "/statuses" in self.path:
value = [[]]
elif "/pulls/" in self.path:
value = {"head": {"sha": sha}, "base": {"ref": "main"}, "state": "open"}
else:
self.send_error(404)
return
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(value).encode())
def log_message(self, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
shim = tmp_path / "bin"
shim.mkdir()
gh = shim / "gh"
gh.write_text(f"#!{sys.executable}\nimport sys,urllib.request\n"
f"u='http://127.0.0.1:{server.server_port}/'+sys.argv[2]\n"
"print(urllib.request.urlopen(u).read().decode())\n")
gh.chmod(0o755)
monkeypatch.setenv("PATH", str(shim) + os.pathsep + os.environ["PATH"])
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
kb.init_db()
try:
yield state
finally:
server.shutdown()
server.server_close()
thread.join()
@pytest.mark.platforms("linux")
def test_pr_completion_requires_current_required_evidence(github):
with connect() as conn:
for conclusion in ("failure", "pending", "cancelled", "timed_out", "action_required", "neutral", "skipped", None, "success"):
github.update(conclusion=conclusion, head="a" * 40)
tid = kb.create_task(conn, title="Publish", completion_contract="acme/repo")
ok = kb.complete_task(conn, tid, result="done", metadata={"published_pr": "https://github.com/acme/repo/pull/7"})
assert ok is (conclusion == "success")
task = kb.get_task(conn, tid)
assert (task.status == "done") is ok
receipts = [json.loads(r[0]) for r in conn.execute(
"SELECT payload FROM task_events WHERE task_id=? AND kind='pr_acceptance'", (tid,))]
assert receipts and receipts[-1]["head_sha"] == "a" * 40
if not ok:
assert task.status in {"running", "ready", "blocked", "review"}
assert "retry" in receipts[-1]["recovery"]
assert receipts[-1]["checks"][0]["id"] == 42
for fault in ("missing", "stale", "head_change"):
github.update(conclusion="success", head="a" * 40)
github[fault] = True
tid = kb.create_task(conn, title=fault, completion_contract="acme/repo")
assert not kb.complete_task(conn, tid, result="done", metadata={"published_pr": "https://github.com/acme/repo/pull/7"})
assert kb.get_task(conn, tid).status != "done"
github.pop(fault)
# Omission and a sibling repository cannot downgrade the stored declaration.
tid = kb.create_task(conn, title="publish", completion_contract="acme/repo")
assert not kb.complete_task(conn, tid, summary="local green")
assert not kb.complete_task(conn, tid, result="done", metadata={"published_pr": "https://github.com/other/repo/pull/7"})
before = len(github["requests"])
local = kb.create_task(conn, title="local", completion_contract="local-only")
assert kb.complete_task(conn, local, summary="https://github.com/acme/repo/pull/7 is background context")
assert len(github["requests"]) == before
@pytest.mark.platforms("linux")
def test_acceptance_receipts_and_terminal_write_share_run_ownership(github):
with connect() as conn:
for conclusion in ("success", "failure"):
tid = kb.create_task(conn, title="race", completion_contract="acme/repo")
owner = kb.claim_task(conn, tid)
run_id = owner.current_run_id
def reclaim():
with connect() as rival:
assert kb.block_task(rival, tid, reason="Reassigned during acceptance")
assert kb.unblock_task(rival, tid)
github["replacement"] = kb.claim_task(rival, tid).current_run_id
github.update(conclusion=conclusion, race=reclaim)
assert not kb.complete_task(conn, tid, result="done", expected_run_id=run_id,
metadata={"published_pr": "https://github.com/acme/repo/pull/7"})
assert kb.get_task(conn, tid).current_run_id == github["replacement"]
assert github["replacement"] != run_id
assert kb.get_task(conn, tid).status != "done"
assert conn.execute("SELECT count(*) FROM task_events WHERE task_id=? AND kind='pr_acceptance'", (tid,)).fetchone()[0] == 0
github.pop("race")