Files
markitect-main/tests/unit/llm/test_config.py
tegwick fecc2fd4fa feat(llm): add LLM integration module with OpenRouter and Claude Code adapters
Implements markitect/llm/ package with concrete LLMAdapter implementations:
- OpenRouterAdapter: HTTP via urllib with retry/backoff on 429/5xx
- ClaudeCodeAdapter: subprocess-based Claude CLI with stdin piping
- Factory pattern: create_adapter("openrouter") or create_adapter("claude-code")
- API key resolution chain: constructor > env var > project-root key file
- 42 unit tests, 2 integration tests (gated on API key / CLI availability)

Also adds the infospace-with-history example with Wealth of Nations VSM
analysis pipeline, templates, schemas, source chapters, and processed
output for chapters 1-2. process_chapters.py now supports --provider
and --model flags for automatic LLM-driven processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 01:17:58 +01:00

106 lines
3.2 KiB
Python

"""Tests for markitect.llm.config."""
import os
from pathlib import Path
from unittest import mock
import pytest
from markitect.llm.config import (
LLMConfig,
resolve_api_key,
find_project_root,
load_config,
)
class TestResolveApiKey:
def test_explicit_key_wins(self):
key = resolve_api_key(explicit="sk-explicit")
assert key == "sk-explicit"
def test_env_var_fallback(self):
with mock.patch.dict(os.environ, {"MY_KEY": "sk-env"}):
key = resolve_api_key(explicit=None, env_var="MY_KEY")
assert key == "sk-env"
def test_env_var_stripped(self):
with mock.patch.dict(os.environ, {"MY_KEY": " sk-env \n"}):
key = resolve_api_key(explicit=None, env_var="MY_KEY")
assert key == "sk-env"
def test_file_fallback(self, tmp_path):
key_file = tmp_path / "key.txt"
key_file.write_text("sk-file\n")
key = resolve_api_key(
explicit=None,
env_var="NONEXISTENT_VAR_XYZ",
key_file_paths=[key_file],
)
assert key == "sk-file"
def test_file_skips_empty(self, tmp_path):
empty = tmp_path / "empty.txt"
empty.write_text("")
good = tmp_path / "good.txt"
good.write_text("sk-good")
key = resolve_api_key(
explicit=None,
env_var="NONEXISTENT_VAR_XYZ",
key_file_paths=[empty, good],
)
assert key == "sk-good"
def test_file_skips_missing(self, tmp_path):
missing = tmp_path / "no-such-file.txt"
key = resolve_api_key(
explicit=None,
env_var="NONEXISTENT_VAR_XYZ",
key_file_paths=[missing],
)
assert key is None
def test_returns_none_when_nothing_found(self):
key = resolve_api_key(
explicit=None,
env_var="NONEXISTENT_VAR_XYZ",
key_file_paths=[],
)
assert key is None
class TestFindProjectRoot:
def test_finds_root(self, tmp_path):
(tmp_path / "pyproject.toml").write_text("[project]")
sub = tmp_path / "a" / "b"
sub.mkdir(parents=True)
assert find_project_root(sub) == tmp_path
def test_returns_none_if_no_marker(self, tmp_path):
sub = tmp_path / "a" / "b"
sub.mkdir(parents=True)
# tmp_path itself won't have pyproject.toml
result = find_project_root(sub)
# Could be None or could find the real project root above;
# the important thing is it doesn't crash
assert result is None or (result / "pyproject.toml").exists()
class TestLoadConfig:
def test_returns_llmconfig(self):
cfg = load_config(provider="claude-code")
assert isinstance(cfg, LLMConfig)
assert cfg.provider == "claude-code"
def test_model_override(self):
cfg = load_config(model="my-model")
assert cfg.model == "my-model"
def test_explicit_key(self):
cfg = load_config(api_key="sk-test")
assert cfg.api_key == "sk-test"
def test_extra_overrides(self):
cfg = load_config(timeout_seconds=60)
assert cfg.timeout_seconds == 60