"""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