From e6d5124ffc212aa9a296822a01e8bc8bf6bed734 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:21:32 +0200 Subject: [PATCH] fix(plane): doctor no longer reads "No resources found" as a ready pod kubectl prints the sentence as one whitespace-split row, which passed the ready check and reported an absent ax controller as "1 pods ready". Rows must carry a READY column; an empty namespace fails. Regression test added. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- src/openultrasast/plane/reconciler.py | 6 ++++-- tests/test_plane_reconciler.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/openultrasast/plane/reconciler.py b/src/openultrasast/plane/reconciler.py index 377392d..aeaad4b 100644 --- a/src/openultrasast/plane/reconciler.py +++ b/src/openultrasast/plane/reconciler.py @@ -469,8 +469,10 @@ def _pods_ready(namespace: str, context: str) -> tuple[bool, str]: ok, out = _sh("kubectl", "--context", context, "-n", namespace, "get", "pods", "--no-headers") if not ok or not out: return False, out or "no pods" - rows = [line.split() for line in out.splitlines()] - bad = [r[0] for r in rows if len(r) > 2 and r[2] not in ("Completed", "Succeeded") and r[1].split("/")[0] != r[1].split("/")[-1]] + rows = [r for r in (line.split() for line in out.splitlines()) if len(r) > 2 and re.fullmatch(r"\d+/\d+", r[1])] + if not rows: # "No resources found" is not a ready namespace + return False, f"no pods in {namespace}" + bad = [r[0] for r in rows if r[2] not in ("Completed", "Succeeded") and r[1].split("/")[0] != r[1].split("/")[1]] return not bad, f"not ready: {', '.join(bad)}" if bad else f"{len(rows)} pods ready" diff --git a/tests/test_plane_reconciler.py b/tests/test_plane_reconciler.py index 9ee7321..a1b3dfb 100644 --- a/tests/test_plane_reconciler.py +++ b/tests/test_plane_reconciler.py @@ -483,3 +483,16 @@ def test_elapsed_polling_is_paced(fake: Fake, tmp_path: Path, monkeypatch: pytes started = time.monotonic() assert reconciler.run(write_run(tmp_path, "paced", PAIR.replace("{serialize}", "")), ax=str(fake.ax)) == "done" assert time.monotonic() - started >= 0.4, "three polls of x plus one of y at 0.1s each" + + +def test_doctor_treats_an_empty_namespace_as_not_ready(monkeypatch: pytest.MonkeyPatch) -> None: + """kubectl's "No resources found" must not read as one ready pod (a plausible-zero instrument failure).""" + from openultrasast.plane import reconciler + + monkeypatch.setattr(reconciler, "_sh", lambda *a: (True, "No resources found in ax-system namespace.")) + ok, text = reconciler._pods_ready("ax-system", "kind-ousast") + assert not ok and "no pods" in text + monkeypatch.setattr(reconciler, "_sh", lambda *a: (True, "a-1 1/1 Running 0 1m\nb-2 0/1 Completed 0 1m")) + assert reconciler._pods_ready("ns", "ctx") == (True, "2 pods ready") + monkeypatch.setattr(reconciler, "_sh", lambda *a: (True, "a-1 0/1 Running 0 1m")) + assert reconciler._pods_ready("ns", "ctx")[0] is False