from __future__ import annotations import httpx import pytest from activity_core.llm_client import LLMConnectClient def test_llm_connect_client_forwards_run_config(monkeypatch) -> None: captured: dict = {} class Response: status_code = 200 def json(self) -> dict: return { "content": '{"summary":"ok","recommendations":[]}', "finish_reason": "stop", "usage": {"input_tokens": 10, "output_tokens": 20}, "raw_response": {"provider_blob": "not persisted"}, } def fake_post(url: str, json: dict, timeout: float) -> Response: captured["url"] = url captured["json"] = json captured["timeout"] = timeout return Response() monkeypatch.setattr(httpx, "post", fake_post) client = LLMConnectClient("http://llm-connect.local/", timeout_seconds=42) result = client.complete( "Prompt", model="fallback-model", config={ "model_name": "custodian-triage-balanced", "temperature": 0.2, "max_tokens": 1200, "max_depth": 2, "model_params": {"reasoning_effort": "medium"}, }, ) assert result == '{"summary":"ok","recommendations":[]}' assert captured["url"] == "http://llm-connect.local/execute" assert captured["timeout"] == 42 assert captured["json"] == { "prompt": "Prompt", "config": { "model_name": "custodian-triage-balanced", "temperature": 0.2, "max_tokens": 1200, "max_depth": 2, "model_params": {"reasoning_effort": "medium"}, "timeout_seconds": 42, }, } assert client.last_response_metadata == { "finish_reason": "stop", "usage": {"input_tokens": 10, "output_tokens": 20}, } class ErrorResponse: """Models an llm-connect error reply, which carries the real cause.""" def __init__(self, status_code: int, body) -> None: self.status_code = status_code self._body = body def json(self): if self._body is _NON_JSON: raise ValueError("not json") return self._body _NON_JSON = object() def _client(monkeypatch, response) -> LLMConnectClient: monkeypatch.setattr(httpx, "post", lambda url, json, timeout: response) return LLMConnectClient("http://llm-connect.local") def test_rejected_provider_key_is_distinguishable_from_a_dead_gateway(monkeypatch) -> None: """llm-connect maps every provider API error onto 502 (server.py:230). ACTIVITY-WP-0031: reporting only "502 Bad Gateway" made a revoked OpenRouter key look like a downed llm-connect for four days. """ client = _client( monkeypatch, ErrorResponse( 502, { "error": "provider_api_error", "provider_status": 401, "provider": "openrouter", "message": "No auth credentials found", }, ), ) with pytest.raises(RuntimeError) as excinfo: client.complete("Prompt") text = str(excinfo.value) assert "HTTP 502" in text assert "error=provider_api_error" in text assert "provider_status=401" in text assert "provider=openrouter" in text assert "message=No auth credentials found" in text def test_error_text_keeps_only_allowlisted_fields(monkeypatch) -> None: client = _client( monkeypatch, ErrorResponse( 502, { "error": "provider_api_error", "provider_status": 401, "api_key": "sk-should-never-appear", "raw_response": {"authorization": "Bearer secret"}, }, ), ) with pytest.raises(RuntimeError) as excinfo: client.complete("Prompt") text = str(excinfo.value) assert "sk-should-never-appear" not in text assert "authorization" not in text assert "provider_status=401" in text def test_error_message_is_bounded(monkeypatch) -> None: client = _client( monkeypatch, ErrorResponse(502, {"error": "provider_api_error", "message": "x" * 5000}), ) with pytest.raises(RuntimeError) as excinfo: client.complete("Prompt") assert len(str(excinfo.value)) < 600 def test_error_without_a_usable_body_still_reports_the_status(monkeypatch) -> None: client = _client(monkeypatch, ErrorResponse(504, _NON_JSON)) with pytest.raises(RuntimeError, match="llm-connect returned HTTP 504"): client.complete("Prompt") def test_error_with_a_non_dict_body_still_reports_the_status(monkeypatch) -> None: client = _client(monkeypatch, ErrorResponse(500, ["unexpected"])) with pytest.raises(RuntimeError, match="llm-connect returned HTTP 500"): client.complete("Prompt")