"""Tests for the ensure_ollama orchestration (Ollama mocked via injected ops).""" import json as _json from types import SimpleNamespace from typer.testing import CliRunner from tht.config import EmbeddingsConfig from tht.cli import ollama_cmd from tht.cli.ollama_cmd import ensure_ollama, ollama_app def _cfg(**kw): emb = EmbeddingsConfig(base_url="http://localhost:11434", **kw) return SimpleNamespace(embeddings=emb) def test_no_embeddings_config_is_hard_error(): r = ensure_ollama(SimpleNamespace(embeddings=None), timeout=5, no_start=False) assert r["ok"] is False and r["stage"] == "config" def test_server_up_model_present_warms_ok(): warmed = [] r = ensure_ollama( _cfg(model="nomic-embed-text-v2-moe"), timeout=5, no_start=False, probe=lambda url: True, installed_models=lambda url: {"nomic-embed-text-v2-moe:latest"}, start=lambda cmd: (_ for _ in ()).throw(AssertionError("must not start")), warm=lambda cfg: warmed.append(True), ) assert r == {"ok": True, "server": "up", "model": "warmed", "model_name": "nomic-embed-text-v2-moe"} assert warmed == [True] def test_server_down_then_started_after_poll(): started = [] probes = iter([False, True]) # down, then up after start r = ensure_ollama( _cfg(), timeout=5, no_start=False, probe=lambda url: next(probes), installed_models=lambda url: {"nomic-embed-text-v2-moe"}, start=lambda cmd: started.append(cmd), warm=lambda cfg: None, sleep=lambda s: None, ) assert r["ok"] is True and r["server"] == "started" assert started and started[0] == ["ollama", "serve"] def test_server_unreachable_after_timeout_is_error(): clk = iter([0.0, 1.0, 2.0, 99.0]) # monotonic crosses the deadline r = ensure_ollama( _cfg(), timeout=5, no_start=False, probe=lambda url: False, # never comes up installed_models=lambda url: set(), start=lambda cmd: None, warm=lambda cfg: None, sleep=lambda s: None, clock=lambda: next(clk), ) assert r["ok"] is False and r["stage"] == "server" def test_no_start_and_down_is_error_without_starting(): r = ensure_ollama( _cfg(), timeout=5, no_start=True, probe=lambda url: False, installed_models=lambda url: set(), start=lambda cmd: (_ for _ in ()).throw(AssertionError("must not start")), warm=lambda cfg: None, ) assert r["ok"] is False and r["stage"] == "server" def test_empty_start_cmd_disables_autostart(): r = ensure_ollama( _cfg(start_cmd=[]), timeout=5, no_start=False, probe=lambda url: False, installed_models=lambda url: set(), start=lambda cmd: (_ for _ in ()).throw(AssertionError("must not start")), warm=lambda cfg: None, ) assert r["ok"] is False and r["stage"] == "server" def test_model_absent_is_error_with_pull_guidance(): r = ensure_ollama( _cfg(model="missing-model"), timeout=5, no_start=False, probe=lambda url: True, installed_models=lambda url: {"nomic-embed-text-v2-moe"}, start=lambda cmd: None, warm=lambda cfg: None, ) assert r["ok"] is False and r["stage"] == "model" assert "ollama pull missing-model" in r["error"] def test_warm_failure_is_error(): r = ensure_ollama( _cfg(), timeout=5, no_start=False, probe=lambda url: True, installed_models=lambda url: {"nomic-embed-text-v2-moe"}, start=lambda cmd: None, warm=lambda cfg: (_ for _ in ()).throw(RuntimeError("boom")), ) assert r["ok"] is False and r["stage"] == "warm" def test_custom_start_cmd_used(): started = [] probes = iter([False, True]) ensure_ollama( _cfg(bin="ollama", start_cmd=["docker", "start", "ollama"]), timeout=5, no_start=False, probe=lambda url: next(probes), installed_models=lambda url: {"nomic-embed-text-v2-moe"}, start=lambda cmd: started.append(cmd), warm=lambda cfg: None, sleep=lambda s: None, ) assert started[0] == ["docker", "start", "ollama"] def test_start_failure_is_server_error(): r = ensure_ollama( _cfg(), timeout=5, no_start=False, probe=lambda url: False, installed_models=lambda url: set(), start=lambda cmd: (_ for _ in ()).throw(FileNotFoundError("ollama: not found")), warm=lambda cfg: None, sleep=lambda s: None, ) assert r["ok"] is False and r["stage"] == "server" assert "avvio" in r["error"].lower() or "not found" in r["error"].lower() def test_initial_probe_exception_is_server_error(): r = ensure_ollama( _cfg(), timeout=5, no_start=False, probe=lambda url: (_ for _ in ()).throw(RuntimeError("boom")), installed_models=lambda url: set(), start=lambda cmd: None, warm=lambda cfg: None, ) assert r["ok"] is False and r["stage"] == "server" def _patch(monkeypatch, result): monkeypatch.setattr(ollama_cmd, "_load_config_or_exit", lambda _c: SimpleNamespace(embeddings=object())) monkeypatch.setattr(ollama_cmd, "ensure_ollama", lambda cfg, **kw: result) def test_cli_ok_exit_zero_and_json_pristine(monkeypatch): _patch(monkeypatch, {"ok": True, "server": "up", "model": "warmed", "model_name": "m"}) res = CliRunner().invoke(ollama_app, ["--json"]) assert res.exit_code == 0, res.output assert _json.loads(res.output) == {"ok": True, "server": "up", "model": "warmed", "model_name": "m"} def test_cli_error_exit_one_and_json_on_stdout(monkeypatch): _patch(monkeypatch, {"ok": False, "stage": "model", "error": "missing"}) res = CliRunner().invoke(ollama_app, ["--json"]) assert res.exit_code == 1 assert _json.loads(res.output) == {"ok": False, "stage": "model", "error": "missing"} def test_cli_error_human_mode_exit_one(monkeypatch): _patch(monkeypatch, {"ok": False, "stage": "server", "error": "down"}) res = CliRunner().invoke(ollama_app, []) assert res.exit_code == 1 def test_cli_registered_on_root_app(): from tht.cli import app # the root Typer app runner = CliRunner() res = runner.invoke(app, ["ollama", "--help"]) assert res.exit_code == 0 assert "ensure" in res.output def test_cli_root_app_ensure_end_to_end(monkeypatch): from tht.cli import app # root tht app _patch(monkeypatch, {"ok": True, "server": "up", "model": "warmed", "model_name": "m"}) res = CliRunner().invoke(app, ["ollama", "ensure", "--json"]) assert res.exit_code == 0, res.output assert _json.loads(res.output) == {"ok": True, "server": "up", "model": "warmed", "model_name": "m"}