feat(realism/llm): DB-backed LLMConfig, factory DB-first dispatch, Ollama HTTP mode

This commit is contained in:
2026-05-09 23:09:36 -04:00
parent f10201e885
commit 155ab59ee8
5 changed files with 385 additions and 15 deletions

View File

View File

@@ -0,0 +1,159 @@
"""Tests for decnet.realism.llm.config and the updated factory DB-first path."""
from __future__ import annotations
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
from decnet.realism.llm import config as _cfg_mod
from decnet.realism.llm import get_llm
from decnet.realism.llm.impl.fake import FakeBackend
from decnet.realism.llm.impl.ollama import OllamaBackend
# ── LLMConfig validation ──────────────────────────────────────────────────────
def test_defaults():
c = _cfg_mod.LLMConfig()
assert c.provider == "ollama"
assert c.base_url is None
assert c.model == "llama3.1"
assert c.timeout == 60.0
def test_base_url_trailing_slash_stripped():
c = _cfg_mod.LLMConfig(base_url="http://localhost:11434/")
assert c.base_url == "http://localhost:11434"
def test_base_url_empty_string_normalised_to_none():
c = _cfg_mod.LLMConfig(base_url="")
assert c.base_url is None
def test_base_url_non_http_rejected():
from pydantic import ValidationError
with pytest.raises(ValidationError, match="http"):
_cfg_mod.LLMConfig(base_url="ollama://localhost")
def test_unknown_provider_rejected():
from pydantic import ValidationError
with pytest.raises(ValidationError):
_cfg_mod.LLMConfig(provider="vllm")
# ── apply() builds the right backend ─────────────────────────────────────────
def test_apply_ollama_no_url():
_cfg_mod._cached_backend = None
_cfg_mod.apply(_cfg_mod.LLMConfig(provider="ollama", model="phi3"))
b = _cfg_mod.get_cached_backend()
assert isinstance(b, OllamaBackend)
assert b.model == "phi3"
assert b.base_url is None
def test_apply_ollama_with_url():
_cfg_mod._cached_backend = None
_cfg_mod.apply(_cfg_mod.LLMConfig(
provider="ollama",
model="llama3.1",
base_url="http://10.0.0.1:11434",
))
b = _cfg_mod.get_cached_backend()
assert isinstance(b, OllamaBackend)
assert b.base_url == "http://10.0.0.1:11434"
def test_apply_fake():
_cfg_mod._cached_backend = None
_cfg_mod.apply(_cfg_mod.LLMConfig(provider="fake"))
b = _cfg_mod.get_cached_backend()
assert isinstance(b, FakeBackend)
def test_apply_ollama_with_api_key(monkeypatch):
from cryptography.fernet import Fernet
key = Fernet.generate_key().decode()
monkeypatch.setenv("DECNET_SECRET_KEY", key)
from decnet.web.db.secrets import encrypt_secret
ct = encrypt_secret("sk-supersecret")
_cfg_mod._cached_backend = None
_cfg_mod.apply(_cfg_mod.LLMConfig(provider="ollama", api_key_ciphertext=ct))
b = _cfg_mod.get_cached_backend()
assert isinstance(b, OllamaBackend)
assert b.api_key == "sk-supersecret"
# ── load_from_db ──────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_load_from_db_returns_none_when_no_row():
repo = MagicMock()
repo.get_realism_config = AsyncMock(return_value=None)
result = await _cfg_mod.load_from_db(repo)
assert result is None
@pytest.mark.asyncio
async def test_load_from_db_parses_valid_row():
repo = MagicMock()
payload = {"provider": "ollama", "model": "qwen2:7b", "timeout": 30}
repo.get_realism_config = AsyncMock(
return_value={"value": json.dumps(payload)}
)
result = await _cfg_mod.load_from_db(repo)
assert result is not None
assert result.model == "qwen2:7b"
assert result.timeout == 30.0
@pytest.mark.asyncio
async def test_load_from_db_returns_none_on_bad_json():
repo = MagicMock()
repo.get_realism_config = AsyncMock(return_value={"value": "not-json{{"})
result = await _cfg_mod.load_from_db(repo)
assert result is None
@pytest.mark.asyncio
async def test_load_from_db_returns_none_on_db_error():
repo = MagicMock()
repo.get_realism_config = AsyncMock(side_effect=RuntimeError("db down"))
result = await _cfg_mod.load_from_db(repo)
assert result is None
# ── factory DB-first path ─────────────────────────────────────────────────────
def test_factory_uses_cached_backend_when_set():
_cfg_mod._cached_backend = None
_cfg_mod.apply(_cfg_mod.LLMConfig(provider="fake"))
backend = get_llm()
assert isinstance(backend, FakeBackend)
def test_factory_falls_back_to_env_when_no_cache(monkeypatch):
_cfg_mod._cached_backend = None
monkeypatch.setenv("DECNET_REALISM_LLM", "ollama")
backend = get_llm()
assert isinstance(backend, OllamaBackend)
def test_factory_model_override_bypasses_cache():
_cfg_mod._cached_backend = None
_cfg_mod.apply(_cfg_mod.LLMConfig(provider="fake"))
# Explicit model override skips the cache and uses env dispatch.
monkeypatch = None # model override makes it fall through to env
# With model= set, the fast-path is skipped; falls to env default.
import os
os.environ.setdefault("DECNET_REALISM_LLM", "ollama")
backend = get_llm(model="llama3:8b")
assert isinstance(backend, OllamaBackend)
assert backend.model == "llama3:8b"