ariadne/tests/test_hermes_code_flow.py
codex ca5b22b977
All checks were successful
Tests / Declarative: Post Actions passed: 1377
feat(hermes): name the Hermes run in the pull request it produced
The pull request said "Proposed by Hermes" and gave a reader nothing to check
that against. "Hermes made this decision" is a claim; the run id is the
receipt, and without it there is no way to open the run, read the prompt the
model was given, or see the tool calls it made. The diagnosis issues have
carried the run id from the start - the pull requests, which are the more
consequential artifact, did not.

The unused build_number parameter on open_pull_request is what the run id
replaces. That parameter was dead - SonarQube flags it as python:S1172 on this
very repository - so the argument count did not grow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:16:47 -03:00

494 lines
19 KiB
Python

from __future__ import annotations
import json
from types import SimpleNamespace
from ariadne.services import hermes_autotriage as autotriage
from ariadne.services import hermes_code_flow as module
from ariadne.services.hermes_agent_client import HermesRunResult
JOB = "hermes-code-demo"
INCIDENT_ID = f"{JOB}/7"
FILE_CONTENTS = "def discount(price):\n return price * 0.5\n"
BUNDLE = {"incident_id": INCIDENT_ID, "jenkins": {"job": JOB}, "log_evidence": {"records": []}}
class FakeStorage:
def __init__(self) -> None:
self.events: list[dict] = []
def record_event(self, event_type, detail) -> None: # type: ignore[no-untyped-def]
self.events.append({"event_type": event_type, "detail": detail})
def list_events(self, limit=200, event_type=None): # type: ignore[no-untyped-def]
rows = [
dict(row)
for row in reversed(self.events)
if event_type is None or row["event_type"] == event_type
]
return rows[:limit]
def _hermes_cfg() -> dict:
return {"base_url": "http://hermes:8642", "api_key": "key", "total_timeout_seconds": 420.0}
def _code_cfg(**overrides) -> dict: # type: ignore[no-untyped-def]
base = {
"hermes_ui_url": "",
"candidate_path": "src/discount.py",
"allowed_path_prefixes": ["src/"],
"allowed_suffixes": [".py"],
"max_patch_bytes": 4000,
"max_changed_lines": 20,
"gitea_base_url": "https://scm.example",
"gitea_token": "secret-token",
"owner": "bstein",
"repo": "hermes-code-demo",
"max_open_proposals": 0,
"fix_categories": [],
"base_branch": "master",
"timeout_seconds": 15.0,
"legacy_job": JOB,
"repos": {},
"job_prefixes": {},
"job_suffixes": {},
"job_base_branches": {},
"max_candidates": 3,
"max_context_chars": 60000,
}
base.update(overrides)
return base
def _model_output(**overrides) -> str: # type: ignore[no-untyped-def]
payload = {
"incident_id": INCIDENT_ID,
"analysis": "The multiplier regressed to 0.5.",
"patch": {
"path": "src/discount.py",
"original": "return price * 0.5",
"replacement": "return price * 0.9",
"rationale": "restore the intended discount",
},
"human_required": False,
"reason": "small localized fix",
}
payload.update(overrides)
return json.dumps(payload)
def _run(status: str = "completed", output=None) -> HermesRunResult: # type: ignore[no-untyped-def]
return HermesRunResult(
status=status,
output=output,
run_id="run-1",
session_id="sess-1",
error=None,
duration_seconds=1.5,
denied_approvals=0,
)
def _no_existing(**overrides) -> dict: # type: ignore[no-untyped-def]
base = {"found": False, "pr_number": None, "url": None, "branch": None, "error": None}
base.update(overrides)
return base
def _install(monkeypatch, *, fetch=None, run=None, push=None, pull=None, existing=None) -> dict: # type: ignore[no-untyped-def]
calls: dict = {"fetches": [], "runs": [], "pushes": [], "pulls": [], "lookups": []}
def fake_find(cfg): # type: ignore[no-untyped-def]
calls["lookups"].append(cfg)
return existing if existing is not None else _no_existing()
monkeypatch.setattr(module.hermes_code_repair, "find_open_proposal", fake_find)
def fake_fetch(cfg, path): # type: ignore[no-untyped-def]
calls["fetches"].append((cfg, path))
if isinstance(fetch, dict):
return fetch.get(path, (None, "file fetch http 404"))
return fetch if fetch is not None else (FILE_CONTENTS, None)
def fake_run(cfg, prompt): # type: ignore[no-untyped-def]
calls["runs"].append((cfg, prompt))
return run if run is not None else _run(output=_model_output())
def fake_push(cfg, incident_id, build_number, patch, contents): # type: ignore[no-untyped-def]
calls["pushes"].append((cfg, incident_id, build_number, patch, contents))
return push if push is not None else {"branch": "hermes-repair/7", "committed": True, "error": None}
def fake_pull(cfg, incident_id, build_number, branch, patch, analysis): # type: ignore[no-untyped-def]
calls["pulls"].append((cfg, incident_id, build_number, branch, patch, analysis))
return pull if pull is not None else {"pr_number": 5, "url": "https://scm.example/pulls/5", "error": None}
monkeypatch.setattr(module.hermes_code_repair, "fetch_file", fake_fetch)
monkeypatch.setattr(module.hermes_agent_client, "run_triage", fake_run)
monkeypatch.setattr(module.hermes_code_repair, "push_branch", fake_push)
monkeypatch.setattr(module.hermes_code_repair, "open_pull_request", fake_pull)
return calls
def _propose(monkeypatch, code_cfg=None, **kwargs): # type: ignore[no-untyped-def]
storage = FakeStorage()
calls = _install(monkeypatch, **kwargs)
cfg = code_cfg if code_cfg is not None else _code_cfg()
result = module.propose_code_fix(storage, INCIDENT_ID, JOB, 7, BUNDLE, _hermes_cfg(), cfg)
return storage, calls, result
def _event(storage: FakeStorage) -> dict:
rows = [row for row in storage.events if row["event_type"] == module.CODE_PROPOSAL_EVENT_TYPE]
assert len(rows) == 1
return rows[0]["detail"]
def test_happy_path_opens_pull_request(monkeypatch) -> None:
storage, calls, result = _propose(monkeypatch)
assert result == {
"status": "pr_opened",
"branch": "hermes-repair/7",
"pr_number": 5,
"url": "https://scm.example/pulls/5",
"path": "src/discount.py",
"run_id": "run-1",
}
assert calls["fetches"][0] == (_code_cfg(), "src/discount.py")
cfg, incident_id, build_number, patch, contents = calls["pushes"][0]
assert (incident_id, build_number) == (INCIDENT_ID, 7)
assert patch.original == "return price * 0.5"
assert contents == "def discount(price):\n return price * 0.9\n"
_, _, _, branch, _, analysis = calls["pulls"][0]
assert branch == "hermes-repair/7"
assert analysis == "The multiplier regressed to 0.5."
detail = _event(storage)
assert detail == {
"incident_id": INCIDENT_ID,
"job": JOB,
"build_number": 7,
"run_id": "run-1",
"validated": True,
"reject_reason": None,
"branch": "hermes-repair/7",
"pr_number": 5,
"url": "https://scm.example/pulls/5",
"repo": "bstein/hermes-code-demo",
"candidates": ["src/discount.py"],
"chosen_path": "src/discount.py",
}
serialized = json.dumps(detail)
assert "return price" not in serialized
assert "secret-token" not in serialized
def test_existing_open_proposal_suppresses_duplicate(monkeypatch) -> None:
"""Only the ceiling suppresses new work, not a single open proposal."""
existing = {
"found": True,
"open_count": module.DEFAULT_MAX_OPEN_PROPOSALS,
"pr_number": 1,
"url": "https://scm.example/pulls/1",
"branch": "hermes-repair/4",
"error": None,
}
storage, calls, result = _propose(monkeypatch, existing=existing)
assert result == {
"status": "human_required",
"reason": "open_proposal_limit_reached",
"pr_number": 1,
"url": "https://scm.example/pulls/1",
"branch": "hermes-repair/4",
}
assert calls["lookups"] == [_code_cfg()]
assert calls["fetches"] == []
assert calls["runs"] == []
assert calls["pushes"] == []
assert calls["pulls"] == []
assert _event(storage) == {
"incident_id": INCIDENT_ID,
"job": JOB,
"build_number": 7,
"run_id": None,
"validated": False,
"reject_reason": "open_proposal_limit_reached",
"branch": "hermes-repair/4",
"pr_number": 1,
"url": "https://scm.example/pulls/1",
"repo": "bstein/hermes-code-demo",
"candidates": [],
"chosen_path": None,
}
def test_proposal_lookup_error_fails_open(monkeypatch) -> None:
storage, calls, result = _propose(
monkeypatch, existing=_no_existing(error="open proposal lookup http 503")
)
assert result["status"] == "pr_opened"
assert len(calls["fetches"]) == 1
assert len(calls["runs"]) == 1
assert _event(storage)["validated"] is True
def test_prompt_is_frozen_shape(monkeypatch) -> None:
_, calls, _ = _propose(monkeypatch)
cfg, prompt = calls["runs"][0]
assert cfg == _hermes_cfg()
assert prompt.startswith("Use $triage-titan-test-failures.\n")
assert f"MINIMAL source fix for incident {INCIDENT_ID}" in prompt
assert "The repository is bstein/hermes-code-demo branch master." in prompt
assert '"incident_id": "<must equal ' + INCIDENT_ID + '>"' in prompt
assert "appearing exactly once" in prompt
assert "Change as few lines as possible; do not reformat; do not add dependencies." in prompt
assert "Ariadne validates and pushes the change — you do not execute anything." in prompt
assert "Set human_required to true if the fix is not a small localized source change." in prompt
assert json.dumps(BUNDLE, separators=(",", ":")) in prompt
assert "Current content of the candidate file src/discount.py:" in prompt
assert prompt.rstrip().endswith(FILE_CONTENTS.rstrip())
def test_fetch_failure_requires_human(monkeypatch) -> None:
storage, calls, result = _propose(monkeypatch, fetch=(None, "file fetch http 404"))
assert result == {"status": "human_required", "reason": "candidate_fetch_failed: file fetch http 404"}
assert calls["runs"] == []
assert calls["pushes"] == []
detail = _event(storage)
assert detail["run_id"] is None
assert detail["validated"] is False
assert detail["reject_reason"] == "candidate_fetch_failed: file fetch http 404"
def test_unfinished_run_requires_human(monkeypatch) -> None:
storage, calls, result = _propose(monkeypatch, run=_run(status="timeout"))
assert result == {"status": "human_required", "reason": "hermes_run_timeout"}
assert calls["pushes"] == []
assert _event(storage)["run_id"] == "run-1"
def test_completed_run_without_output_requires_human(monkeypatch) -> None:
_, calls, result = _propose(monkeypatch, run=_run(status="completed", output=None))
assert result == {"status": "human_required", "reason": "hermes_run_completed"}
assert calls["pushes"] == []
def test_invalid_patch_response_requires_human(monkeypatch) -> None:
storage, calls, result = _propose(monkeypatch, run=_run(output="no json here"))
assert result == {"status": "human_required", "reason": "patch_invalid: no_json_object_found"}
assert calls["pushes"] == []
assert _event(storage)["validated"] is False
def test_patch_path_mismatch_requires_human(monkeypatch) -> None:
output = _model_output(
patch={
"path": "src/other.py",
"original": "return price * 0.5",
"replacement": "return price * 0.9",
"rationale": "r",
}
)
_, calls, result = _propose(monkeypatch, run=_run(output=output))
assert result["reason"] == "patch_path_mismatch: got 'src/other.py' expected 'src/discount.py'"
assert calls["pushes"] == []
def test_rejected_gate_requires_human(monkeypatch) -> None:
output = _model_output(
patch={
"path": "src/discount.py",
"original": "not in the file",
"replacement": "still not",
"rationale": "r",
}
)
_, calls, result = _propose(monkeypatch, run=_run(output=output))
assert result == {"status": "human_required", "reason": "patch_rejected: original_missing"}
assert calls["pushes"] == []
def test_push_failure_requires_human(monkeypatch) -> None:
storage, calls, result = _propose(
monkeypatch, push={"branch": "hermes-repair/7", "committed": False, "error": "commit http 500"}
)
assert result == {"status": "human_required", "reason": "branch_push_failed: commit http 500"}
assert calls["pulls"] == []
detail = _event(storage)
assert detail["validated"] is True
assert detail["branch"] is None
def test_pull_request_failure_requires_human(monkeypatch) -> None:
storage, _, result = _propose(
monkeypatch, pull={"pr_number": None, "url": None, "error": "pull request http 500"}
)
assert result == {"status": "human_required", "reason": "pull_request_failed: pull request http 500"}
detail = _event(storage)
assert detail["validated"] is True
assert detail["branch"] == "hermes-repair/7"
assert detail["pr_number"] is None
def test_code_config_maps_settings() -> None:
config = SimpleNamespace(
hermes_code_job=JOB,
hermes_code_candidate_path="src/discount.py",
hermes_code_allowed_prefixes=["src/"],
hermes_code_allowed_suffixes=[".py"],
hermes_code_max_patch_bytes=4000,
hermes_code_max_changed_lines=20,
hermes_gitea_base_url="https://scm.example",
hermes_gitea_token="secret-token",
hermes_code_owner="bstein",
hermes_code_repo="hermes-code-demo",
hermes_code_base_branch="master",
)
assert module.code_config(config) == _code_cfg()
def _orchestrator_settings(**overrides) -> SimpleNamespace: # type: ignore[no-untyped-def]
values = {
"hermes_autotriage_enabled": True,
"hermes_autotriage_job_allowlist": [JOB],
"hermes_api_url": "http://hermes:8642",
"hermes_api_key": "key",
"hermes_run_timeout_seconds": 420.0,
"hermes_code_enabled": True,
"hermes_code_job": JOB,
"hermes_code_owner": "bstein",
"hermes_code_repo": "hermes-code-demo",
"hermes_code_base_branch": "master",
"hermes_code_candidate_path": "src/discount.py",
"hermes_code_allowed_prefixes": ["src/"],
"hermes_code_allowed_suffixes": [".py"],
"hermes_code_max_patch_bytes": 4000,
"hermes_code_max_changed_lines": 20,
"hermes_gitea_base_url": "https://scm.example",
"hermes_gitea_token": "secret-token",
}
values.update(overrides)
return SimpleNamespace(**values)
def _prepare_orchestrator(monkeypatch, flow_result, **setting_overrides): # type: ignore[no-untyped-def]
storage = FakeStorage()
calls: list = []
monkeypatch.setattr(autotriage, "settings", _orchestrator_settings(**setting_overrides))
monkeypatch.setattr(
autotriage.hermes_jenkins_client,
"fetch_job_payload",
lambda job: {
"lastBuild": {
"number": 7,
"result": "FAILURE",
"building": False,
"url": "https://ci.example/7/",
}
},
)
monkeypatch.setattr(autotriage.hermes_evidence, "collect_evidence", lambda i, j, b: dict(BUNDLE))
def fake_propose(storage_arg, incident_id, job, build_number, bundle, hermes_cfg, code_cfg): # type: ignore[no-untyped-def]
calls.append((incident_id, job, build_number, bundle, hermes_cfg, code_cfg))
return flow_result
monkeypatch.setattr(autotriage.hermes_code_flow, "propose_code_fix", fake_propose)
triage_calls: list = []
def fake_run_triage(cfg, prompt): # type: ignore[no-untyped-def]
triage_calls.append(prompt)
return _run(status="error")
monkeypatch.setattr(autotriage.hermes_agent_client, "run_triage", fake_run_triage)
return storage, calls, triage_calls
def test_orchestrator_records_code_fix_proposed_on_pr(monkeypatch) -> None:
storage, calls, triage_calls = _prepare_orchestrator(
monkeypatch,
{"status": "pr_opened", "branch": "hermes-repair/7", "pr_number": 5, "url": "https://scm.example/pulls/5"},
)
summary = autotriage.run_hermes_autotriage(storage)
assert summary["jobs"][JOB] == {
"status": "human_required",
"incident_id": INCIDENT_ID,
"reason": "code_fix_proposed",
}
incidents = [row["detail"] for row in storage.events if row["event_type"] == autotriage.INCIDENT_EVENT_TYPE]
assert [detail["status"] for detail in incidents] == ["detected", "human_required"]
assert incidents[-1]["phase"] == {
"reason": "code_fix_proposed",
"branch": "hermes-repair/7",
"pr_number": 5,
"url": "https://scm.example/pulls/5",
}
assert [row for row in storage.events if row["event_type"] == autotriage.ACTION_EVENT_TYPE] == []
assert triage_calls == []
incident_id, job, build_number, bundle, hermes_cfg, code_cfg = calls[0]
assert (incident_id, job, build_number) == (INCIDENT_ID, JOB, 7)
assert bundle == BUNDLE
assert hermes_cfg == _hermes_cfg()
assert code_cfg == _code_cfg()
def test_orchestrator_records_flow_failure_reason(monkeypatch) -> None:
storage, _, _ = _prepare_orchestrator(
monkeypatch, {"status": "human_required", "reason": "patch_rejected: path_unsafe"}
)
summary = autotriage.run_hermes_autotriage(storage)
assert summary["jobs"][JOB]["reason"] == "patch_rejected: path_unsafe"
incidents = [row["detail"] for row in storage.events if row["event_type"] == autotriage.INCIDENT_EVENT_TYPE]
assert incidents[-1]["status"] == "human_required"
assert incidents[-1]["phase"] == {"reason": "patch_rejected: path_unsafe"}
def test_orchestrator_defaults_missing_flow_reason(monkeypatch) -> None:
storage, _, _ = _prepare_orchestrator(monkeypatch, {"status": "human_required"})
summary = autotriage.run_hermes_autotriage(storage)
assert summary["jobs"][JOB]["reason"] == "code_fix_not_proposed"
def test_orchestrator_skips_code_path_for_other_jobs(monkeypatch) -> None:
storage, calls, triage_calls = _prepare_orchestrator(
monkeypatch, {"status": "pr_opened"}, hermes_code_job="another-job"
)
summary = autotriage.run_hermes_autotriage(storage)
assert summary["jobs"][JOB]["status"] == "human_required"
assert summary["jobs"][JOB]["reason"] == "hermes_run_error"
assert calls == []
assert len(triage_calls) == 1
def test_open_proposals_below_the_ceiling_do_not_suppress_new_work(monkeypatch) -> None:
"""A fix for one failure must not be withheld because another awaits review."""
existing = {
"found": True,
"open_count": 3,
"pr_number": 1,
"url": "https://scm.example/pulls/1",
"branch": "hermes-repair/4",
"error": None,
}
_storage, _calls, result = _propose(monkeypatch, existing=existing)
assert result["status"] == "pr_opened"
def test_the_ceiling_is_configurable(monkeypatch) -> None:
"""Review capacity is a property of the reviewer, not of the code."""
existing = {
"found": True,
"open_count": 2,
"pr_number": 1,
"url": "https://scm.example/pulls/1",
"branch": "hermes-repair/4",
"error": None,
}
cfg = {**_code_cfg(), "max_open_proposals": 2}
_storage, _calls, result = _propose(monkeypatch, existing=existing, code_cfg=cfg)
assert result["reason"] == "open_proposal_limit_reached"