"""Judge backend selection + transformers-fallback — no GPU, no downloads.""" import importlib import formscout.config as config from formscout.serving import get_vlm_client from formscout.serving.llama_cpp import LlamaCppClient def _reload_config(monkeypatch, **env): for k, v in env.items(): if v is None: monkeypatch.delenv(k, raising=False) else: monkeypatch.setenv(k, v) importlib.reload(config) return config def test_resolve_backend_default_local(monkeypatch): cfg = _reload_config(monkeypatch, FORMSCOUT_JUDGE_BACKEND=None, SPACE_ID=None) assert cfg.resolve_judge_backend() == "llama_cpp" def test_resolve_backend_auto_on_zero_gpu_space(monkeypatch): cfg = _reload_config(monkeypatch, FORMSCOUT_JUDGE_BACKEND="auto", SPACE_ID="me/space", SPACES_ZERO_GPU="true") assert cfg.resolve_judge_backend() == "transformers" importlib.reload(config) def test_resolve_backend_auto_on_cpu_space_stays_llama(monkeypatch): # A CPU-only Space must NOT load the 17 GB transformers model. cfg = _reload_config(monkeypatch, FORMSCOUT_JUDGE_BACKEND="auto", SPACE_ID="me/space", SPACES_ZERO_GPU=None, ZERO_GPU=None) assert cfg.resolve_judge_backend() == "llama_cpp" importlib.reload(config) def test_resolve_backend_explicit(monkeypatch): cfg = _reload_config(monkeypatch, FORMSCOUT_JUDGE_BACKEND="llama_cpp", SPACE_ID="me/space") assert cfg.resolve_judge_backend() == "llama_cpp" importlib.reload(config) # restore def test_factory_returns_llama_cpp_locally(monkeypatch): _reload_config(monkeypatch, FORMSCOUT_JUDGE_BACKEND="llama_cpp", SPACE_ID=None) client = get_vlm_client() assert isinstance(client, LlamaCppClient) importlib.reload(config) def test_transformers_client_available_is_cheap_bool(): # available must not load/download the model — just report a bool from formscout.serving.transformers_vlm import TransformersVLMClient c = TransformersVLMClient() assert isinstance(c.available, bool) c._failed = True assert c.available is False def test_judge_uses_rubric_on_fallback_sentinel(): from formscout.agents.judge import JudgeAgent from formscout.types import BiomechFeatures, ScoreResult, MovementResult, IngestResult import numpy as np agent = JudgeAgent() class _FakeClient: available = True def complete(self, *a, **k): return {"error": "no gpu", "fallback": True, "text": ""} agent._client = _FakeClient() features = BiomechFeatures(test_name="deep_squat", view="2d", side="na", angles={}, alignments={}, symmetry_delta=None, timing={}, confidence=0.9) rubric = ScoreResult(score=2, rationale="rubric says 2", confidence=0.8) movement = MovementResult(test_name="deep_squat", side="na", confidence=1.0) ingest = IngestResult(frames=[np.zeros((10, 10, 3), np.uint8)], fps=30.0, duration=0.03, n_people=1, width=10, height=10) res = agent.run(features, rubric, movement, ingest) assert res.score == 2 assert "rubric-only" in res.rationale