fix(hermes): give a non-model finding an honest headline and label
All checks were successful
Tests / Declarative: Post Actions passed: 1183
All checks were successful
Tests / Declarative: Post Actions passed: 1183
Two remaining misattributions in the artifact a human reads first. The summary line opened with "Hermes auto-triage classified" even when no run happened, and the hung-build path fell through to the generic "undiagnosed" label because HUNG_CLASSIFICATION was defined and never wired, so lesavka issue #3 was titled 'undiagnosed' for a build that plainly overran. The headline now reads "Ariadne recorded incident X as Y" when there was no run, and a caller may supply an explicit classification that is used only when no diagnosis produced one - a real diagnosis is never overridden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
dca283a17b
commit
011bc2c993
@ -104,7 +104,8 @@ def _handle_running_build(
|
|||||||
base = {"incident_id": incident_id, "job": job, "build_number": _int_value(last_build.get("number"))}
|
base = {"incident_id": incident_id, "job": job, "build_number": _int_value(last_build.get("number"))}
|
||||||
hermes_events.record_incident(storage, base, "human_required", {"reason": reason})
|
hermes_events.record_incident(storage, base, "human_required", {"reason": reason})
|
||||||
bundle = hermes_hung_builds.hung_bundle(job, last_build, cap)
|
bundle = hermes_hung_builds.hung_bundle(job, last_build, cap)
|
||||||
_file_incident_issue(storage, base, _diagnosis(bundle, None, reason, None), tick_state)
|
hung = {"classification": hermes_hung_builds.HUNG_CLASSIFICATION}
|
||||||
|
_file_incident_issue(storage, base, {**_diagnosis(bundle, None, reason, None), **hung}, tick_state)
|
||||||
return {"status": "human_required", "incident_id": incident_id, "reason": reason}
|
return {"status": "human_required", "incident_id": incident_id, "reason": reason}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -140,10 +140,17 @@ def fact_fields(fact: Any) -> dict[str, str]:
|
|||||||
def _summary_line(context: dict) -> str:
|
def _summary_line(context: dict) -> str:
|
||||||
"""Render the one-line summary that opens the issue body."""
|
"""Render the one-line summary that opens the issue body."""
|
||||||
|
|
||||||
|
incident_id = context.get("incident_id")
|
||||||
|
classification = context.get("classification")
|
||||||
|
run_id = str(context.get("run_id") or "")
|
||||||
|
if not run_id or run_id == "unknown":
|
||||||
|
# The headline is the first thing read, so it must not credit a model
|
||||||
|
# for a finding Ariadne made on its own.
|
||||||
|
return f"Ariadne recorded incident `{incident_id}` as **{classification}**."
|
||||||
confidence = context.get("confidence")
|
confidence = context.get("confidence")
|
||||||
return (
|
return (
|
||||||
f"Hermes auto-triage classified incident `{context.get('incident_id')}` as "
|
f"Hermes auto-triage classified incident `{incident_id}` as "
|
||||||
f"**{context.get('classification')}** (confidence {'n/a' if confidence is None else confidence}); "
|
f"**{classification}** (confidence {'n/a' if confidence is None else confidence}); "
|
||||||
f"first failed gate: `{context.get('first_failed_gate') or 'unknown'}`."
|
f"first failed gate: `{context.get('first_failed_gate') or 'unknown'}`."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -192,7 +192,11 @@ def issue_context(base: dict[str, Any], diagnosis: dict[str, Any]) -> dict[str,
|
|||||||
"job": str(base.get("job") or ""),
|
"job": str(base.get("job") or ""),
|
||||||
"build_number": base.get("build_number"),
|
"build_number": base.get("build_number"),
|
||||||
"build_url": str(jenkins.get("url") or "") if isinstance(jenkins, dict) else "",
|
"build_url": str(jenkins.get("url") or "") if isinstance(jenkins, dict) else "",
|
||||||
"classification": str(getattr(decision, "classification", "") or body.UNDIAGNOSED),
|
"classification": str(
|
||||||
|
getattr(decision, "classification", "")
|
||||||
|
or diagnosis.get("classification")
|
||||||
|
or body.UNDIAGNOSED
|
||||||
|
),
|
||||||
"confidence": getattr(decision, "confidence", None),
|
"confidence": getattr(decision, "confidence", None),
|
||||||
"first_failed_gate": str(getattr(decision, "first_failed_gate", "") or ""),
|
"first_failed_gate": str(getattr(decision, "first_failed_gate", "") or ""),
|
||||||
"reason": str(getattr(decision, "reason", "") or authorize_reason),
|
"reason": str(getattr(decision, "reason", "") or authorize_reason),
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from ariadne.services import hermes_incident_body as body
|
from ariadne.services import hermes_incident_body as body
|
||||||
from ariadne.services import hermes_incident_issue as module
|
from ariadne.services import hermes_incident_issue as module
|
||||||
|
|
||||||
@ -52,3 +54,48 @@ def test_observation_is_omitted_when_a_diagnosis_cited_facts() -> None:
|
|||||||
assert module._observation(jenkins) == ""
|
assert module._observation(jenkins) == ""
|
||||||
assert module._observation(None) == ""
|
assert module._observation(None) == ""
|
||||||
assert module._observation({"console_tail": " only tail "}) == "only tail"
|
assert module._observation({"console_tail": " only tail "}) == "only tail"
|
||||||
|
|
||||||
|
|
||||||
|
def test_headline_credits_ariadne_when_no_model_ran() -> None:
|
||||||
|
"""The summary line is read first; it must not credit a model either."""
|
||||||
|
|
||||||
|
context = module.issue_context(
|
||||||
|
{"incident_id": "lesavka/584", "job": "lesavka", "build_number": 584},
|
||||||
|
{"bundle": {}, "outcome": None, "authorize_reason": "hung_build",
|
||||||
|
"run_id": None, "classification": "build_exceeded_time_cap"},
|
||||||
|
)
|
||||||
|
rendered = body.issue_body(context)
|
||||||
|
|
||||||
|
assert rendered.startswith("Ariadne recorded incident `lesavka/584` as **build_exceeded_time_cap**.")
|
||||||
|
assert "Hermes auto-triage classified" not in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_headline_keeps_hermes_wording_and_confidence_for_a_real_run() -> None:
|
||||||
|
decision = SimpleNamespace(
|
||||||
|
classification="pytest_test_failure", confidence=0.99,
|
||||||
|
first_failed_gate="tests", reason="r", facts=[], inferences=[],
|
||||||
|
)
|
||||||
|
context = module.issue_context(
|
||||||
|
{"incident_id": "ariadne/409", "job": "ariadne", "build_number": 409},
|
||||||
|
{"bundle": {}, "outcome": SimpleNamespace(decision=decision),
|
||||||
|
"authorize_reason": "human_required", "run_id": "run_abc"},
|
||||||
|
)
|
||||||
|
rendered = body.issue_body(context)
|
||||||
|
|
||||||
|
assert "Hermes auto-triage classified incident `ariadne/409`" in rendered
|
||||||
|
assert "confidence 0.99" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_explicit_classification_is_used_only_without_a_decision() -> None:
|
||||||
|
"""A real diagnosis must never be overridden by a fallback label."""
|
||||||
|
|
||||||
|
decision = SimpleNamespace(
|
||||||
|
classification="pytest_test_failure", confidence=0.9,
|
||||||
|
first_failed_gate="t", reason="r", facts=[], inferences=[],
|
||||||
|
)
|
||||||
|
ctx = module.issue_context(
|
||||||
|
{"incident_id": "a/1", "job": "a", "build_number": 1},
|
||||||
|
{"bundle": {}, "outcome": SimpleNamespace(decision=decision),
|
||||||
|
"classification": "build_exceeded_time_cap", "run_id": "run_x"},
|
||||||
|
)
|
||||||
|
assert ctx["classification"] == "pytest_test_failure"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user