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
90 lines
3.3 KiB
Python
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
|