All checks were successful
Tests / Declarative: Post Actions passed: 1440
The first advice issues went out rendered through build-failure wording and were wrong in four places at once: the title printed a source line as a build number, the header claimed a first failed gate for a build that passed, the heading asked why a human was needed when nothing had broken, and the SonarQube link was labelled "Failed build". Every one of those is small and every one of them is the kind of thing that makes a reader distrust the rest of the page - which matters more here than usual, because the whole point of the issue is a suggestion they have to judge for themselves. A finding-sourced issue now says what it is: SonarQube reports this rule in this file, the build is green, this is a standing finding rather than a failure. Build-failure issues are untouched, and a test asserts each wording stays out of the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
290 lines
9.7 KiB
Python
290 lines
9.7 KiB
Python
"""Tests for filing a suggested fix when a finding cannot become a patch."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from ariadne.services import hermes_sonar_advice as module
|
|
|
|
|
|
ISSUE = {
|
|
"key": "AZ1",
|
|
"rule": "python:S3776",
|
|
"severity": "CRITICAL",
|
|
"type": "CODE_SMELL",
|
|
"effort": "11min",
|
|
"path": "ariadne/services/thing.py",
|
|
"line": 42,
|
|
"message": "Refactor this function to reduce its Cognitive Complexity from 21 to the 15 allowed.",
|
|
}
|
|
|
|
REPO_CFG = {
|
|
"owner": "bstein",
|
|
"repo": "ariadne",
|
|
"gitea_base_url": "https://scm.example",
|
|
"gitea_token": "t",
|
|
"base_branch": "master",
|
|
}
|
|
|
|
INCIDENT = "sonar/ariadne/python:S3776/AZ1"
|
|
|
|
SUGGESTION = {
|
|
"path": "ariadne/services/thing.py",
|
|
"explanation": "Extract the validation branch into its own helper.",
|
|
"code": "def _validate(row):\n return bool(row)",
|
|
}
|
|
|
|
|
|
def _config(**overrides):
|
|
values = {
|
|
"hermes_ui_url": "https://agent.example",
|
|
"hermes_sonar_ui_url": "https://quality.example",
|
|
"hermes_gitea_base_url": "https://scm.example",
|
|
"hermes_gitea_token": "t",
|
|
}
|
|
values.update(overrides)
|
|
return SimpleNamespace(**values)
|
|
|
|
|
|
class _Storage:
|
|
def __init__(self):
|
|
self.events = []
|
|
|
|
def record_event(self, event_type, detail):
|
|
self.events.append((event_type, detail))
|
|
|
|
|
|
def _response(**overrides):
|
|
payload = {
|
|
"incident_id": INCIDENT,
|
|
"analysis": "The function branches five ways over the same row.",
|
|
"code_suggestions": [dict(SUGGESTION)],
|
|
"human_required": True,
|
|
"reason": "needs a maintainer",
|
|
}
|
|
payload.update(overrides)
|
|
return json.dumps(payload)
|
|
|
|
|
|
@pytest.fixture
|
|
def wiring(monkeypatch):
|
|
state = {
|
|
"existing": {"found": False},
|
|
"contents": "def thing():\n pass\n",
|
|
"fetch_error": None,
|
|
"run": SimpleNamespace(status="completed", output=_response(), run_id="run_x"),
|
|
"created": {"issue_number": 9, "url": "https://scm.example/issues/9", "error": None},
|
|
"filed_context": [],
|
|
}
|
|
monkeypatch.setattr(
|
|
module.hermes_incident_issue, "find_open_incident_issue",
|
|
lambda cfg, job, classification, incident: state["existing"],
|
|
)
|
|
monkeypatch.setattr(
|
|
module.hermes_code_repair, "fetch_file",
|
|
lambda cfg, path: (state["contents"], state["fetch_error"]),
|
|
)
|
|
monkeypatch.setattr(module.hermes_agent_client, "run_triage", lambda cfg, prompt: state["run"])
|
|
|
|
def _create(cfg, context):
|
|
state["filed_context"].append(context)
|
|
return state["created"]
|
|
|
|
monkeypatch.setattr(module.hermes_incident_issue, "create_incident_issue", _create)
|
|
return state
|
|
|
|
|
|
def test_a_finding_becomes_an_issue_carrying_the_suggested_code(wiring) -> None:
|
|
storage = _Storage()
|
|
|
|
result = module.advise(storage, _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
assert result == {"filed": True, "reason": "issue_filed", "url": "https://scm.example/issues/9"}
|
|
context = wiring["filed_context"][0]
|
|
assert context["incident_id"] == INCIDENT
|
|
assert context["classification"] == "python:S3776"
|
|
assert context["code_suggestions"][0].code.startswith("def _validate")
|
|
assert context["reason"].startswith("The function branches")
|
|
|
|
|
|
def test_the_issue_links_to_both_the_finding_and_the_run(wiring) -> None:
|
|
module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
context = wiring["filed_context"][0]
|
|
assert context["build_url"] == (
|
|
"https://quality.example/project/issues?resolved=false&id=ariadne&open=AZ1"
|
|
)
|
|
assert context["run_url"] == "https://agent.example/chat?resume=run_x"
|
|
|
|
|
|
def test_the_issue_says_why_nothing_was_patched(wiring) -> None:
|
|
"""Otherwise it reads as the automation having failed."""
|
|
|
|
module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
assert "no automated patch was possible" in wiring["filed_context"][0]["authorize_reason"]
|
|
|
|
|
|
def test_a_rule_already_filed_is_not_filed_again(wiring) -> None:
|
|
"""One rule is one root cause; an issue per instance buries the repo."""
|
|
|
|
wiring["existing"] = {"found": True, "url": "https://scm.example/issues/3"}
|
|
|
|
result = module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
assert result["filed"] is False
|
|
assert result["reason"] == "issue_already_open"
|
|
assert wiring["filed_context"] == []
|
|
|
|
|
|
def test_an_unreadable_file_files_nothing(wiring) -> None:
|
|
wiring["contents"] = None
|
|
wiring["fetch_error"] = "http 404"
|
|
|
|
result = module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
assert result["filed"] is False
|
|
assert "file_fetch_failed" in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("status", "output"),
|
|
[("failed", ""), ("completed", ""), ("timeout", "{}")],
|
|
)
|
|
def test_a_run_that_produced_nothing_files_nothing(wiring, status, output) -> None:
|
|
wiring["run"] = SimpleNamespace(status=status, output=output, run_id="r")
|
|
|
|
assert module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))["filed"] is False
|
|
|
|
|
|
def test_a_response_with_no_suggestions_files_nothing(wiring) -> None:
|
|
wiring["run"] = SimpleNamespace(
|
|
status="completed", output=_response(code_suggestions=[]), run_id="r"
|
|
)
|
|
|
|
result = module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
assert result["reason"] == "no_suggestions_returned"
|
|
|
|
|
|
def test_a_failed_creation_is_reported(wiring) -> None:
|
|
wiring["created"] = {"issue_number": None, "url": None, "error": "http 500"}
|
|
|
|
assert module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))["filed"] is False
|
|
|
|
|
|
def test_every_attempt_is_recorded(wiring) -> None:
|
|
storage = _Storage()
|
|
module.advise(storage, _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
assert storage.events[0][0] == module.ADVICE_EVENT_TYPE
|
|
assert storage.events[0][1]["incident_id"] == INCIDENT
|
|
|
|
|
|
def test_advice_never_raises(monkeypatch) -> None:
|
|
"""It runs after a pull request was already declined; it must not erase it."""
|
|
|
|
monkeypatch.setattr(
|
|
module.hermes_incident_issue, "find_open_incident_issue",
|
|
lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom")),
|
|
)
|
|
|
|
result = module.advise(_Storage(), _config(), {}, REPO_CFG, "ariadne", dict(ISSUE))
|
|
|
|
assert result["filed"] is False
|
|
assert "advice_failed" in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw",
|
|
[
|
|
"",
|
|
"not json at all",
|
|
'{"incident_id": "someone/else", "analysis": "a", "code_suggestions": [], "human_required": true, "reason": "r"}',
|
|
'{"incident_id": "' + INCIDENT + '", "code_suggestions": "nope"}',
|
|
],
|
|
)
|
|
def test_an_unusable_response_yields_no_suggestions(raw) -> None:
|
|
assert module.parse_advice(raw, INCIDENT) == ([], "")
|
|
|
|
|
|
def test_a_good_response_is_parsed() -> None:
|
|
suggestions, analysis = module.parse_advice("noise " + _response() + " trailing", INCIDENT)
|
|
|
|
assert len(suggestions) == 1
|
|
assert suggestions[0].path == "ariadne/services/thing.py"
|
|
assert analysis.startswith("The function branches")
|
|
|
|
|
|
def test_the_prompt_states_that_nothing_is_applied() -> None:
|
|
prompt = module._prompt(INCIDENT, dict(ISSUE), "def thing(): pass")
|
|
|
|
assert "not a build failure" in prompt
|
|
assert "Nothing you return is applied" in prompt
|
|
assert "python:S3776" in prompt
|
|
assert "def thing(): pass" in prompt
|
|
|
|
|
|
def test_the_prompt_bounds_the_file_it_sends() -> None:
|
|
prompt = module._prompt(INCIDENT, dict(ISSUE), "x" * 90000)
|
|
|
|
assert len(prompt) < 45000
|
|
|
|
|
|
def test_a_finding_issue_never_uses_build_failure_wording() -> None:
|
|
"""A line number printed as a build number reads as a failure that never happened."""
|
|
|
|
from ariadne.services import hermes_incident_body as body
|
|
from ariadne.services import hermes_code_suggestion as cs
|
|
|
|
context = {
|
|
"incident_id": INCIDENT,
|
|
"job": "ariadne",
|
|
"build_number": 42,
|
|
"classification": "python:S3776",
|
|
"finding_path": "ariadne/services/thing.py",
|
|
"reason": "The function branches five ways.",
|
|
"authorize_reason": "no automated patch was possible for this finding",
|
|
"build_url": "https://quality.example/project/issues?open=AZ1",
|
|
"run_id": "run_x",
|
|
"run_url": "https://agent.example/chat?resume=run_x",
|
|
"code_suggestions": cs.from_payload({"code_suggestions": [dict(SUGGESTION)]}),
|
|
}
|
|
|
|
assert body.issue_title(context) == "[hermes] ariadne: python:S3776"
|
|
rendered = body.issue_body(context)
|
|
assert rendered.startswith("SonarQube reports **python:S3776**")
|
|
assert "The build is green" in rendered
|
|
assert "## What is wrong" in rendered
|
|
assert "- SonarQube finding: https://quality.example" in rendered
|
|
assert "Failed build" not in rendered
|
|
assert "first failed gate" not in rendered
|
|
assert "Why a human is needed" not in rendered
|
|
|
|
|
|
def test_a_build_failure_issue_is_unchanged() -> None:
|
|
"""The finding wording must not leak into real triage issues."""
|
|
|
|
from ariadne.services import hermes_incident_body as body
|
|
|
|
context = {
|
|
"incident_id": "ariadne/408",
|
|
"job": "ariadne",
|
|
"build_number": 408,
|
|
"classification": "pytest_test_failure",
|
|
"reason": "a repository test failure",
|
|
"authorize_reason": "human_required",
|
|
"build_url": "https://ci.example/job/ariadne/408/",
|
|
"run_id": "run_y",
|
|
}
|
|
|
|
assert body.issue_title(context) == "[hermes] ariadne #408: pytest_test_failure"
|
|
rendered = body.issue_body(context)
|
|
assert "## Why a human is needed" in rendered
|
|
assert "- Failed build: https://ci.example" in rendered
|
|
assert "did not authorize automated remediation" in rendered
|
|
assert "SonarQube" not in rendered
|