diff --git a/ion/propose/engine.py b/ion/propose/engine.py index 23cc8be..c5e6eee 100644 --- a/ion/propose/engine.py +++ b/ion/propose/engine.py @@ -136,6 +136,21 @@ def run_propose(ctx, dry_run: bool = False) -> dict: if c["kind"] not in ("policy_fix", "health_fix") or json.loads(latest["state"]).get("created_gen") == gen: info["state"] = f"closed ({latest['status']})" continue + if c["kind"] == "health_fix": + # A done health_fix means its PR was opened; while that PR still waits for a + # maintainer merge, the finding is being handled. Re-proposing it (seen live: + # P-0014/P-0015 duplicating P-0011/P-0012) is pure tracker noise. + st = json.loads(latest["state"]) + pr_n = st.get("pr_number") + if pr_n: + try: + pr = ctx.gh.rest("GET", f"/repos/{cfg.org}/{c['subject']['repo']}/pulls/{int(pr_n)}") + except Exception: # noqa: BLE001 (an unreadable PR never suppresses a finding) + pr = {} + if pr.get("state") == "open" and not pr.get("merged"): + info["state"] = f"suppressed (PR #{int(pr_n)} open)" + res["suppressed"].append(key) + continue if c["persist"] and not scoring.ready(streak, cfg): info["state"] = f"waiting ({streak}/{cfg['thresholds']['persist_runs']} runs)" continue diff --git a/tests/test_propose.py b/tests/test_propose.py index 52cb5b8..862b331 100644 --- a/tests/test_propose.py +++ b/tests/test_propose.py @@ -87,6 +87,27 @@ def test_hysteresis_in_engine_then_issue_created(ctx): assert not ctx.gh.writes_to("POST", "/comments") # never any comment +def test_done_health_fix_with_open_pr_is_not_reproposed(ctx): + analysed(ctx); run_propose(ctx) + run_collect(ctx); run_analyze(ctx); run_propose(ctx) + rows = ctx.db.q("select * from proposals where kind='health_fix'") + assert rows, "the fixture world should produce a health_fix proposal" + p = proposals.view(rows[0]) + repo = p["subject"]["repo"] + # the executor finished and its PR is still open, waiting for a maintainer merge + proposals.set_status(ctx.db, p["id"], "done") + ctx.db.set_state(p["id"], pr_number=41) + run_collect(ctx); run_analyze(ctx) + r = run_propose(ctx) + assert len(ctx.db.q("select 1 from proposals where kind='health_fix'")) == len(rows) + assert any("open" in str(c.get("state")) for c in r["candidates"] + if c["key"] == p["key"]), r["candidates"] + # once that PR is no longer open, a later generation may propose again + ctx.gh.merged.add((repo, 41)) + run_collect(ctx); run_analyze(ctx); run_propose(ctx) + assert len(ctx.db.q("select 1 from proposals where kind='health_fix'")) > len(rows) + + def test_dry_run_writes_nothing(ctx): analysed(ctx) before = len(ctx.gh.writes)