Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/hyrule_engineering_loop/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
23 changes: 21 additions & 2 deletions tests/test_phase24_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from hyrule_engineering_loop.daemon import (
CORE_REPOS,
ICINGA_EXIT_STATUS,
DaemonConfig,
DaemonReport,
acquire_lock,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 ----------------------------------------


Expand Down