Files
87d9525176 feat(resolver): opt-in semantic nearest-neighbour fallback (#34) (#140)
Closes #114, #115, #116, #117.

- Deterministic, offline, dependency-free char-ngram embedding backend + config
  section (semantic.enabled default false, min_similarity, backend).
- Committed embedding index (data/kb_embedding_index.json) mapping vectors to real
  KB entry ids only; built by scripts/build_embedding_index.py, loaded once/cached.
- Resolver falls back to the nearest entry on a deterministic miss when enabled,
  accepting >= min_similarity and emitting SC0020; NSFW-gated neighbours are
  skipped. Disabled => byte-identical to before.
- Guarantee tests: deterministic lookup always wins (exact hit never queries the
  index), fallback emits only KB tags (no invention), identical input => identical
  output. SC0020 registered.

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:23:38 +02:00

161 lines
5.1 KiB
Python

"""Tests for the configuration system (issue #5, MASTER_SPEC §23)."""
from __future__ import annotations
import dataclasses
import json
import pytest
from compiler.common.config import Config, ConfigError, load_config
from compiler.common.result import Severity
# The canonical example from MASTER_SPEC §23.2.
EXAMPLE_CONFIG = {
"schema": "1.0",
"analyzer": {
"backend": "ollama",
"model": "llama3",
"temperature": 0.0,
"max_retries": 3,
"timeout": 60,
},
"resolver": {
"knowledge_base": "knowledge_base/",
"strict_mode": True,
"allow_aliases": True,
"expansion_enabled": True,
"max_expansion_depth": 8,
"include_nsfw": False,
},
"validator": {"allow_unknown_fields": False},
"prompt_builder": {
"target": "easy_illustrious",
"separator": ",",
"remove_duplicate_tags": True,
},
"semantic": {"enabled": False, "min_similarity": 0.5, "backend": "char_ngram"},
"debug": {"enabled": False, "level": "basic"},
}
# --- defaults --------------------------------------------------------------
def test_defaults_match_section_23_2() -> None:
c = Config()
assert c.schema == "1.0"
assert c.analyzer.backend == "ollama"
assert c.analyzer.temperature == pytest.approx(0.0)
assert c.analyzer.max_retries == 3
assert c.analyzer.timeout == 60
assert c.resolver.knowledge_base == "knowledge_base/"
assert c.resolver.max_expansion_depth == 8
assert c.resolver.strict_mode is True
assert c.validator.allow_unknown_fields is False
assert c.prompt_builder.separator == ","
assert c.prompt_builder.target == "easy_illustrious"
assert c.debug.enabled is False
assert c.debug.level == "basic"
def test_defaults_equal_full_example() -> None:
assert Config() == Config.from_json(EXAMPLE_CONFIG)
# --- load ------------------------------------------------------------------
def test_full_example_loads_and_roundtrips() -> None:
c = Config.from_json(EXAMPLE_CONFIG)
assert c.to_json() == EXAMPLE_CONFIG
def test_empty_config_uses_all_defaults() -> None:
assert Config.from_json({}) == Config()
def test_partial_config_fills_defaults() -> None:
c = Config.from_json({"analyzer": {"model": "mistral"}})
assert c.analyzer.model == "mistral"
assert c.analyzer.temperature == pytest.approx(0.0) # default preserved
assert c.resolver.max_expansion_depth == 8
def test_semantic_defaults_off() -> None:
c = Config()
assert c.semantic.enabled is False
assert c.semantic.min_similarity == pytest.approx(0.5)
assert c.semantic.backend == "char_ngram"
def test_semantic_section_roundtrips() -> None:
c = Config.from_json({"semantic": {"enabled": True, "min_similarity": 0.8}})
assert c.semantic.enabled is True
assert c.semantic.min_similarity == pytest.approx(0.8)
assert c.to_json()["semantic"]["enabled"] is True
def test_knowledge_base_version_defaults_to_none_and_is_omitted() -> None:
c = Config()
assert c.resolver.knowledge_base_version is None
assert "knowledge_base_version" not in c.resolver.to_json() # omitted when unset
def test_knowledge_base_version_roundtrips_when_set() -> None:
c = Config.from_json({"resolver": {"knowledge_base_version": "2.0.0"}})
assert c.resolver.knowledge_base_version == "2.0.0"
assert c.to_json()["resolver"]["knowledge_base_version"] == "2.0.0"
def test_load_config_reads_file(tmp_path) -> None:
path = tmp_path / "config.json"
path.write_text(json.dumps(EXAMPLE_CONFIG), encoding="utf-8")
assert load_config(path) == Config.from_json(EXAMPLE_CONFIG)
# --- invalid config -> SC0014 ----------------------------------------------
def test_wrong_type_rejected_with_sc0014() -> None:
with pytest.raises(ConfigError) as exc:
Config.from_json({"analyzer": {"temperature": "hot"}})
assert exc.value.message.code == "SC0014"
assert exc.value.message.severity is Severity.FATAL
def test_unknown_field_rejected() -> None:
with pytest.raises(ConfigError):
Config.from_json({"analyzer": {"unknown_option": 1}})
def test_random_seed_option_is_forbidden() -> None:
with pytest.raises(ConfigError) as exc:
Config.from_json({"analyzer": {"seed": 42}})
assert exc.value.message.code == "SC0014"
def test_negative_temperature_rejected() -> None:
with pytest.raises(ConfigError):
Config.from_json({"analyzer": {"temperature": -1.0}})
# --- immutability ----------------------------------------------------------
def test_config_is_immutable() -> None:
with pytest.raises(dataclasses.FrozenInstanceError):
Config().analyzer.temperature = 1.0 # type: ignore[misc]
# --- shipped default file --------------------------------------------------
def test_shipped_default_config_file_matches_defaults() -> None:
from pathlib import Path
repo_root = Path(__file__).resolve().parent.parent
default_file = repo_root / "config" / "default_config.json"
data = json.loads(default_file.read_text(encoding="utf-8"))
assert Config.from_json(data) == Config()