"""Testy hybrid routingu — AI klasifikace + fallbacky.""" import pytest @pytest.fixture() def hybrid(app_module): """App v hybrid módu s 'ready' enginem (bez reálného subprocessu).""" import engine as engine_module app_module.SETTINGS.update({"agent_mode": "hybrid", "router_mode": "ai"}) app_module.ENGINE.state = engine_module.EngineState.READY return app_module def test_explicit_tags_override_ai(hybrid, monkeypatch): # tagy mají absolutní přednost — AI router se vůbec nesmí volat def _boom(messages): raise AssertionError("router se nemá volat") monkeypatch.setattr(hybrid, "_router_llm_call", _boom) assert hybrid._route_task("[kimi] narhni schéma") == "api" assert hybrid._route_task("@api udělej X") == "api" assert hybrid._route_task("[local] oprav překlep") == "local" assert hybrid._route_task("[qwen] oprav překlep") == "local" def test_context_limit_overrides_ai(hybrid, monkeypatch): monkeypatch.setattr(hybrid, "_router_llm_call", lambda m: (_ for _ in ()).throw(AssertionError)) hybrid.SETTINGS.update({"local_context_limit": 100}) assert hybrid._route_task("x" * 200) == "api" def test_ai_router_decides(hybrid, monkeypatch): calls = [] def fake_llm(messages): calls.append(messages) return "API" monkeypatch.setattr(hybrid, "_router_llm_call", fake_llm) assert hybrid._route_task("oprav mi tenhle test") == "api" # system prompt + user zpráva assert calls[0][0]["role"] == "system" assert "LOCAL or API" in calls[0][0]["content"] monkeypatch.setattr(hybrid, "_router_llm_call", lambda m: "LOCAL") assert hybrid._route_task("oprav mi tenhle test") == "local" def test_ai_router_parses_verbose_answers(hybrid, monkeypatch): monkeypatch.setattr(hybrid, "_router_llm_call", lambda m: "Odpověď: LOCAL — jde o rutinní úpravu.") assert hybrid._route_task("přejmenuj proměnnou") == "local" # obě slova v odpovědi => rozhoduje první výskyt monkeypatch.setattr(hybrid, "_router_llm_call", lambda m: "API (not LOCAL) because of architecture") assert hybrid._route_task("běžná otázka") == "api" def test_ai_router_garbage_falls_back_to_keywords(hybrid, monkeypatch): monkeypatch.setattr(hybrid, "_router_llm_call", lambda m: "žblepty 42") assert hybrid._route_task("proveď komplexní analýza modulu") == "api" # keyword assert hybrid._route_task("oprav překlep v readme") == "local" def test_ai_router_exception_falls_back(hybrid, monkeypatch): def _fail(messages): raise RuntimeError("engine timeout") monkeypatch.setattr(hybrid, "_router_llm_call", _fail) assert hybrid._route_task("navrhni architektura systému") == "api" # keyword assert hybrid._route_task("vypiš soubory") == "local" def test_engine_not_ready_falls_back(hybrid, monkeypatch): import engine as engine_module hybrid.ENGINE.state = engine_module.EngineState.STOPPED monkeypatch.setattr(hybrid, "_router_llm_call", lambda m: (_ for _ in ()).throw(AssertionError)) assert hybrid._route_task("root cause tohoto pádu") == "api" # keyword assert hybrid._route_task("malý fix") == "local" def test_keywords_mode_skips_ai(hybrid, monkeypatch): hybrid.SETTINGS.update({"router_mode": "keywords"}) monkeypatch.setattr(hybrid, "_router_llm_call", lambda m: (_ for _ in ()).throw(AssertionError)) assert hybrid._route_task("security audit") == "api" assert hybrid._route_task("napiš unit test") == "local" def test_router_mode_validation(settings_module): mgr = settings_module.SettingsManager() assert mgr.get().router_mode == "ai" # default applied, reload_needed, _ = mgr.update({"router_mode": "keywords"}) assert applied == {"router_mode": "keywords"} and reload_needed is False # case-insensitive normalizace applied, _, errors = mgr.update({"router_mode": "AI"}) assert applied == {"router_mode": "ai"} and errors == {} _, _, errors = mgr.update({"router_mode": "nesmysl"}) assert "router_mode" in errors def test_new_default_model_and_presets(app_module): from presets import PRESETS, infer_parsers assert app_module.SETTINGS.get().model == "Qwen/Qwen3-Coder-Next-FP8" assert "qwen3-coder-next-fp8" in PRESETS assert "minimax-m2.7" in PRESETS assert "minimax-m2" not in PRESETS # parser rodiny: Coder-Next dědí qwen3_coder, Devstral -> mistral assert infer_parsers("Qwen/Qwen3-Coder-Next-FP8") == ("qwen3_coder", "") assert infer_parsers("mistralai/Devstral-2-123B-Instruct-2512") == ("mistral", "") assert infer_parsers("MiniMaxAI/MiniMax-M2.7") == ("minimax_m2", "minimax_m2_append_think")