diff --git a/steward/mcp/tools/overview.py b/steward/mcp/tools/overview.py index 4e0e742..9b0ee83 100644 --- a/steward/mcp/tools/overview.py +++ b/steward/mcp/tools/overview.py @@ -191,7 +191,8 @@ def still_failing(failed, succeeded) -> list: """The failed runs nothing has since put right: a later success of the same playbook from the same source on the same hosts clears one, as long as it was at least as real (a dry run's success clears only a dry run's - failure).""" + failure). `succeeded` may be a one-pass result, so it is read once.""" + succeeded = list(succeeded) return [f for f in failed if not any( _run_key(s) == _run_key(f) and s.started_at > f.started_at and (_dry(f) or not _dry(s)) for s in succeeded)] diff --git a/tests/mcp/test_tools.py b/tests/mcp/test_tools.py index cf246c2..92f89df 100644 --- a/tests/mcp/test_tools.py +++ b/tests/mcp/test_tools.py @@ -214,6 +214,15 @@ def test_a_failed_run_put_right_since_is_not_a_finding(): _run(S, 40, playbook="upkeep/y.yml"), _run(S, 70)]) == [real_fail] +def test_every_failure_is_checked_against_every_success_even_from_a_one_pass_result(): + """The live miss: a query result is an iterator, and the first failure's + check used it up, so a second failure was never cleared.""" + F, S = AnsibleRunStatus.failed, AnsibleRunStatus.success + a, b = _run(F, 60, check=True, playbook="upkeep/a.yml"), _run(F, 50, check=True, playbook="upkeep/b.yml") + successes = iter([_run(S, 45, check=True, playbook="upkeep/b.yml"), _run(S, 55, check=True, playbook="upkeep/a.yml")]) + assert overview.still_failing([b, a], successes) == [] + + def test_rank_puts_critical_first_then_oldest(): fs = [overview.finding("warning", "k", "w-old", "", since=NOW - timedelta(days=1)), overview.finding("critical", "k", "c-new", "", since=NOW - timedelta(minutes=1)),