turnstone/tests/test_services_llm.py
pyr0ball 7d46314e86 feat: switch LLM backend to OpenAI-compat; add cf-orch remote inference support
Turnstone now calls /v1/chat/completions instead of Ollama's /api/generate.
This format works with both local Ollama (>=0.1.24) and a remote cf-orch
coordinator, enabling GPU-less nodes like Contributor2's to route diagnoses through
the cluster without any local model.

- llm.py: OpenAI-compat messages format, optional Bearer auth header
- diagnose.py: thread llm_api_key through the call chain
- rest.py: llm_api_key pref (default empty), SettingsBody field, passed to diagnose
- SettingsView.vue: API Key field, label updated from "Ollama URL" to "LLM Endpoint URL"
- tests: updated mocks for new response shape; added bearer token assertion test
2026-05-12 12:58:38 -07:00

90 lines
3.3 KiB
Python

"""Tests for app/services/llm.py — graceful failure and context building."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from app.services.llm import summarize, _build_context
from app.services.search import SearchResult
def _entry(text: str, severity: str = "INFO", ts: str = "2026-05-06T21:00:00+00:00") -> SearchResult:
return SearchResult(
entry_id="x",
source_id="svc",
sequence=0,
timestamp_iso=ts,
severity=severity,
text=text,
matched_patterns=[],
repeat_count=1,
out_of_order=False,
rank=0.0,
)
def test_summarize_returns_none_on_connection_error():
with patch("app.services.llm.httpx.post", side_effect=ConnectionError("refused")):
result = summarize("ollama crashed", [_entry("failed")], "http://bad", "llama3")
assert result is None
def test_summarize_returns_none_on_http_error():
mock_resp = MagicMock()
mock_resp.raise_for_status.side_effect = Exception("404")
with patch("app.services.llm.httpx.post", return_value=mock_resp):
result = summarize("ollama crashed", [_entry("failed")], "http://host", "llama3")
assert result is None
def test_summarize_returns_none_on_empty_response():
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"choices": [{"message": {"content": ""}}]}
with patch("app.services.llm.httpx.post", return_value=mock_resp):
result = summarize("query", [_entry("x")], "http://host", "llama3")
assert result is None
def test_summarize_returns_none_on_missing_choices():
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"choices": []}
with patch("app.services.llm.httpx.post", return_value=mock_resp):
result = summarize("query", [_entry("x")], "http://host", "llama3")
assert result is None
def test_summarize_returns_text_on_success():
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"choices": [{"message": {"content": "Ollama exited with code 1."}}]}
with patch("app.services.llm.httpx.post", return_value=mock_resp):
result = summarize("ollama crashed", [_entry("Failed")], "http://host", "llama3")
assert result == "Ollama exited with code 1."
def test_summarize_sends_bearer_token():
mock_resp = MagicMock()
mock_resp.raise_for_status.return_value = None
mock_resp.json.return_value = {"choices": [{"message": {"content": "disk full"}}]}
with patch("app.services.llm.httpx.post", return_value=mock_resp) as mock_post:
summarize("disk error", [_entry("ENOSPC")], "http://host", "llama3", api_key="test-key")
call_kwargs = mock_post.call_args
assert call_kwargs.kwargs["headers"] == {"Authorization": "Bearer test-key"}
def test_build_context_sorts_errors_first():
entries = [
_entry("info message", severity="INFO"),
_entry("critical crash", severity="CRITICAL"),
_entry("warn spike", severity="WARN"),
]
ctx = _build_context(entries)
lines = ctx.splitlines()
assert "CRITICAL" in lines[0]
assert "WARN" in lines[1]
def test_summarize_empty_entries_returns_none():
result = summarize("query", [], "http://host", "model")
assert result is None