"""„Jetzt suchen“ im Radar als Auftrag (24.09.2026): Die Route antwortet sofort mit einer Auftrags-ID, gesucht wird in radar_lauf.py --nur-suche. Vorher lief die Suche synchron in der Anfrage und konnte Minuten dauern.""" import sys from datetime import datetime import pytest import radar_lauf from fastapi import FastAPI from fastapi.testclient import TestClient from routers import radar as radar_router from services import radar @pytest.fixture def auftraege(monkeypatch): """Job-Engine-Attrappe: merkt sich den Start; läuft schon einer, kommt der zurück.""" stand: dict = {"gestartet": [], "laeuft": None} def start_job_exklusiv(gruppe, args, label, **kw): if stand["laeuft"]: return None, stand["laeuft"] stand["gestartet"].append({"gruppe": gruppe, "args": args, "label": label, **kw}) return "job123", None monkeypatch.setattr(radar.jobengine, "start_job_exklusiv", start_job_exklusiv) return stand def test_suche_startet_einen_auftrag(auftraege): ergebnis = radar.suche_starten() assert ergebnis["ok"] is True and ergebnis["job_id"] == "job123" and "Aufträge" in ergebnis["text"] (start,) = auftraege["gestartet"] assert start["gruppe"] == "radar" and start["zeitlimit_s"] == radar.SUCHE_ZEITLIMIT_S assert start["args"][0] == sys.executable assert start["args"][1].endswith("radar_lauf.py") and start["args"][2:] == ["--nur-suche"] def test_laufende_suche_wird_nicht_doppelt_gestartet(auftraege): auftraege["laeuft"] = {"id": "job-alt", "state": "running"} ergebnis = radar.suche_starten() assert (ergebnis["job_id"], ergebnis["laeuft_schon"]) == ("job-alt", True) assert auftraege["gestartet"] == [] def test_route_antwortet_sofort_ohne_selbst_zu_suchen(auftraege, monkeypatch): def nicht_hier(): raise AssertionError("Die Route darf nicht selbst suchen.") monkeypatch.setattr(radar, "suche", nicht_hier) app = FastAPI() app.include_router(radar_router.router) antwort = TestClient(app).post("/api/radar/suche") assert antwort.status_code == 200 and antwort.json()["job_id"] == "job123" @pytest.mark.parametrize("ergebnis, code, zeile", [ ({"neu": ["Ornith 1.5"], "wartend": 2, "quellen": {"watchlist": True, "discover": True}}, 0, "Suche fertig: 1 neu (Ornith 1.5), 2 warten auf den Nachttest."), ({"neu": [], "wartend": 1, "quellen": {"watchlist": True, "discover": False}}, 0, "Suche fertig: nichts Neues, 1 warten auf den Nachttest, Hugging Face war nicht erreichbar."), ({"neu": [], "wartend": 0, "quellen": {"watchlist": False, "discover": False}}, 1, ("Suche fertig: nichts Neues, 0 warten auf den Nachttest, Hugging Face war nicht erreichbar, " "die Merkliste war nicht lesbar.")), ]) def test_nur_suche_sagt_das_ergebnis_in_der_letzten_zeile(monkeypatch, capsys, ergebnis, code, zeile): monkeypatch.setattr(radar_lauf.radar, "suche", lambda: ergebnis) assert radar_lauf.nur_suche() == code assert capsys.readouterr().out.strip().splitlines()[-1] == zeile def test_nur_suche_bei_absturz(monkeypatch, capsys): def kaputt(): raise OSError("Platte voll") monkeypatch.setattr(radar_lauf.radar, "suche", kaputt) assert radar_lauf.nur_suche() == 1 assert capsys.readouterr().out.strip().endswith("Die Suche ist gescheitert: OSError: Platte voll") # --- „Jetzt testen“ (seit 25.09.2026) ------------------------------------------------------------------------ @pytest.fixture def radarstand(monkeypatch, tmp_path): monkeypatch.setattr(radar, "STORE_PATH", tmp_path / "mc2-radar.json") monkeypatch.setattr(radar, "RADAR_DIR", tmp_path / "radar") (tmp_path / "radar").mkdir() stand = radar._leer() # Zahlen vom 25.09.: Ornith (~27 GB mit KV-Cache) passt tagsüber, Flash-Next (84,6 GB) nur nachts. stand["kandidaten"]["ornith"] = {"id": "ornith", "name": "Ornith-1.5-35B-A3B", "rolle": "hirn", "status": "neu", "passt": True, "eng": False, "bedarf_gb": 27.4, "groesse_gb": 22.6} stand["kandidaten"]["flash"] = {"id": "flash", "name": "Qwen3.8-Flash-Next", "rolle": "coder", "status": "wartet", "passt": True, "eng": True, "bedarf_gb": 84.6, "groesse_gb": 82.9} stand["kandidaten"]["alt"] = {"id": "alt", "name": "Alt", "rolle": "hirn", "status": "durchgefallen", "passt": True, "bedarf_gb": 20.0} radar._speichere(stand) return stand MITTAG = datetime(2026, 9, 25, 13, 0, tzinfo=radar.LOCAL_TZ) NACHT = datetime(2026, 9, 26, 1, 15, tzinfo=radar.LOCAL_TZ) def test_jetzt_testbar_nur_wenn_auch_der_coder_noch_passt(radarstand): k = radarstand["kandidaten"] assert radar.jetzt_testbar(k["ornith"], MITTAG) == (True, None) testbar, grund = radar.jetzt_testbar(k["flash"], MITTAG) assert not testbar and grund.startswith("Zu groß für einen Test tagsüber: braucht ~84,6 GB") assert radar.jetzt_testbar(k["alt"], MITTAG) == (False, None) # schon beurteilt testbar, grund = radar.jetzt_testbar(k["ornith"], NACHT) assert not testbar and "testet das Radar selbst" in grund oeffentlich = {x["id"]: x for x in radar.uebersicht(MITTAG)["kandidaten"]} assert oeffentlich["ornith"]["testbar_jetzt"] is True and oeffentlich["flash"]["testbar_jetzt"] is False def test_jetzt_testen_startet_einen_auftrag(radarstand, auftraege, monkeypatch): monkeypatch.setattr(radar, "jetzt", lambda: MITTAG) ergebnis = radar.test_starten("ornith") assert ergebnis["ok"] and ergebnis["job_id"] == "job123" start = auftraege["gestartet"][0] assert start["gruppe"] == "radar" and start["args"][1:] == [str(radar._LAUF_SKRIPT), "--test", "ornith"] assert start["label"] == "Modell-Radar: Test von Ornith-1.5-35B-A3B" zu_gross = radar.test_starten("flash") assert zu_gross["ok"] is False and zu_gross["status"] == 409 and "Zu groß" in zu_gross["detail"] assert radar.test_starten("gibt-es-nicht")["status"] == 404 app = FastAPI() app.include_router(radar_router.router) antwort = TestClient(app).post("/api/radar/flash/testen") assert antwort.status_code == 409 and "Zu groß" in antwort.json()["detail"]