"""Extra real-behavior tests for adapters.inference.brain_api_adapter. Complements tests/adapters/test_brain_api_adapter_v2.py to raise coverage of the BrainAPIAdapter. Every HTTP call is mocked at the MODULE namespace (`safe_http_request` / `httpx`); no real network or sleeps. Assertions check the REAL request body + headers built and the REAL parsed response — not bare mock-call counts. """ import base64 from unittest.mock import MagicMock, patch import httpx import pytest from adapters.inference.brain_api_adapter import BrainAPIAdapter from core.domain.entities.ai_schemas import InferenceResponse from core.domain.exceptions import ConfigurationError, InferenceError from core.ports.inference_port import InferenceNotImplementedError MODULE = "adapters.inference.brain_api_adapter.safe_http_request" def _resp(payload, status=200): r = MagicMock() r.status_code = status r.json.return_value = payload return r @pytest.fixture(autouse=True) def _clean_brain_env(monkeypatch): # The repo `.env` defines BRAIN_API_URL/BRAIN_API_KEY, and the adapter falls # back to them when api_url/api_key are None. Clear both for every test so the # "url/key missing" cases stay deterministic regardless of ambient env / # whichever earlier test loaded .env into os.environ. monkeypatch.delenv("BRAIN_API_URL", raising=False) monkeypatch.delenv("BRAIN_API_KEY", raising=False) @pytest.fixture def adapter(): return BrainAPIAdapter(api_url="http://brain:5000", api_key="dev-secret-key") @pytest.fixture def adapter_no_key(monkeypatch): # Ensure no ambient BRAIN_API_KEY leaks in (the adapter falls back to the # env var when api_key=None), so the "empty headers" assertion is deterministic. monkeypatch.delenv("BRAIN_API_KEY", raising=False) return BrainAPIAdapter(api_url="http://brain:5000", api_key=None) # --- generate: usage logging, payload, errors -------------------------------- def test_generate_logs_usage_and_builds_payload(): usage_port = MagicMock() a = BrainAPIAdapter(api_url="http://brain:5000", api_key="k", usage_port=usage_port) with patch(MODULE) as req: req.return_value = _resp( { "text": "ok", "usage": {"prompt_tokens": 11, "completion_tokens": 7}, "thinking": "reasoned", } ) res = a.generate( "Q", system_prompt="SYS", thinking_budget=64, thinking_mode=True, temperature=0.3, # extra kwarg flows into payload ) # Parsed response carries text + metadata. assert res.text == "ok" assert res.metadata.thinking == "reasoned" assert res.metadata.usage == {"prompt_tokens": 11, "completion_tokens": 7} # Real request shape. method, url = req.call_args.args kwargs = req.call_args.kwargs assert method == "POST" assert url == "http://brain:5000/generate" assert kwargs["headers"] == {"X-API-Key": "k"} assert kwargs["json"] == { "prompt": "Q", "system_prompt": "SYS", "thinking_budget": 64, "thinking_mode": True, "include_logprobs": False, "temperature": 0.3, } # Usage logged with the allocated budget passed through. usage_port.log_usage.assert_called_once() log_kwargs = usage_port.log_usage.call_args assert log_kwargs.args[0] == "brain:api" assert log_kwargs.args[1] == 11 # input tokens assert log_kwargs.args[2] == 7 # output tokens assert log_kwargs.kwargs["allocated_budget"] == 64 def test_generate_raises_when_url_missing(): with pytest.raises(ConfigurationError, match="BRAIN_API_URL is not configured"): BrainAPIAdapter(api_url=None, api_key="k") def test_init_raises_on_malformed_url(): # Now routed through check_brain_config: a present-but-malformed URL is caught. with pytest.raises(ConfigurationError, match="malformed"): BrainAPIAdapter(api_url="not-a-url", api_key="k") def test_generate_reraises_http_error(adapter): with patch(MODULE, side_effect=RuntimeError("boom")): with pytest.raises(RuntimeError, match="boom"): adapter.generate("Q") def test_generate_handles_missing_usage_block(adapter): # No "usage" key -> tokens default to 0, no KeyError. with patch(MODULE) as req: req.return_value = _resp({"text": "bare"}) res = adapter.generate("Q") assert res.text == "bare" assert res.metadata.logprobs is None def test_generate_without_api_key_sends_empty_headers(adapter_no_key): with patch(MODULE) as req: req.return_value = _resp({"text": "x"}) adapter_no_key.generate("Q") assert req.call_args.kwargs["headers"] == {} def test_generate_pins_the_configured_model_in_the_payload(): a = BrainAPIAdapter(api_url="http://brain:5000", api_key="k", model="small:1.5b") with patch("adapters.inference.brain_api_adapter.safe_http_request") as req: req.return_value = MagicMock(json=MagicMock(return_value={"text": "ok"})) a.generate("Q") assert req.call_args.kwargs["json"]["model"] == "small:1.5b" def test_generate_omits_the_model_when_none_is_pinned(adapter): with patch("adapters.inference.brain_api_adapter.safe_http_request") as req: req.return_value = MagicMock(json=MagicMock(return_value={"text": "ok"})) adapter.generate("Q") assert "model" not in req.call_args.kwargs["json"] # --- stream_generate --------------------------------------------------------- def test_stream_generate_raises_when_url_missing(): with pytest.raises(ConfigurationError, match="BRAIN_API_URL is not configured"): BrainAPIAdapter(api_url=None, api_key="k") def test_stream_generate_builds_payload_and_yields_all_chunks(adapter): with patch("adapters.inference.brain_api_adapter.httpx.stream") as stream: res = MagicMock() res.iter_text.return_value = ["a", "b", "c"] stream.return_value.__enter__.return_value = res chunks = list(adapter.stream_generate("Q", thinking_mode=True)) assert [c.text for c in chunks] == ["a", "b", "c"] assert all(isinstance(c, InferenceResponse) for c in chunks) # Endpoint + payload built correctly. args, kwargs = stream.call_args assert args == ("POST", "http://brain:5000/stream_generate") assert kwargs["json"]["thinking_mode"] is True assert kwargs["headers"] == {"X-API-Key": "dev-secret-key"} res.raise_for_status.assert_called_once() def test_stream_generate_reraises_http_status_error(adapter): with patch("adapters.inference.brain_api_adapter.httpx.stream") as stream: res = MagicMock() res.raise_for_status.side_effect = httpx.HTTPStatusError( "bad", request=MagicMock(), response=MagicMock() ) stream.return_value.__enter__.return_value = res with pytest.raises(httpx.HTTPStatusError): list(adapter.stream_generate("Q")) def test_stream_generate_reraises_request_error(adapter): with patch("adapters.inference.brain_api_adapter.httpx.stream") as stream: res = MagicMock() res.raise_for_status.side_effect = httpx.RequestError("net down") stream.return_value.__enter__.return_value = res with pytest.raises(httpx.RequestError): list(adapter.stream_generate("Q")) def test_stream_generate_reraises_generic_error(adapter): with patch("adapters.inference.brain_api_adapter.httpx.stream") as stream: stream.side_effect = ValueError("weird") with pytest.raises(ValueError, match="weird"): list(adapter.stream_generate("Q")) # --- text embedding ---------------------------------------------------------- def test_get_text_embedding_parses_and_logs(): usage_port = MagicMock() a = BrainAPIAdapter(api_url="http://brain:5000", api_key="k", usage_port=usage_port) with patch(MODULE) as req: req.return_value = _resp({"embedding": [0.1, 0.2, 0.3]}) out = a.get_text_embedding("hello") assert out == [0.1, 0.2, 0.3] method, url = req.call_args.args assert url == "http://brain:5000/v1/embeddings" assert req.call_args.kwargs["json"] == {"text": "hello"} usage_port.log_usage.assert_called_once_with( "brain:embeddings", 0, 0, 1, allocated_budget=0 ) def test_get_text_embedding_reraises(adapter): with patch(MODULE, side_effect=RuntimeError("x")): with pytest.raises(RuntimeError): adapter.get_text_embedding("hello") # --- image generation / sprite ---------------------------------------------- def test_generate_image_returns_url(adapter): with patch(MODULE) as req: req.return_value = _resp({"image_url_or_b64": "http://img/1.png"}) out = adapter.generate_image("a cat", style="ghibli") assert out == "http://img/1.png" assert req.call_args.args[1] == "http://brain:5000/vision/generate" assert req.call_args.kwargs["json"] == {"prompt": "a cat", "style": "ghibli"} def test_generate_sprite_wraps_prompt_and_delegates(adapter): with patch(MODULE) as req: req.return_value = _resp({"image_url_or_b64": "sprite.png"}) out = adapter.generate_sprite("a knight", style="cel") assert out == "sprite.png" sent_prompt = req.call_args.kwargs["json"]["prompt"] assert "character sprite" in sent_prompt assert "a knight" in sent_prompt def test_generate_sprite_failure_raises_not_implemented(adapter): with patch(MODULE, side_effect=RuntimeError("down")): with pytest.raises(InferenceNotImplementedError) as ei: adapter.generate_sprite("a knight") assert "down" in str(ei.value) # --- image embedding (incl. no-URL early return) ----------------------------- def test_get_image_embedding_raises_without_url(): with pytest.raises(ConfigurationError, match="BRAIN_API_URL is not configured"): BrainAPIAdapter(api_url=None, api_key="k") def test_get_image_embedding_encodes_and_parses(adapter): raw = b"\x89PNGbytes" with patch(MODULE) as req: req.return_value = _resp({"embedding": [1.0, 2.0]}) out = adapter.get_image_embedding(raw, model_id="clip-v2") assert out == [1.0, 2.0] body = req.call_args.kwargs["json"] assert body["image"] == base64.b64encode(raw).decode("utf-8") assert body["model_id"] == "clip-v2" assert req.call_args.args[1] == "http://brain:5000/vision/embedding" def test_get_image_embedding_raises_inference_error_on_transport_failure(adapter): """Le contrat de toute la branche : une panne se lève, elle ne se déguise jamais en `[]`. Avant ce correctif, seule l'exception brute (`RuntimeError`) remontait -- un appelant qui n'attrape que `InferenceError` (comme `VisualIndexService`/`FallbackInferenceAdapter`) la laissait filtrer.""" with patch(MODULE, side_effect=RuntimeError("x")): with pytest.raises(InferenceError, match="x"): adapter.get_image_embedding(b"data") def test_get_image_embedding_raises_on_empty_vector(adapter): with patch(MODULE) as req: req.return_value = _resp({"embedding": []}) with pytest.raises(InferenceError): adapter.get_image_embedding(b"data") def test_get_image_embedding_raises_on_zero_vector(adapter): """Un vecteur non vide mais entièrement nul est la même panne déguisée.""" with patch(MODULE) as req: req.return_value = _resp({"embedding": [0.0, 0.0, 0.0]}) with pytest.raises(InferenceError): adapter.get_image_embedding(b"data") # --- CLIP text-tower embedding (visual search, text query) ------------------ # # The text tower of the SAME CLIP model that produced the image space -- # reaches a DIFFERENT route than get_image_embedding. Never confused with # get_text_embedding (the generic sentence-transformers endpoint above). def test_get_text_embedding_clip_encodes_and_parses(adapter): with patch(MODULE) as req: req.return_value = _resp({"embedding": [0.1, 0.2]}) out = adapter.get_text_embedding_clip( "une fille aux cheveux blancs", model_id="dudcjs2779/anime-style-tag-clip", ) assert out == [0.1, 0.2] assert req.call_args.args[1] == "http://brain:5000/vision/embedding/text" assert req.call_args.kwargs["json"] == { "text": "une fille aux cheveux blancs", "model_id": "dudcjs2779/anime-style-tag-clip", } def test_get_text_embedding_clip_raises_inference_error_on_transport_failure(adapter): with patch(MODULE, side_effect=RuntimeError("down")): with pytest.raises(InferenceError, match="down"): adapter.get_text_embedding_clip("x", model_id="m") def test_get_text_embedding_clip_raises_on_empty_vector(adapter): with patch(MODULE) as req: req.return_value = _resp({"embedding": []}) with pytest.raises(InferenceError): adapter.get_text_embedding_clip("x", model_id="m") def test_get_text_embedding_clip_raises_on_zero_vector(adapter): with patch(MODULE) as req: req.return_value = _resp({"embedding": [0.0, 0.0]}) with pytest.raises(InferenceError): adapter.get_text_embedding_clip("x", model_id="m") # --- CCIP character embedding (visual search, character target) ------------- # # "Is this the SAME character?" -- a different model, a different route, no # model_id (CCIP is a single fixed model, unlike CLIP's multi-model tower). def test_get_character_embedding_encodes_and_parses(adapter): raw = b"\x89PNG-character-portrait" with patch(MODULE) as req: req.return_value = _resp({"embedding": [0.3] * 768}) out = adapter.get_character_embedding(raw) assert out == [0.3] * 768 assert req.call_args.args[1] == "http://brain:5000/vision/character/embedding" assert req.call_args.kwargs["json"] == { "image": base64.b64encode(raw).decode("utf-8") } def test_get_character_embedding_raises_inference_error_on_transport_failure(adapter): with patch(MODULE, side_effect=RuntimeError("down")): with pytest.raises(InferenceError, match="down"): adapter.get_character_embedding(b"img") def test_get_character_embedding_raises_on_empty_vector(adapter): with patch(MODULE) as req: req.return_value = _resp({"embedding": []}) with pytest.raises(InferenceError): adapter.get_character_embedding(b"img") def test_get_character_embedding_raises_on_zero_vector(adapter): with patch(MODULE) as req: req.return_value = _resp({"embedding": [0.0] * 768}) with pytest.raises(InferenceError): adapter.get_character_embedding(b"img") # --- vision: classify / detect ---------------------------------------------- def test_classify_image_returns_labels(adapter): raw = b"img" with patch(MODULE) as req: req.return_value = _resp({"labels": {"cat": 0.9, "dog": 0.1}}) out = adapter.classify_image(raw, ["cat", "dog"], model_id="m") assert out == {"cat": 0.9, "dog": 0.1} body = req.call_args.kwargs["json"] assert body["candidate_labels"] == ["cat", "dog"] assert body["image"] == base64.b64encode(raw).decode("utf-8") assert req.call_args.args[1] == "http://brain:5000/vision/classify" def test_detect_objects_returns_list(adapter): with patch(MODULE) as req: req.return_value = _resp({"objects": [{"box": [0, 0, 1, 1]}]}) out = adapter.detect_objects(b"img", ["person"]) assert out == [{"box": [0, 0, 1, 1]}] assert req.call_args.args[1] == "http://brain:5000/vision/detect" def test_classify_image_reraises(adapter): with patch(MODULE, side_effect=RuntimeError("x")): with pytest.raises(RuntimeError): adapter.classify_image(b"img", ["a"]) # --- video embeddings / localization / transforms ---------------------------- def test_get_video_temporal_embeddings(adapter): with patch(MODULE) as req: req.return_value = _resp({"embeddings": [{"t": 0, "v": [0.1]}]}) out = adapter.get_video_temporal_embeddings(b"vid") assert out == [{"t": 0, "v": [0.1]}] assert req.call_args.args[1] == "http://brain:5000/video/embeddings" def test_localize_video_actions(adapter): with patch(MODULE) as req: req.return_value = _resp({"actions": [{"start": 1, "end": 2}]}) out = adapter.localize_video_actions(b"vid", ["run"]) assert out == [{"start": 1, "end": 2}] assert req.call_args.kwargs["json"]["queries"] == ["run"] def test_transform_image_to_anime(adapter): with patch(MODULE) as req: req.return_value = _resp({"image_url_or_b64": "anime.png"}) out = adapter.transform_image_to_anime(b"img", "ghibli", prompt="p") assert out == "anime.png" body = req.call_args.kwargs["json"] assert body["studio_style"] == "ghibli" assert body["prompt"] == "p" def test_transform_video_to_anime(adapter): with patch(MODULE) as req: req.return_value = _resp({"video_url_or_b64": "anime.mp4"}) out = adapter.transform_video_to_anime(b"vid", "madhouse") assert out == "anime.mp4" assert req.call_args.args[1] == "http://brain:5000/video/transform/anime" def test_transform_image_reraises(adapter): with patch(MODULE, side_effect=RuntimeError("x")): with pytest.raises(RuntimeError): adapter.transform_image_to_anime(b"img", "s") # --- audio: soundscape / clone / s2s ----------------------------------------- def test_generate_soundscape(adapter): with patch(MODULE) as req: req.return_value = _resp({"audio_url_or_b64": "snd.wav"}) out = adapter.generate_soundscape({"scene": "battle"}, prompt="epic") assert out == "snd.wav" body = req.call_args.kwargs["json"] assert body["video_metadata"] == {"scene": "battle"} assert body["prompt"] == "epic" def test_clone_voice_roundtrips_base64(adapter): out_bytes = b"\x00\x01audio" with patch(MODULE) as req: req.return_value = _resp( {"audio_b64": base64.b64encode(out_bytes).decode("utf-8")} ) out = adapter.clone_voice("bonjour", b"ref-audio", language="en") assert out == out_bytes # decoded back from base64 body = req.call_args.kwargs["json"] assert body["language"] == "en" assert body["reference_audio"] == base64.b64encode(b"ref-audio").decode("utf-8") def test_speech_to_speech_roundtrips_base64(adapter): out_bytes = b"reply-audio" with patch(MODULE) as req: req.return_value = _resp( {"audio_b64": base64.b64encode(out_bytes).decode("utf-8")} ) out = adapter.speech_to_speech(b"in-audio", system_prompt="be nice") assert out == out_bytes assert req.call_args.args[1] == "http://brain:5000/audio/speech-to-speech" def test_clone_voice_reraises(adapter): with patch(MODULE, side_effect=RuntimeError("x")): with pytest.raises(RuntimeError): adapter.clone_voice("t", b"ref") # --- depth / 3d -------------------------------------------------------------- def test_estimate_depth_decodes_b64(adapter): depth = b"depthmap" with patch(MODULE) as req: req.return_value = _resp({"depth_b64": base64.b64encode(depth).decode("utf-8")}) out = adapter.estimate_depth(b"img") assert out == depth def test_generate_3d_scene(adapter): with patch(MODULE) as req: req.return_value = _resp({"scene_data": {"points": 1000}}) out = adapter.generate_3d_scene(b"img", b"depth") assert out == {"points": 1000} body = req.call_args.kwargs["json"] assert body["image"] == base64.b64encode(b"img").decode("utf-8") assert body["depth_map"] == base64.b64encode(b"depth").decode("utf-8") # --- manga ------------------------------------------------------------------- def test_process_manga_page_returns_full_json(adapter): with patch(MODULE) as req: req.return_value = _resp({"panels": [], "text": "x"}) out = adapter.process_manga_page(b"img") assert out == {"panels": [], "text": "x"} def test_translate_manga_page(adapter): with patch(MODULE) as req: req.return_value = _resp({"translated": True}) out = adapter.translate_manga_page(b"img", target_lang="English") assert out == {"translated": True} assert req.call_args.kwargs["json"]["target_lang"] == "English" def test_inpaint_text_bubbles(adapter): placements = [{"x": 1, "y": 2, "text": "hi"}] with patch(MODULE) as req: req.return_value = _resp({"image_url_or_b64": "out.png"}) out = adapter.inpaint_text_bubbles(b"img", placements) assert out == "out.png" assert req.call_args.kwargs["json"]["text_placements"] == placements def test_process_manga_page_reraises(adapter): with patch(MODULE, side_effect=RuntimeError("x")): with pytest.raises(RuntimeError): adapter.process_manga_page(b"img") # --- descriptions ------------------------------------------------------------ def test_generate_image_description(adapter): with patch(MODULE) as req: req.return_value = _resp({"description": "a red sky"}) out = adapter.generate_image_description(b"img", prompt="describe") assert out == "a red sky" assert req.call_args.kwargs["json"]["prompt"] == "describe" def test_generate_video_description(adapter): with patch(MODULE) as req: req.return_value = _resp({"description": "a fight scene"}) out = adapter.generate_video_description(b"vid") assert out == "a fight scene" assert req.call_args.args[1] == "http://brain:5000/video/describe" # --- rerank / diagnostics / uncertainty / visual rerank / late interaction --- def test_rerank_documents(adapter): with patch(MODULE) as req: req.return_value = _resp({"scores": [0.8, 0.2]}) out = adapter.rerank_documents("q", ["d1", "d2"]) assert out == [0.8, 0.2] body = req.call_args.kwargs["json"] assert body == {"query": "q", "documents": ["d1", "d2"]} assert req.call_args.args[1] == "http://brain:5000/v1/rerank" def test_get_diagnostics(adapter): with patch(MODULE) as req: req.return_value = _resp({"diagnostics": {"attn": [1]}}) out = adapter.get_diagnostics("p", "c") assert out == {"attn": [1]} def test_calculate_uncertainty(adapter): with patch(MODULE) as req: req.return_value = _resp({"uncertainty_metrics": {"entropy": 0.4}}) out = adapter.calculate_uncertainty("p", "c") assert out == {"entropy": 0.4} def test_visual_rerank(adapter): with patch(MODULE) as req: req.return_value = _resp({"reranked_items": [{"url": "a", "score": 1}]}) out = adapter.visual_rerank("q", ["a", "b"], system_prompt="judge") assert out == [{"url": "a", "score": 1}] body = req.call_args.kwargs["json"] assert body["image_urls"] == ["a", "b"] assert body["system_prompt"] == "judge" def test_get_multimodal_late_interaction(adapter): with patch(MODULE) as req: req.return_value = _resp({"embeddings": [[0.1, 0.2], [0.3, 0.4]]}) out = adapter.get_multimodal_late_interaction(b"img") assert out == [[0.1, 0.2], [0.3, 0.4]] def test_rerank_documents_reraises(adapter): with patch(MODULE, side_effect=RuntimeError("x")): with pytest.raises(RuntimeError): adapter.rerank_documents("q", ["d"]) # --- moderation: native path + super() fallback ------------------------------ def test_moderate_content_returns_native_payload(adapter): with patch(MODULE) as req: req.return_value = _resp({"moderation": {"is_safe": True, "score": 0.0}}) out = adapter.moderate_content("hello", ["nsfw"]) assert out == {"is_safe": True, "score": 0.0} body = req.call_args.kwargs["json"] assert body == {"text": "hello", "categories": ["nsfw"]} def test_moderate_content_falls_back_to_super_on_error(adapter): # Native endpoint fails -> falls back to base class keyword heuristic, which # itself calls generate_structured -> generate. Make generate raise so the # keyword fallback inside the base class is exercised, with a flagged word. with patch(MODULE, side_effect=RuntimeError("api down")): with patch.object(adapter, "generate", side_effect=RuntimeError("no llm")): out = adapter.moderate_content("this is nsfw content", ["x"]) assert out["is_safe"] is False assert "nsfw" in out["detected_categories"] assert out["action"] == "block" def test_moderate_content_pins_the_configured_model(): a = BrainAPIAdapter(api_url="http://brain:5000", api_key="k", model="small:1.5b") with patch("adapters.inference.brain_api_adapter.safe_http_request") as req: req.return_value = MagicMock( json=MagicMock(return_value={"moderation": {"is_safe": True}}) ) out = a.moderate_content("texte", ["HATE_SPEECH"]) assert req.call_args.kwargs["json"]["model"] == "small:1.5b" assert out == {"is_safe": True} # --- generate_structured delegates to base implementation -------------------- def test_generate_structured_delegates_to_super(adapter): # Base generate_structured calls self.generate; return JSON text it can parse. with patch.object(adapter, "generate") as gen: gen.return_value = InferenceResponse(text='{"name": "Naruto", "rank": 1}') out = adapter.generate_structured("extract", dict) assert out == {"name": "Naruto", "rank": 1} def test_generate_structured_reraises_on_total_failure(adapter): with patch.object(adapter, "generate", side_effect=RuntimeError("boom")): with pytest.raises(RuntimeError, match="boom"): adapter.generate_structured("extract", dict, max_retries=1) # --- health_check: online / degraded / offline ------------------------------- def test_health_check_online(adapter): with patch("adapters.inference.brain_api_adapter.httpx.get") as get: get.return_value = MagicMock(status_code=200) out = adapter.health_check() assert out["status"] == "online" assert out["engine"] == "BrainAPI" assert "latency_ms" in out assert get.call_args.args[0] == "http://brain:5000/health" def test_health_check_honors_the_remote_verdict_over_the_status_code(adapter): # The brain answers 200 while grading its own engine as degraded (e.g. Ollama # does not serve the configured model). Trusting the status code alone kept # exactly that brain in the FallbackAdapter rotation, 404-ing every call. with patch("adapters.inference.brain_api_adapter.httpx.get") as get: get.return_value = MagicMock( status_code=200, json=MagicMock(return_value={"status": "degraded", "engine": "Ollama"}), ) out = adapter.health_check() assert out["status"] == "degraded" assert out["engine"] == "BrainAPI" def test_health_check_online_when_remote_body_reports_online(adapter): with patch("adapters.inference.brain_api_adapter.httpx.get") as get: get.return_value = MagicMock( status_code=200, json=MagicMock(return_value={"status": "online", "engine": "Ollama"}), ) out = adapter.health_check() assert out["status"] == "online" def test_health_check_degraded_on_non_200(adapter): with patch("adapters.inference.brain_api_adapter.httpx.get") as get: get.return_value = MagicMock(status_code=503) out = adapter.health_check() assert out["status"] == "degraded" def test_health_check_offline_on_exception(adapter): with patch( "adapters.inference.brain_api_adapter.httpx.get", side_effect=httpx.ConnectError("refused"), ): out = adapter.health_check() assert out == {"status": "offline", "engine": "BrainAPI"} # --- error re-raise contract for the remaining passthrough methods ----------- # Each of these wraps safe_http_request in try/except that logs and re-raises. # Parametrized to exercise every error branch with a single helper. @pytest.mark.parametrize( "call", [ lambda a: a.detect_objects(b"img", ["x"]), lambda a: a.get_video_temporal_embeddings(b"vid"), lambda a: a.localize_video_actions(b"vid", ["run"]), lambda a: a.transform_video_to_anime(b"vid", "s"), lambda a: a.generate_soundscape({"k": 1}), lambda a: a.speech_to_speech(b"aud"), lambda a: a.estimate_depth(b"img"), lambda a: a.generate_3d_scene(b"img", b"depth"), lambda a: a.translate_manga_page(b"img"), lambda a: a.inpaint_text_bubbles(b"img", []), lambda a: a.generate_image_description(b"img"), lambda a: a.generate_video_description(b"vid"), lambda a: a.get_diagnostics("p", "c"), lambda a: a.calculate_uncertainty("p", "c"), lambda a: a.visual_rerank("q", ["a"]), lambda a: a.get_multimodal_late_interaction(b"img"), ], ) def test_passthrough_methods_reraise_on_error(adapter, call): with patch(MODULE, side_effect=RuntimeError("api boom")): with pytest.raises(RuntimeError, match="api boom"): call(adapter)