From 38a5e924fe0549b8356640dcae9dea8969656ef7 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 20 May 2026 00:54:31 -0300 Subject: [PATCH] test: cover ariadne triage evidence helpers --- tests/test_testing_triage.py | 392 +++++++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) diff --git a/tests/test_testing_triage.py b/tests/test_testing_triage.py index 519f023..7008880 100644 --- a/tests/test_testing_triage.py +++ b/tests/test_testing_triage.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json + from ariadne.services import testing_triage @@ -36,6 +38,18 @@ class DummyStorage: return matching[-limit:][::-1] +class SettingsStub: + def __init__(self, **overrides) -> None: # type: ignore[no-untyped-def] + self.vm_url = "" + self.cluster_state_vm_timeout_sec = 1.0 + self.jenkins_base_url = "" + self.jenkins_api_user = "" + self.jenkins_api_token = "" + self.jenkins_api_timeout_sec = 1.0 + for key, value in overrides.items(): + setattr(self, key, value) + + def test_collect_testing_triage_builds_bundle(monkeypatch) -> None: storage = DummyStorage() monkeypatch.setattr( @@ -72,3 +86,381 @@ def test_run_testing_triage_stores_latest(monkeypatch) -> None: assert summary.status == "ok" assert storage.events[0][0] == testing_triage.TRIAGE_EVENT_TYPE assert latest["summary"]["status"] == "ok" + + +def test_latest_testing_triage_bundle_handles_json_strings() -> None: + class JsonStorage: + def list_events(self, limit: int = 1, event_type: str | None = None): # type: ignore[no-untyped-def] + assert limit == 1 + assert event_type == testing_triage.TRIAGE_EVENT_TYPE + return [{"detail": json.dumps({"summary": {"status": "ok"}})}] + + latest = testing_triage.latest_testing_triage_bundle(JsonStorage()) # type: ignore[arg-type] + + assert latest == {"summary": {"status": "ok"}} + + +def test_latest_testing_triage_bundle_ignores_bad_payloads() -> None: + class BadStorage: + def __init__(self, detail) -> None: # type: ignore[no-untyped-def] + self.detail = detail + + def list_events(self, limit: int = 1, event_type: str | None = None): # type: ignore[no-untyped-def] + return [{"detail": self.detail}] + + assert testing_triage.latest_testing_triage_bundle(BadStorage("{")) is None # type: ignore[arg-type] + assert testing_triage.latest_testing_triage_bundle(BadStorage(["nope"])) is None # type: ignore[arg-type] + assert testing_triage.latest_testing_triage_bundle(BadStorage(None)) is None # type: ignore[arg-type] + + class EmptyStorage: + def list_events(self, limit: int = 1, event_type: str | None = None): # type: ignore[no-untyped-def] + return [] + + assert testing_triage.latest_testing_triage_bundle(EmptyStorage()) is None # type: ignore[arg-type] + + +def test_latest_cluster_snapshot_falls_back_to_live_collect(monkeypatch) -> None: + class BrokenStorage: + def latest_cluster_state(self): # type: ignore[no-untyped-def] + raise RuntimeError("storage down") + + monkeypatch.setattr( + testing_triage, + "collect_cluster_state", + lambda: ({"collected_at": "live"}, {"status": "ok"}), + ) + errors: list[str] = [] + + snapshot = testing_triage._latest_cluster_snapshot(BrokenStorage(), errors) # noqa: SLF001 + + assert snapshot == {"collected_at": "live"} + assert errors == ["cluster_state_latest: storage down"] + + +def test_latest_cluster_snapshot_records_collect_error(monkeypatch) -> None: + monkeypatch.setattr( + testing_triage, + "collect_cluster_state", + lambda: (_ for _ in ()).throw(RuntimeError("api down")), + ) + errors: list[str] = [] + + snapshot = testing_triage._latest_cluster_snapshot(None, errors) # noqa: SLF001 + + assert snapshot == {} + assert errors == ["cluster_state_collect: api down"] + + +def test_cluster_evidence_limits_and_defaults() -> None: + snapshot = { + "summary": { + "health_bullets": list(range(20)), + "attention_ranked": [{"item": i} for i in range(20)], + }, + "nodes_summary": {"total": 2, "ready": 1, "not_ready": 1, "not_ready_names": ["titan-06"]}, + "flux": {"items": [{"name": str(i)} for i in range(20)]}, + "pod_issues": { + "items": [{"pod": str(i)} for i in range(20)], + "pending_oldest": [{"pod": "p"}], + }, + "jobs": {"failing": [{"job": "j"}], "active_oldest": [{"job": "old"}]}, + "events": {"warnings_recent": [{"message": "warn"}]}, + } + + evidence = testing_triage._cluster_evidence(snapshot) # noqa: SLF001 + + assert len(evidence["health_bullets"]) == testing_triage._MAX_EVIDENCE_ITEMS # noqa: SLF001 + assert evidence["nodes"]["not_ready_names"] == ["titan-06"] + assert evidence["jobs_failing"] == [{"job": "j"}] + + +def test_vm_items_handles_success_failure_and_bad_values(monkeypatch) -> None: + class FakeResponse: + def __init__(self, payload) -> None: # type: ignore[no-untyped-def] + self.payload = payload + + def raise_for_status(self) -> None: + return None + + def json(self): # type: ignore[no-untyped-def] + return self.payload + + class FakeClient: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + self.payload = kwargs.pop("payload", None) + + def __enter__(self): + return self + + def __exit__(self, *args) -> None: # type: ignore[no-untyped-def] + return None + + def get(self, url, params=None): # type: ignore[no-untyped-def] + assert url.endswith("/api/v1/query") + assert params == {"query": "up"} + return FakeResponse( + { + "status": "success", + "data": { + "result": [ + {"metric": {"suite": "ariadne", "__name__": "ignored"}, "value": [1, "2.5"]}, + {"metric": {"suite": "metis"}, "value": [1, "bad"]}, + ] + }, + } + ) + + monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url="http://vm")) + monkeypatch.setattr(testing_triage.httpx, "Client", FakeClient) + errors: list[str] = [] + + items = testing_triage._vm_items("up", errors) # noqa: SLF001 + + assert items == [ + {"labels": {"suite": "ariadne"}, "value": 2.5}, + {"labels": {"suite": "metis"}, "value": 0.0}, + ] + assert errors == [] + + +def test_vm_items_records_errors(monkeypatch) -> None: + class BrokenClient: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + return None + + def __enter__(self): + raise RuntimeError("network down") + + def __exit__(self, *args) -> None: # type: ignore[no-untyped-def] + return None + + monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url="http://vm")) + monkeypatch.setattr(testing_triage.httpx, "Client", BrokenClient) + errors: list[str] = [] + + assert testing_triage._vm_items("up", errors) == [] # noqa: SLF001 + assert errors == ["victoria_metrics: network down"] + + +def test_vm_items_handles_disabled_and_query_failure(monkeypatch) -> None: + class FailedResponse: + def raise_for_status(self) -> None: + return None + + def json(self): # type: ignore[no-untyped-def] + return {"status": "error"} + + class FailedClient: + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + return None + + def __enter__(self): + return self + + def __exit__(self, *args) -> None: # type: ignore[no-untyped-def] + return None + + def get(self, url, params=None): # type: ignore[no-untyped-def] + return FailedResponse() + + errors: list[str] = [] + monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url="")) + assert testing_triage._vm_items("up", errors) == [] # noqa: SLF001 + assert errors == [] + + monkeypatch.setattr(testing_triage, "settings", SettingsStub(vm_url="http://vm")) + monkeypatch.setattr(testing_triage.httpx, "Client", FailedClient) + assert testing_triage._vm_items("up", errors) == [] # noqa: SLF001 + assert errors == ["victoria_metrics: query failed"] + + +def test_jenkins_flatten_status_and_log_tail(monkeypatch) -> None: + rows = [ + "skip", + {"name": "", "lastBuild": {}}, + { + "name": "folder", + "jobs": [ + { + "name": "child", + "url": "http://jenkins/job/folder/job/child/", + "color": "red", + "lastBuild": { + "number": 7, + "result": "FAILURE", + "timestamp": 2000, + "duration": 3000, + "url": "http://jenkins/build/7/", + }, + } + ], + }, + {"name": "empty", "jobs": []}, + ] + + flattened = testing_triage._flatten_jobs(rows) # noqa: SLF001 + job = testing_triage._jenkins_job(flattened[0]) # noqa: SLF001 + + assert flattened[0]["name"] == "folder/child" + assert job is not None + assert job["status"] == "failure" + assert job["last_run_ts"] == 2.0 + assert job["last_duration_seconds"] == 3.0 + assert job["console_url"] == "http://jenkins/build/7/consoleText" + assert testing_triage._jenkins_status({"color": "blue_anime"}, "") == "running" # noqa: SLF001 + assert testing_triage._jenkins_status({}, "SUCCESS") == "success" # noqa: SLF001 + assert testing_triage._jenkins_status({"color": "green"}, "") == "success" # noqa: SLF001 + assert testing_triage._jenkins_status({"color": "yellow"}, "") == "failure" # noqa: SLF001 + assert testing_triage._jenkins_status({"color": "grey"}, "") == "unknown" # noqa: SLF001 + assert testing_triage._jenkins_job({"name": 1, "url": "u"}) is None # noqa: SLF001 + long_tail = testing_triage._tail_text("x" * (testing_triage._MAX_JENKINS_LOG_CHARS + 10)) # noqa: SLF001 + assert len(long_tail) == testing_triage._MAX_JENKINS_LOG_CHARS # noqa: SLF001 + assert testing_triage._tail_text("\n".join(str(i) for i in range(100))).startswith("20\n") # noqa: SLF001 + + +def test_attach_jenkins_log_tail_ignores_missing_url_and_records_errors(monkeypatch) -> None: + class BrokenClient: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] + return None + + def __enter__(self): + return self + + def __exit__(self, *args) -> None: # type: ignore[no-untyped-def] + return None + + def get(self, url): # type: ignore[no-untyped-def] + raise RuntimeError("log gone") + + errors: list[str] = [] + testing_triage._attach_jenkins_log_tail({"job": "missing"}, errors) # noqa: SLF001 + assert errors == [] + + monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_api_timeout_sec=1)) + monkeypatch.setattr(testing_triage.httpx, "Client", BrokenClient) + testing_triage._attach_jenkins_log_tail({"job": "ariadne", "console_url": "http://jenkins/log"}, errors) # noqa: SLF001 + + assert errors == ["jenkins_log:ariadne: log gone"] + + +def test_fetch_jenkins_jobs_and_log_tail(monkeypatch) -> None: + class FakeResponse: + def __init__(self, payload=None, text="") -> None: # type: ignore[no-untyped-def] + self.payload = payload + self.text = text + + def raise_for_status(self) -> None: + return None + + def json(self): # type: ignore[no-untyped-def] + return self.payload + + class FakeClient: + def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] + assert kwargs["auth"] == ("jenkins", "token") + + def __enter__(self): + return self + + def __exit__(self, *args) -> None: # type: ignore[no-untyped-def] + return None + + def get(self, url, params=None): # type: ignore[no-untyped-def] + if url.endswith("/api/json"): + assert "tree" in params + return FakeResponse( + { + "jobs": [ + { + "name": "ariadne", + "url": "http://jenkins/job/ariadne/", + "color": "red", + "lastBuild": {"number": 8, "result": "FAILURE", "timestamp": 1000, "duration": 1000}, + } + ] + } + ) + return FakeResponse(text="line1\nline2") + + monkeypatch.setattr( + testing_triage, + "settings", + SettingsStub(jenkins_api_user="jenkins", jenkins_api_token="token", jenkins_api_timeout_sec=3), + ) + monkeypatch.setattr(testing_triage.httpx, "Client", FakeClient) + + jobs = testing_triage._fetch_jenkins_jobs("http://jenkins") # noqa: SLF001 + errors: list[str] = [] + testing_triage._attach_jenkins_log_tail(jobs[0], errors) # noqa: SLF001 + + assert jobs[0]["job"] == "ariadne" + assert jobs[0]["status"] == "failure" + assert jobs[0]["log_tail"] == "line1\nline2" + assert errors == [] + + +def test_jenkins_signals_handles_disabled_and_failures(monkeypatch) -> None: + monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_base_url="")) + assert testing_triage._jenkins_signals([]) == {"failed_builds": []} # noqa: SLF001 + + monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_base_url="http://jenkins")) + monkeypatch.setattr( + testing_triage, + "_fetch_jenkins_jobs", + lambda base_url: (_ for _ in ()).throw(RuntimeError("boom")), + ) + errors: list[str] = [] + + assert testing_triage._jenkins_signals(errors) == {"failed_builds": []} # noqa: SLF001 + assert errors == ["jenkins: boom"] + + +def test_jenkins_signals_attaches_recent_failed_builds(monkeypatch) -> None: + jobs = [ + {"job": "old", "status": "failure", "last_run_ts": 1}, + {"job": "ok", "status": "success", "last_run_ts": 5}, + {"job": "running", "status": "running", "last_run_ts": 10}, + {"job": "unknown", "status": "unknown", "last_run_ts": 3}, + ] + attached: list[str] = [] + monkeypatch.setattr(testing_triage, "settings", SettingsStub(jenkins_base_url="http://jenkins")) + monkeypatch.setattr(testing_triage, "_fetch_jenkins_jobs", lambda base_url: jobs) + monkeypatch.setattr(testing_triage, "_attach_jenkins_log_tail", lambda job, errors: attached.append(job["job"])) + + signals = testing_triage._jenkins_signals([]) # noqa: SLF001 + + assert [item["job"] for item in signals["failed_builds"]] == ["running", "unknown", "old"] + assert attached == ["running", "unknown", "old"] + + +def test_summary_and_markdown_helpers() -> None: + quality = { + "failed_runs_24h": {"items": [{"labels": {"suite": "ariadne"}, "value": 2}]}, + "empty": {"items": []}, + "bad": "skip", + } + cluster = { + "flux_not_ready": [{"name": "monitoring"}], + "pod_issues": [], + "jobs_failing": [{"job": "job"}], + "collected_at": "now", + } + jenkins = {"failed_builds": [{"job": "ariadne"}]} + + summary = testing_triage._summary(cluster, quality, jenkins, []) # noqa: SLF001 + markdown = testing_triage._render_markdown( # noqa: SLF001 + { + "generated_at": "now", + "summary": summary, + "evidence": {"cluster": cluster, "quality": quality, "jenkins": jenkins}, + "unknowns": ["missing vm"], + } + ) + + assert summary["status"] == "needs_attention" + assert summary["failed_suites"] == ["ariadne"] + assert "- Flux: monitoring" in markdown + assert "- failed_runs_24h: {'suite': 'ariadne'} value=2" in markdown + assert "- missing vm" in markdown + assert testing_triage._markdown_named_items("Pods", ["bad"], "pod") == ["- Pods: none"] # noqa: SLF001 + assert testing_triage._jenkins_auth() is None # noqa: SLF001