From 2bb989c9b8899416c689a365f88ab7f311017478 Mon Sep 17 00:00:00 2001 From: David Hyrule Date: Tue, 14 Jul 2026 18:57:18 +0200 Subject: [PATCH] fix: suppress non-actionable Discord run summaries --- src/hyrule_engineering_loop/daemon.py | 10 ++++++++-- tests/test_phase24_daemon.py | 23 +++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/hyrule_engineering_loop/daemon.py b/src/hyrule_engineering_loop/daemon.py index c49f088..ec4f818 100644 --- a/src/hyrule_engineering_loop/daemon.py +++ b/src/hyrule_engineering_loop/daemon.py @@ -306,7 +306,13 @@ def _default_http_post(url: str, payload: dict[str, Any]) -> None: def notify_discord(report: DaemonReport, *, poster: Poster | None = None) -> bool: - """One-line run summary to the configured Discord webhook.""" + """Post only a newly published PR; Icinga owns failure case cards.""" + # Idle/budget/lock outcomes are not actionable. Failures already enter the + # persistent AI case path via the passive Icinga result, so posting those + # here would create a duplicate. A published PR is the one direct message + # that represents a new operator action. + if report.outcome != "published": + return False webhook = os.environ.get("HYRULE_DISCORD_WEBHOOK") if not webhook: return False @@ -326,7 +332,7 @@ def notify_discord(report: DaemonReport, *, poster: Poster | None = None) -> boo "published": 0, "idle": 0, "needs_triage": 1, - "over_budget": 1, + "over_budget": 0, "locked": 0, "refused_ci": 2, "error": 2, diff --git a/tests/test_phase24_daemon.py b/tests/test_phase24_daemon.py index 7618fd8..96b9b71 100644 --- a/tests/test_phase24_daemon.py +++ b/tests/test_phase24_daemon.py @@ -11,6 +11,7 @@ from hyrule_engineering_loop.daemon import ( CORE_REPOS, + ICINGA_EXIT_STATUS, DaemonConfig, DaemonReport, acquire_lock, @@ -719,8 +720,8 @@ def test_budget_exhaustion_journals_and_next_run_unaffected( assert report1.outcome == "needs_triage" assert report1.pr_url is None assert "budget exhausted" in report1.detail - assert report1.notifications == ["discord"] - assert "needs_triage" in discord[0]["content"] + assert report1.notifications == [] + assert discord == [] published: list[dict[str, Any]] = [] report2 = daemon_once( @@ -751,6 +752,24 @@ def test_daily_run_budget_stops_further_runs(tmp_path: Path) -> None: assert "run budget" in report.detail +@pytest.mark.parametrize("outcome", ["idle", "over_budget", "locked", "needs_triage", "refused_ci", "error"]) +def test_non_published_outcomes_do_not_duplicate_discord( + monkeypatch: pytest.MonkeyPatch, outcome: str +) -> None: + monkeypatch.setenv("HYRULE_DISCORD_WEBHOOK", "https://discord.invalid/webhook") + monkeypatch.setenv("HYRULE_ICINGA_URL", "https://mon.invalid:5665") + monkeypatch.setenv("HYRULE_ICINGA_USER", "loop") + monkeypatch.setenv("HYRULE_ICINGA_PASSWORD", "secret") + discord: list[dict[str, Any]] = [] + icinga: list[dict[str, Any]] = [] + report = DaemonReport(outcome=outcome, detail="test outcome") + + assert notify_discord(report, poster=lambda url, payload: discord.append(payload)) is False + assert notify_icinga(report, poster=lambda url, payload: icinga.append(payload)) is True + assert discord == [] + assert icinga[0]["exit_status"] == ICINGA_EXIT_STATUS[outcome] + + # --- kill criterion: stall detection ----------------------------------------