266 lines
7.7 KiB
Python
266 lines
7.7 KiB
Python
"""CW-2 — composition_registry loader tests.
|
||
|
||
Covers: empty no-op, round-trip, deterministic order, manifest mismatch,
|
||
declared-without-file, cache, byte-stability, allowlist enforcement
|
||
(defense-in-depth), polarity validation, conflict detection,
|
||
``is_affirmed`` / ``is_falsified`` semantics.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from generate.comprehension.composition_registry import (
|
||
CompositionRegistry,
|
||
CompositionRegistryLoadError,
|
||
WrongCompositionCategory,
|
||
clear_cache,
|
||
is_affirmed,
|
||
is_falsified,
|
||
load_composition_registry,
|
||
lookup,
|
||
)
|
||
from packs.compile_compositions import compile_compositions
|
||
from teaching.math_composition_ratification import SAFE_COMPOSITION_CATEGORIES
|
||
|
||
|
||
def _write_entry(
|
||
path: Path,
|
||
surface_pattern: str,
|
||
composition_category: str,
|
||
polarity: str = "affirms",
|
||
provenance: str = "test_provenance",
|
||
) -> None:
|
||
with path.open("a", encoding="utf-8") as f:
|
||
f.write(
|
||
json.dumps(
|
||
{
|
||
"surface_pattern": surface_pattern,
|
||
"composition_category": composition_category,
|
||
"polarity": polarity,
|
||
"provenance": provenance,
|
||
"evidence_hashes": [],
|
||
}
|
||
)
|
||
+ "\n"
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def empty_pack(tmp_path: Path) -> Path:
|
||
(tmp_path / "manifest.json").write_text(
|
||
json.dumps({"pack_id": "test_empty", "checksum": "deadbeef"}),
|
||
encoding="utf-8",
|
||
)
|
||
return tmp_path
|
||
|
||
|
||
@pytest.fixture
|
||
def populated_pack(tmp_path: Path) -> Path:
|
||
comp_dir = tmp_path / "compositions"
|
||
comp_dir.mkdir()
|
||
_write_entry(
|
||
comp_dir / "multiplicative.jsonl",
|
||
"bound(count) × bound(unit_cost)",
|
||
"multiplicative_composition",
|
||
polarity="affirms",
|
||
)
|
||
_write_entry(
|
||
comp_dir / "additive.jsonl",
|
||
"bound(qty_a) + bound(qty_b)",
|
||
"additive_composition",
|
||
polarity="affirms",
|
||
)
|
||
_, sha = compile_compositions(tmp_path)
|
||
(tmp_path / "manifest.json").write_text(
|
||
json.dumps(
|
||
{
|
||
"pack_id": "test_populated",
|
||
"checksum": "deadbeef",
|
||
"composition_checksum": sha,
|
||
}
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
return tmp_path
|
||
|
||
|
||
def setup_function(_):
|
||
clear_cache()
|
||
|
||
|
||
def test_empty_pack_returns_empty_registry(empty_pack: Path):
|
||
reg = load_composition_registry(empty_pack)
|
||
assert isinstance(reg, CompositionRegistry)
|
||
assert reg.is_empty()
|
||
|
||
|
||
def test_populated_round_trip(populated_pack: Path):
|
||
reg = load_composition_registry(populated_pack)
|
||
assert not reg.is_empty()
|
||
entry = lookup(reg, "bound(count) × bound(unit_cost)")
|
||
assert entry is not None
|
||
assert entry.composition_category == "multiplicative_composition"
|
||
assert entry.polarity == "affirms"
|
||
|
||
|
||
def test_safe_categories_pinned():
|
||
# Defense-in-depth: the load-time allowlist matches what the handler
|
||
# enforces at write time. If the handler's set ever drifts, this
|
||
# test fails — operator must update both surfaces consciously.
|
||
assert SAFE_COMPOSITION_CATEGORIES == frozenset(
|
||
{
|
||
"multiplicative_composition",
|
||
"additive_composition",
|
||
"subtractive_composition",
|
||
}
|
||
)
|
||
|
||
|
||
def test_unsafe_category_at_load_raises(tmp_path: Path):
|
||
comp_dir = tmp_path / "compositions"
|
||
comp_dir.mkdir()
|
||
_write_entry(
|
||
comp_dir / "bad.jsonl",
|
||
"bound(a) ratio bound(b)",
|
||
"ratio_composition", # explicitly deferred per ADR-0169
|
||
)
|
||
_, sha = compile_compositions(tmp_path)
|
||
(tmp_path / "manifest.json").write_text(
|
||
json.dumps(
|
||
{
|
||
"pack_id": "test",
|
||
"checksum": "x",
|
||
"composition_checksum": sha,
|
||
}
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
with pytest.raises(WrongCompositionCategory, match="ratio_composition"):
|
||
load_composition_registry(tmp_path)
|
||
|
||
|
||
def test_invalid_polarity_raises(tmp_path: Path):
|
||
comp_dir = tmp_path / "compositions"
|
||
comp_dir.mkdir()
|
||
_write_entry(
|
||
comp_dir / "bad.jsonl",
|
||
"x",
|
||
"multiplicative_composition",
|
||
polarity="maybe",
|
||
)
|
||
_, sha = compile_compositions(tmp_path)
|
||
(tmp_path / "manifest.json").write_text(
|
||
json.dumps(
|
||
{
|
||
"pack_id": "test",
|
||
"checksum": "x",
|
||
"composition_checksum": sha,
|
||
}
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
with pytest.raises(CompositionRegistryLoadError, match="invalid polarity"):
|
||
load_composition_registry(tmp_path)
|
||
|
||
|
||
def test_is_affirmed_and_is_falsified(tmp_path: Path):
|
||
comp_dir = tmp_path / "compositions"
|
||
comp_dir.mkdir()
|
||
_write_entry(
|
||
comp_dir / "ok.jsonl",
|
||
"shape_a",
|
||
"multiplicative_composition",
|
||
polarity="affirms",
|
||
)
|
||
_write_entry(
|
||
comp_dir / "ok.jsonl",
|
||
"shape_b",
|
||
"multiplicative_composition",
|
||
polarity="falsifies",
|
||
)
|
||
_, sha = compile_compositions(tmp_path)
|
||
(tmp_path / "manifest.json").write_text(
|
||
json.dumps({"pack_id": "t", "checksum": "x", "composition_checksum": sha}),
|
||
encoding="utf-8",
|
||
)
|
||
reg = load_composition_registry(tmp_path)
|
||
assert is_affirmed(reg, "shape_a") is True
|
||
assert is_falsified(reg, "shape_a") is False
|
||
assert is_affirmed(reg, "shape_b") is False
|
||
assert is_falsified(reg, "shape_b") is True
|
||
# Absence
|
||
assert is_affirmed(reg, "shape_unknown") is False
|
||
assert is_falsified(reg, "shape_unknown") is False
|
||
|
||
|
||
def test_manifest_mismatch_raises(populated_pack: Path):
|
||
manifest_path = populated_pack / "manifest.json"
|
||
payload = json.loads(manifest_path.read_text())
|
||
payload["composition_checksum"] = "f" * 64
|
||
manifest_path.write_text(json.dumps(payload), encoding="utf-8")
|
||
clear_cache()
|
||
with pytest.raises(CompositionRegistryLoadError, match="checksum mismatch"):
|
||
load_composition_registry(populated_pack)
|
||
|
||
|
||
def test_declared_without_file_raises(tmp_path: Path):
|
||
(tmp_path / "manifest.json").write_text(
|
||
json.dumps(
|
||
{
|
||
"pack_id": "test",
|
||
"checksum": "x",
|
||
"composition_checksum": "f" * 64,
|
||
}
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
with pytest.raises(CompositionRegistryLoadError, match="missing"):
|
||
load_composition_registry(tmp_path)
|
||
|
||
|
||
def test_compile_byte_stable(tmp_path: Path):
|
||
comp_dir = tmp_path / "compositions"
|
||
comp_dir.mkdir()
|
||
_write_entry(
|
||
comp_dir / "a.jsonl",
|
||
"bound(count) × bound(unit_cost)",
|
||
"multiplicative_composition",
|
||
)
|
||
b1, s1 = compile_compositions(tmp_path)
|
||
b2, s2 = compile_compositions(tmp_path)
|
||
assert b1 == b2
|
||
assert s1 == s2
|
||
|
||
|
||
def test_cache_returns_same_instance(populated_pack: Path):
|
||
reg1 = load_composition_registry(populated_pack)
|
||
reg2 = load_composition_registry(populated_pack)
|
||
assert reg1 is reg2
|
||
|
||
|
||
def test_conflicting_pattern_polarities_raise(tmp_path: Path):
|
||
comp_dir = tmp_path / "compositions"
|
||
comp_dir.mkdir()
|
||
_write_entry(
|
||
comp_dir / "a.jsonl",
|
||
"same_pattern",
|
||
"multiplicative_composition",
|
||
polarity="affirms",
|
||
)
|
||
_write_entry(
|
||
comp_dir / "b.jsonl",
|
||
"same_pattern",
|
||
"multiplicative_composition",
|
||
polarity="falsifies",
|
||
)
|
||
_, sha = compile_compositions(tmp_path)
|
||
(tmp_path / "manifest.json").write_text(
|
||
json.dumps({"pack_id": "t", "checksum": "x", "composition_checksum": sha}),
|
||
encoding="utf-8",
|
||
)
|
||
with pytest.raises(CompositionRegistryLoadError, match="conflicting entries"):
|
||
load_composition_registry(tmp_path)
|