Replaces LICENSE (GPLv3 -> AGPLv3) and prepends `SPDX-License-Identifier: AGPL-3.0-or-later` to every source file across decnet/, decnet_web/, tests/, scripts/, and tools/. Rationale: closes the GPLv3 ASP loophole so any party operating a modified DECNET as a network service must offer their modified source. Personal copyright (Samuel Paschuan) + inbound=outbound contributions make a future unilateral relicense infeasible. - LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt) - COPYRIGHT: project copyright notice - tools/add_spdx_headers.py: idempotent header injector (shebang- and PEP 263-aware) Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh). No behavior change; comments only.
161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""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"
|