diff --git a/ariadne/app.py b/ariadne/app.py index 00b1829..b460f46 100644 --- a/ariadne/app.py +++ b/ariadne/app.py @@ -32,7 +32,14 @@ from .services.nextcloud import nextcloud from .services.opensearch_prune import prune_indices from .services.platform_quality_probe import platform_quality_probe from .services.pod_cleaner import clean_finished_pods -from .services.testing_triage import TRIAGE_EVENT_TYPE, collect_testing_triage, latest_testing_triage_bundle, run_testing_triage +from .services.testing_triage import ( + TRIAGE_EVENT_TYPE, + collect_testing_triage, + latest_testing_triage_bundle, + latest_testing_triage_diagnosis, + run_testing_triage, + run_testing_triage_diagnosis, +) from .services.vault import vault from .services.vaultwarden_sync import run_vaultwarden_sync from .services.wger import wger diff --git a/ariadne/app_admin_routes.py b/ariadne/app_admin_routes.py index 0439ffe..175287a 100644 --- a/ariadne/app_admin_routes.py +++ b/ariadne/app_admin_routes.py @@ -181,6 +181,42 @@ def _register_admin_routes(app: FastAPI, require_auth: Callable, deps: Callable[ module.storage.record_event(module.TRIAGE_EVENT_TYPE, bundle) return JSONResponse(bundle) + @app.get("/api/admin/testing/triage/diagnosis/latest") + def get_testing_triage_diagnosis(ctx: AuthContext = Depends(require_auth)) -> JSONResponse: + """Return the latest local-model testing triage diagnosis.""" + + module = deps() + module._require_admin(ctx) + diagnosis = module.latest_testing_triage_diagnosis(module.storage) + if not diagnosis: + raise HTTPException(status_code=404, detail="testing triage diagnosis unavailable") + return JSONResponse(diagnosis) + + @app.get("/api/internal/testing/triage/diagnosis/latest") + def get_testing_triage_diagnosis_internal() -> JSONResponse: + """Return the latest local-model diagnosis for trusted internal callers.""" + + module = deps() + diagnosis = module.latest_testing_triage_diagnosis(module.storage) + if not diagnosis: + raise HTTPException(status_code=404, detail="testing triage diagnosis unavailable") + return JSONResponse(diagnosis) + + @app.post("/api/admin/testing/triage/diagnosis/run") + def run_testing_triage_diagnosis(ctx: AuthContext = Depends(require_auth)) -> JSONResponse: + """Collect evidence and run the local model testing diagnosis.""" + + module = deps() + module._require_admin(ctx) + return JSONResponse(module.run_testing_triage_diagnosis(module.storage)) + + @app.post("/api/internal/testing/triage/diagnosis/run") + def run_testing_triage_diagnosis_internal() -> JSONResponse: + """Collect evidence and run local model diagnosis for trusted internal callers.""" + + module = deps() + return JSONResponse(module.run_testing_triage_diagnosis(module.storage)) + @app.post("/api/admin/access/requests/{username}/approve") async def approve_access_request( username: str, diff --git a/ariadne/services/testing_triage.py b/ariadne/services/testing_triage.py index 68557df..904defe 100644 --- a/ariadne/services/testing_triage.py +++ b/ariadne/services/testing_triage.py @@ -16,6 +16,7 @@ from .cluster_state import collect_cluster_state logger = get_logger(__name__) TRIAGE_EVENT_TYPE = "testing_triage_bundle" +TRIAGE_DIAGNOSIS_EVENT_TYPE = "testing_triage_diagnosis" _SUCCESS_STATUS = "ok|passed|success|not_applicable|skipped|na|n/a" _JENKINS_TREE = ( "jobs[name,url,color,lastBuild[number,result,timestamp,duration,url]," @@ -25,6 +26,15 @@ _JENKINS_TREE = ( _MAX_JENKINS_LOG_LINES = 80 _MAX_JENKINS_LOG_CHARS = 12000 _MAX_EVIDENCE_ITEMS = 12 +_MAX_MODEL_EVIDENCE_CHARS = 24000 +_MAX_MODEL_OUTPUT_CHARS = 12000 +_DIAGNOSIS_SYSTEM_PROMPT = ( + "You are Ariadne's local testing triage model. Use only the supplied JSON evidence. " + "Return JSON only with keys: headline, root_cause, blast_radius, confidence, " + "needs_human, next_actions, evidence_refs. Confidence must be low, medium, or high. " + "Next actions must be read-only verification or Flux/IaC changes; never suggest " + "mutating kubectl commands or reading Kubernetes Secret values." +) @dataclass(frozen=True) @@ -43,7 +53,17 @@ class TestingTriageSummary: def latest_testing_triage_bundle(storage: Storage) -> dict[str, Any] | None: """Return the most recent stored testing triage bundle, if present.""" - rows = storage.list_events(limit=1, event_type=TRIAGE_EVENT_TYPE) + return _latest_event_payload(storage, TRIAGE_EVENT_TYPE) + + +def latest_testing_triage_diagnosis(storage: Storage) -> dict[str, Any] | None: + """Return the most recent stored local-model testing diagnosis, if present.""" + + return _latest_event_payload(storage, TRIAGE_DIAGNOSIS_EVENT_TYPE) + + +def _latest_event_payload(storage: Storage, event_type: str) -> dict[str, Any] | None: + rows = storage.list_events(limit=1, event_type=event_type) if not rows: return None detail = rows[0].get("detail") @@ -63,6 +83,9 @@ def run_testing_triage(storage: Storage) -> TestingTriageSummary: bundle = collect_testing_triage(storage) storage.record_event(TRIAGE_EVENT_TYPE, bundle) + if _model_diagnosis_enabled(): + diagnosis = diagnose_testing_triage(bundle) + storage.record_event(TRIAGE_DIAGNOSIS_EVENT_TYPE, diagnosis) summary = bundle.get("summary") if isinstance(bundle.get("summary"), dict) else {} result = TestingTriageSummary( status=str(summary.get("status") or "unknown"), @@ -81,6 +104,24 @@ def run_testing_triage(storage: Storage) -> TestingTriageSummary: return result +def run_testing_triage_diagnosis(storage: Storage) -> dict[str, Any]: + """Collect evidence, ask the local model for diagnosis, and store both artifacts.""" + + bundle = collect_testing_triage(storage) + storage.record_event(TRIAGE_EVENT_TYPE, bundle) + diagnosis = diagnose_testing_triage(bundle) + storage.record_event(TRIAGE_DIAGNOSIS_EVENT_TYPE, diagnosis) + logger.info( + "testing triage diagnosis stored", + extra={ + "event": "testing_triage_diagnosis", + "status": diagnosis.get("status") or "unknown", + "model": diagnosis.get("model") or "", + }, + ) + return diagnosis + + def collect_testing_triage(storage: Storage | None = None) -> dict[str, Any]: """Build a bounded evidence bundle for agentic testing triage. @@ -121,6 +162,222 @@ def collect_testing_triage(storage: Storage | None = None) -> dict[str, Any]: return bundle +def diagnose_testing_triage(bundle: dict[str, Any]) -> dict[str, Any]: + """Ask the configured local model to summarize a deterministic triage bundle.""" + + base_url = _model_url() + if not base_url: + return _diagnosis_unavailable(bundle, "model_url_not_configured") + + try: + with httpx.Client(timeout=_model_timeout()) as client: + response = client.post( + f"{base_url}/api/generate", + json={ + "model": _model_name(), + "system": _DIAGNOSIS_SYSTEM_PROMPT, + "prompt": _diagnosis_prompt(bundle), + "stream": False, + "format": "json", + "options": { + "temperature": 0.1, + "top_p": 0.9, + }, + }, + ) + response.raise_for_status() + payload = response.json() + except Exception as exc: + return _diagnosis_unavailable(bundle, f"model_request_failed: {exc}") + + raw = str(payload.get("response") or "") + parsed, parse_error = _parse_model_response(raw) + return _diagnosis_from_model(bundle, parsed, raw, parse_error) + + +def _model_diagnosis_enabled() -> bool: + return bool(_model_url()) + + +def _model_url() -> str: + return str(getattr(settings, "testing_triage_model_url", "") or "").strip().rstrip("/") + + +def _model_name() -> str: + return str(getattr(settings, "testing_triage_model", "qwen2.5:7b-instruct-q4_0") or "").strip() + + +def _model_timeout() -> float: + return float(getattr(settings, "testing_triage_model_timeout_sec", 180.0) or 180.0) + + +def _diagnosis_prompt(bundle: dict[str, Any]) -> str: + payload = { + "task": "Summarize testing and cluster evidence for tonight's debugging work.", + "required_output_schema": { + "headline": "one sentence", + "root_cause": "most likely cause, or say evidence is insufficient", + "blast_radius": "affected suites, namespaces, pods, nodes, or services", + "confidence": "low|medium|high", + "needs_human": True, + "next_actions": ["short, concrete actions"], + "evidence_refs": ["specific evidence keys or values used"], + }, + "bundle": _model_evidence_payload(bundle), + } + evidence = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str) + if len(evidence) > _MAX_MODEL_EVIDENCE_CHARS: + evidence = evidence[:_MAX_MODEL_EVIDENCE_CHARS] + "\n[truncated]" + return evidence + + +def _model_evidence_payload(bundle: dict[str, Any]) -> dict[str, Any]: + summary = bundle.get("summary") if isinstance(bundle.get("summary"), dict) else {} + evidence = bundle.get("evidence") if isinstance(bundle.get("evidence"), dict) else {} + return { + "kind": bundle.get("kind"), + "generated_at": bundle.get("generated_at"), + "summary": summary, + "evidence": evidence, + "unknowns": bundle.get("unknowns") if isinstance(bundle.get("unknowns"), list) else [], + } + + +def _parse_model_response(raw: str) -> tuple[dict[str, Any], str | None]: + if not raw.strip(): + return {}, "empty_model_response" + try: + parsed = json.loads(raw) + except json.JSONDecodeError as exc: + return {}, f"model_json_parse_failed: {exc}" + return (parsed if isinstance(parsed, dict) else {}, None) + + +def _diagnosis_from_model( + bundle: dict[str, Any], + parsed: dict[str, Any], + raw: str, + parse_error: str | None, +) -> dict[str, Any]: + summary = bundle.get("summary") if isinstance(bundle.get("summary"), dict) else {} + unknowns = list(bundle.get("unknowns") or []) if isinstance(bundle.get("unknowns"), list) else [] + if parse_error: + unknowns.append(parse_error) + diagnosis = { + "headline": _text_value(parsed.get("headline"), "Testing triage needs review."), + "root_cause": _text_value(parsed.get("root_cause"), "Evidence is insufficient for a confident root cause."), + "blast_radius": _text_value(parsed.get("blast_radius"), _blast_radius_fallback(summary)), + "confidence": _confidence(parsed.get("confidence")), + "needs_human": _bool_value(parsed.get("needs_human"), bool(summary.get("problem_count"))), + "next_actions": _text_list(parsed.get("next_actions")) or _default_next_actions(summary), + "evidence_refs": _text_list(parsed.get("evidence_refs")) or _default_evidence_refs(summary), + } + return { + "kind": "testing_triage_diagnosis", + "generated_at": datetime.now(timezone.utc).isoformat(), + "evidence_generated_at": bundle.get("generated_at") or "", + "evidence_summary": summary, + "status": "needs_attention" if diagnosis["needs_human"] or summary.get("problem_count") else "ok", + "model": _model_name(), + "source": "local_ollama", + "diagnosis": diagnosis, + "openclaw": { + "ariadne_latest_url": "/api/internal/testing/triage/diagnosis/latest", + "ariadne_run_url": "/api/internal/testing/triage/diagnosis/run", + "evidence_url": "/api/internal/testing/triage/latest", + }, + "unknowns": unknowns, + "raw_model": raw[:_MAX_MODEL_OUTPUT_CHARS], + } + + +def _diagnosis_unavailable(bundle: dict[str, Any], reason: str) -> dict[str, Any]: + summary = bundle.get("summary") if isinstance(bundle.get("summary"), dict) else {} + return { + "kind": "testing_triage_diagnosis", + "generated_at": datetime.now(timezone.utc).isoformat(), + "evidence_generated_at": bundle.get("generated_at") or "", + "evidence_summary": summary, + "status": "unavailable", + "model": _model_name(), + "source": "local_ollama", + "diagnosis": { + "headline": "Local model diagnosis is unavailable.", + "root_cause": reason, + "blast_radius": _blast_radius_fallback(summary), + "confidence": "low", + "needs_human": True, + "next_actions": [ + "Use the stored evidence bundle for manual triage.", + "Verify the Ariadne testing triage model URL and OpenClaw Ollama service health.", + ], + "evidence_refs": _default_evidence_refs(summary), + }, + "openclaw": { + "ariadne_latest_url": "/api/internal/testing/triage/diagnosis/latest", + "ariadne_run_url": "/api/internal/testing/triage/diagnosis/run", + "evidence_url": "/api/internal/testing/triage/latest", + }, + "unknowns": [reason], + "raw_model": "", + } + + +def _text_value(value: Any, default: str) -> str: + if isinstance(value, str) and value.strip(): + return value.strip() + return default + + +def _text_list(value: Any) -> list[str]: + if isinstance(value, str) and value.strip(): + return [value.strip()] + if not isinstance(value, list): + return [] + return [str(item).strip() for item in value if str(item).strip()][:8] + + +def _confidence(value: Any) -> str: + confidence = str(value or "").strip().lower() + return confidence if confidence in {"low", "medium", "high"} else "low" + + +def _bool_value(value: Any, default: bool) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + lowered = value.strip().lower() + if lowered in {"true", "yes", "1"}: + return True + if lowered in {"false", "no", "0"}: + return False + return default + + +def _blast_radius_fallback(summary: dict[str, Any]) -> str: + failed_suites = summary.get("failed_suites") if isinstance(summary.get("failed_suites"), list) else [] + if failed_suites: + return ", ".join(str(item) for item in failed_suites[:6]) + return "No failed suite scope identified in the evidence bundle." + + +def _default_next_actions(summary: dict[str, Any]) -> list[str]: + if int(summary.get("problem_count") or 0) > 0: + return [ + "Review the evidence bundle sections with non-empty problem lists.", + "Check the named Jenkins build logs and Flux Kustomizations before changing manifests.", + ] + return ["No action required unless a fresh bundle changes the status."] + + +def _default_evidence_refs(summary: dict[str, Any]) -> list[str]: + refs = [f"summary.status={summary.get('status')}", f"summary.problem_count={summary.get('problem_count')}"] + failed_suites = summary.get("failed_suites") if isinstance(summary.get("failed_suites"), list) else [] + if failed_suites: + refs.append("summary.failed_suites=" + ",".join(str(item) for item in failed_suites[:6])) + return refs + + def _latest_cluster_snapshot(storage: Storage | None, errors: list[str]) -> dict[str, Any]: if storage is not None: try: diff --git a/ariadne/settings.py b/ariadne/settings.py index 9403b74..4322b3a 100644 --- a/ariadne/settings.py +++ b/ariadne/settings.py @@ -19,6 +19,7 @@ from .settings_sections import ( _portal_group_config, _schedule_config, _smtp_config, + _testing_triage_config, _vault_config, _vaultwarden_config, _wger_config, @@ -172,6 +173,9 @@ class Settings: jenkins_workspace_cleanup_min_age_hours: float jenkins_workspace_cleanup_dry_run: bool jenkins_workspace_cleanup_max_deletions_per_run: int + testing_triage_model_url: str + testing_triage_model: str + testing_triage_model_timeout_sec: float vaultwarden_namespace: str vaultwarden_pod_label: str @@ -264,6 +268,7 @@ class Settings: platform_quality_probe_cfg = _platform_quality_probe_config() jenkins_build_weather_cfg = _jenkins_build_weather_config() jenkins_workspace_cleanup_cfg = _jenkins_workspace_cleanup_config() + testing_triage_cfg = _testing_triage_config() vaultwarden_cfg = _vaultwarden_config() schedule_cfg = _schedule_config() cluster_cfg = _cluster_state_config() @@ -306,6 +311,7 @@ class Settings: **platform_quality_probe_cfg, **jenkins_build_weather_cfg, **jenkins_workspace_cleanup_cfg, + **testing_triage_cfg, **vaultwarden_cfg, **schedule_cfg, **cluster_cfg, diff --git a/ariadne/settings_sections.py b/ariadne/settings_sections.py index 7688c72..8cdb63b 100644 --- a/ariadne/settings_sections.py +++ b/ariadne/settings_sections.py @@ -242,6 +242,14 @@ def _jenkins_workspace_cleanup_config() -> dict[str, Any]: } +def _testing_triage_config() -> dict[str, Any]: + return { + "testing_triage_model_url": _env("ARIADNE_TESTING_TRIAGE_MODEL_URL", "").rstrip("/"), + "testing_triage_model": _env("ARIADNE_TESTING_TRIAGE_MODEL", "qwen2.5:7b-instruct-q4_0"), + "testing_triage_model_timeout_sec": _env_float("ARIADNE_TESTING_TRIAGE_MODEL_TIMEOUT_SEC", 180.0), + } + + def _vaultwarden_config() -> dict[str, Any]: return { "vaultwarden_namespace": _env("VAULTWARDEN_NAMESPACE", "vaultwarden"), diff --git a/tests/test_settings.py b/tests/test_settings.py index f43be5c..e5f8c46 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -34,6 +34,9 @@ def test_from_env_includes_jenkins_weather_settings(monkeypatch) -> None: monkeypatch.setenv("JENKINS_API_TIMEOUT_SEC", "8.5") monkeypatch.setenv("ARIADNE_SCHEDULE_JENKINS_BUILD_WEATHER", "*/9 * * * *") monkeypatch.setenv("ARIADNE_SCHEDULE_TESTING_TRIAGE", "*/11 * * * *") + monkeypatch.setenv("ARIADNE_TESTING_TRIAGE_MODEL_URL", "http://openclaw-ollama:11434/") + monkeypatch.setenv("ARIADNE_TESTING_TRIAGE_MODEL", "local-model") + monkeypatch.setenv("ARIADNE_TESTING_TRIAGE_MODEL_TIMEOUT_SEC", "33.5") cfg = Settings.from_env() assert cfg.jenkins_base_url == "https://ci.bstein.dev" @@ -42,3 +45,6 @@ def test_from_env_includes_jenkins_weather_settings(monkeypatch) -> None: assert cfg.jenkins_api_timeout_sec == 8.5 assert cfg.jenkins_build_weather_cron == "*/9 * * * *" assert cfg.testing_triage_cron == "*/11 * * * *" + assert cfg.testing_triage_model_url == "http://openclaw-ollama:11434" + assert cfg.testing_triage_model == "local-model" + assert cfg.testing_triage_model_timeout_sec == 33.5 diff --git a/tests/test_testing_triage.py b/tests/test_testing_triage.py index 7008880..4ab67dd 100644 --- a/tests/test_testing_triage.py +++ b/tests/test_testing_triage.py @@ -46,6 +46,9 @@ class SettingsStub: self.jenkins_api_user = "" self.jenkins_api_token = "" self.jenkins_api_timeout_sec = 1.0 + self.testing_triage_model_url = "" + self.testing_triage_model = "qwen2.5:7b-instruct-q4_0" + self.testing_triage_model_timeout_sec = 1.0 for key, value in overrides.items(): setattr(self, key, value) @@ -88,6 +91,26 @@ def test_run_testing_triage_stores_latest(monkeypatch) -> None: assert latest["summary"]["status"] == "ok" +def test_run_testing_triage_stores_diagnosis_when_model_enabled(monkeypatch) -> None: + storage = DummyStorage() + bundle = {"summary": {"status": "needs_attention", "problem_count": 1, "failed_suites": ["ariadne"]}} + diagnosis = {"kind": "testing_triage_diagnosis", "status": "needs_attention"} + + monkeypatch.setattr(testing_triage, "settings", SettingsStub(testing_triage_model_url="http://ollama")) + monkeypatch.setattr(testing_triage, "collect_testing_triage", lambda _storage: bundle) + monkeypatch.setattr(testing_triage, "diagnose_testing_triage", lambda _bundle: diagnosis) + + summary = testing_triage.run_testing_triage(storage) + latest = testing_triage.latest_testing_triage_diagnosis(storage) + + assert summary.status == "needs_attention" + assert [event[0] for event in storage.events] == [ + testing_triage.TRIAGE_EVENT_TYPE, + testing_triage.TRIAGE_DIAGNOSIS_EVENT_TYPE, + ] + assert latest == diagnosis + + 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] @@ -119,6 +142,107 @@ def test_latest_testing_triage_bundle_ignores_bad_payloads() -> None: assert testing_triage.latest_testing_triage_bundle(EmptyStorage()) is None # type: ignore[arg-type] +def test_diagnose_testing_triage_calls_local_ollama(monkeypatch) -> None: + captured = {} + model_response = json.dumps( + { + "headline": "Ariadne has one failing suite.", + "root_cause": "Jenkins reported a failed ariadne run.", + "blast_radius": "ariadne", + "confidence": "medium", + "needs_human": True, + "next_actions": ["Inspect the failed Jenkins build log."], + "evidence_refs": ["summary.failed_suites=ariadne"], + } + ) + + class FakeResponse: + def raise_for_status(self) -> None: + return None + + def json(self): # type: ignore[no-untyped-def] + return {"response": model_response} + + class FakeClient: + def __init__(self, *, timeout) -> None: # type: ignore[no-untyped-def] + captured["timeout"] = timeout + + def __enter__(self): + return self + + def __exit__(self, *args) -> None: # type: ignore[no-untyped-def] + return None + + def post(self, url, json=None): # type: ignore[no-untyped-def] + captured["url"] = url + captured["request"] = json + return FakeResponse() + + monkeypatch.setattr( + testing_triage, + "settings", + SettingsStub( + testing_triage_model_url="http://ollama/", + testing_triage_model="tiny-model", + testing_triage_model_timeout_sec=3.0, + ), + ) + monkeypatch.setattr(testing_triage.httpx, "Client", FakeClient) + + diagnosis = testing_triage.diagnose_testing_triage( + { + "kind": "testing_triage_bundle", + "generated_at": "now", + "summary": {"status": "needs_attention", "problem_count": 1, "failed_suites": ["ariadne"]}, + "evidence": {"jenkins": {"failed_builds": [{"job": "ariadne"}]}}, + "unknowns": [], + } + ) + + assert captured["url"] == "http://ollama/api/generate" + assert captured["timeout"] == 3.0 + assert captured["request"]["model"] == "tiny-model" + assert captured["request"]["format"] == "json" + assert diagnosis["kind"] == "testing_triage_diagnosis" + assert diagnosis["status"] == "needs_attention" + assert diagnosis["diagnosis"]["confidence"] == "medium" + assert diagnosis["diagnosis"]["next_actions"] == ["Inspect the failed Jenkins build log."] + + +def test_diagnose_testing_triage_handles_disabled_and_bad_json(monkeypatch) -> None: + monkeypatch.setattr(testing_triage, "settings", SettingsStub(testing_triage_model_url="")) + disabled = testing_triage.diagnose_testing_triage({"summary": {"status": "ok", "problem_count": 0}}) + assert disabled["status"] == "unavailable" + assert disabled["diagnosis"]["root_cause"] == "model_url_not_configured" + + class BadResponse: + def raise_for_status(self) -> None: + return None + + def json(self): # type: ignore[no-untyped-def] + return {"response": "not json"} + + class BadClient: + 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 post(self, url, json=None): # type: ignore[no-untyped-def] + return BadResponse() + + monkeypatch.setattr(testing_triage, "settings", SettingsStub(testing_triage_model_url="http://ollama")) + monkeypatch.setattr(testing_triage.httpx, "Client", BadClient) + diagnosis = testing_triage.diagnose_testing_triage({"summary": {"status": "ok", "problem_count": 0}}) + + assert diagnosis["status"] == "ok" + assert "model_json_parse_failed" in diagnosis["unknowns"][0] + + def test_latest_cluster_snapshot_falls_back_to_live_collect(monkeypatch) -> None: class BrokenStorage: def latest_cluster_state(self): # type: ignore[no-untyped-def] diff --git a/tests/unit/app/test_app_admin_routes.py b/tests/unit/app/test_app_admin_routes.py index 2d9d32e..ecc79f2 100644 --- a/tests/unit/app/test_app_admin_routes.py +++ b/tests/unit/app/test_app_admin_routes.py @@ -217,10 +217,13 @@ def test_testing_triage_routes(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) bundle = {"kind": "testing_triage_bundle", "summary": {"status": "ok"}} + diagnosis = {"kind": "testing_triage_diagnosis", "status": "ok"} recorded = [] monkeypatch.setattr(app_module, "latest_testing_triage_bundle", lambda _storage: bundle) monkeypatch.setattr(app_module, "collect_testing_triage", lambda _storage: bundle) + monkeypatch.setattr(app_module, "latest_testing_triage_diagnosis", lambda _storage: diagnosis) + monkeypatch.setattr(app_module, "run_testing_triage_diagnosis", lambda _storage: diagnosis) monkeypatch.setattr(app_module.storage, "record_event", lambda event, detail: recorded.append((event, detail))) admin_latest = client.get( @@ -228,31 +231,53 @@ def test_testing_triage_routes(monkeypatch) -> None: headers={"Authorization": "Bearer token"}, ) internal_latest = client.get("/api/internal/testing/triage/latest") + admin_diagnosis_latest = client.get( + "/api/admin/testing/triage/diagnosis/latest", + headers={"Authorization": "Bearer token"}, + ) + internal_diagnosis_latest = client.get("/api/internal/testing/triage/diagnosis/latest") admin_collect = client.post( "/api/admin/testing/triage/collect", headers={"Authorization": "Bearer token"}, ) internal_collect = client.post("/api/internal/testing/triage/collect") + admin_diagnosis_run = client.post( + "/api/admin/testing/triage/diagnosis/run", + headers={"Authorization": "Bearer token"}, + ) + internal_diagnosis_run = client.post("/api/internal/testing/triage/diagnosis/run") assert admin_latest.status_code == 200 assert internal_latest.status_code == 200 + assert admin_diagnosis_latest.status_code == 200 + assert internal_diagnosis_latest.status_code == 200 assert admin_collect.status_code == 200 assert internal_collect.status_code == 200 + assert admin_diagnosis_run.status_code == 200 + assert internal_diagnosis_run.status_code == 200 assert recorded[0][0] == app_module.TRIAGE_EVENT_TYPE def test_testing_triage_latest_unavailable(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={}) client = _client(monkeypatch, ctx) monkeypatch.setattr(app_module, "latest_testing_triage_bundle", lambda _storage: None) + monkeypatch.setattr(app_module, "latest_testing_triage_diagnosis", lambda _storage: None) admin_resp = client.get( "/api/admin/testing/triage/latest", headers={"Authorization": "Bearer token"}, ) internal_resp = client.get("/api/internal/testing/triage/latest") + admin_diagnosis = client.get( + "/api/admin/testing/triage/diagnosis/latest", + headers={"Authorization": "Bearer token"}, + ) + internal_diagnosis = client.get("/api/internal/testing/triage/diagnosis/latest") assert admin_resp.status_code == 404 assert internal_resp.status_code == 404 + assert admin_diagnosis.status_code == 404 + assert internal_diagnosis.status_code == 404 def test_access_request_approve(monkeypatch) -> None: ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})