core/tests/test_math_lexical_ratification.py

304 lines
9.3 KiB
Python

"""ADR-0167 W2-D — LexicalClaim ratification handler tests."""
from __future__ import annotations
import functools
import hashlib
import json
import shutil
from pathlib import Path
import pytest
from generate.comprehension import lexicon as comprehension_lexicon
from generate.comprehension import lifecycle
from generate.comprehension.audit import AuditRow, audit_problem
from generate.comprehension.state import ReaderRefusal
from teaching.math_evidence import from_audit_row
from teaching.math_lexical_ratification import (
AlreadyRatified,
EvidenceTampering,
SAFE_CATEGORIES,
UnknownCategory,
WrongClaimSubType,
WrongZeroViolationCandidate,
apply_lexical_claim,
)
REPO_ROOT = Path(__file__).resolve().parents[1]
PACK_ROOT = REPO_ROOT / "language_packs" / "data" / "en_core_math_v1"
CASES_PATH = REPO_ROOT / "evals" / "gsm8k_math" / "train_sample" / "v1" / "cases.jsonl"
@pytest.fixture()
def pack_copy(tmp_path: Path) -> Path:
target = tmp_path / "en_core_math_v1"
shutil.copytree(PACK_ROOT, target)
comprehension_lexicon._CACHE.clear()
lifecycle._get_lexicon.cache_clear()
yield target
comprehension_lexicon._CACHE.clear()
lifecycle._get_lexicon.cache_clear()
def _row(surface: str, *, missing_operator: str = "lexicon_entry") -> AuditRow:
return AuditRow(
case_id=f"case-{surface}",
sentence_index=0,
token_index=2,
token_text=surface,
recognized_terms=("Ava", "counts"),
skipped_frame=None,
missing_operator=missing_operator,
refusal_reason="unknown_word",
refusal_detail=f"no primitive or lexicon match for '{surface}'",
)
def _claim(surface: str, *, sub_type: str = "lexical"):
return from_audit_row(_row(surface), sub_type) # type: ignore[arg-type]
def _entries(path: Path) -> list[dict]:
return [
json.loads(line)
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def _manifest_sha(pack_root: Path) -> str:
return hashlib.sha256((pack_root / "manifest.json").read_bytes()).hexdigest()
def _use_pack_for_reader(monkeypatch: pytest.MonkeyPatch, pack_root: Path) -> None:
comprehension_lexicon._CACHE.clear()
@functools.cache
def _tmp_lexicon():
return comprehension_lexicon.load_lexicon(pack_root)
monkeypatch.setattr(lifecycle, "_get_lexicon", _tmp_lexicon)
def test_apply_lexical_claim_writes_lemma(pack_copy: Path) -> None:
receipt = apply_lexical_claim(
claim=_claim("widgets"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
assert receipt.lemma == "widgets"
assert receipt.category == "drain_token"
assert receipt.is_alias is False
assert receipt.aliased_to is None
entry = next(e for e in _entries(pack_copy / "lexicon" / "drain_token.jsonl") if e["lemma"] == "widgets")
assert entry == {
"lemma": "widgets",
"category": "drain_token",
"aliases": [],
"provenance": receipt.provenance,
}
lex = comprehension_lexicon.load_lexicon(pack_copy)
resolved = comprehension_lexicon.lookup(lex, "widgets")
assert resolved is not None
assert resolved.category == "drain_token"
def test_receipt_records_before_after_sha(pack_copy: Path) -> None:
receipt = apply_lexical_claim(
claim=_claim("widgets"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
assert len(receipt.file_sha256_before) == 64
assert len(receipt.file_sha256_after) == 64
assert receipt.file_sha256_before != receipt.file_sha256_after
assert receipt.evidence_hash == _claim("widgets").evidence_hash
def test_idempotent_raises_already_ratified(pack_copy: Path) -> None:
claim = _claim("widgets")
apply_lexical_claim(
claim=claim,
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
with pytest.raises(AlreadyRatified, match="already ratified"):
apply_lexical_claim(
claim=claim,
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
def test_rejects_non_lexical_sub_type(pack_copy: Path) -> None:
with pytest.raises(WrongClaimSubType):
apply_lexical_claim(
claim=_claim("widgets", sub_type="frame"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
def test_rejects_unsafe_category(pack_copy: Path) -> None:
with pytest.raises(WrongZeroViolationCandidate, match="open reader frames"):
apply_lexical_claim(
claim=_claim("widgets"),
category="accumulation_verb",
reviewer="Ada",
pack_root=pack_copy,
)
def test_rejects_unknown_category(pack_copy: Path) -> None:
with pytest.raises(UnknownCategory):
apply_lexical_claim(
claim=_claim("widgets"),
category="not_a_category",
reviewer="Ada",
pack_root=pack_copy,
)
def test_rejects_evidence_tampering(pack_copy: Path) -> None:
claim = _claim("widgets")
object.__setattr__(claim, "evidence_hash", "0" * 64)
with pytest.raises(EvidenceTampering):
apply_lexical_claim(
claim=claim,
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
def test_alphabetical_sort_preserved(pack_copy: Path) -> None:
apply_lexical_claim(
claim=_claim("aardwidgets"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
lemmas = [entry["lemma"] for entry in _entries(pack_copy / "lexicon" / "drain_token.jsonl")]
assert lemmas == sorted(lemmas)
def test_manifest_checksum_unchanged(pack_copy: Path) -> None:
manifest_bytes_before = (pack_copy / "manifest.json").read_bytes()
manifest_sha_before = _manifest_sha(pack_copy)
declared_before = json.loads(manifest_bytes_before)["checksum"]
apply_lexical_claim(
claim=_claim("widgets"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
manifest_bytes_after = (pack_copy / "manifest.json").read_bytes()
assert manifest_bytes_after == manifest_bytes_before
assert _manifest_sha(pack_copy) == manifest_sha_before
assert json.loads(manifest_bytes_after)["checksum"] == declared_before
def test_hazard_case_0050_remains_refused(pack_copy: Path, monkeypatch: pytest.MonkeyPatch) -> None:
with pytest.raises(AlreadyRatified):
apply_lexical_claim(
claim=_claim("along"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
_use_pack_for_reader(monkeypatch, pack_copy)
cases = [json.loads(line) for line in CASES_PATH.read_text().splitlines()]
case = next(c for c in cases if c["case_id"] == "gsm8k-train-sample-v1-0050")
result, _rows = audit_problem(case["question"], case_id=case["case_id"])
assert isinstance(result, ReaderRefusal)
assert result.sentence_index == 0
def test_audit_artifact_still_has_zero_admissions_after_tmp_ratification(
pack_copy: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
apply_lexical_claim(
claim=_claim("widgets"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
_use_pack_for_reader(monkeypatch, pack_copy)
cases = [json.loads(line) for line in CASES_PATH.read_text().splitlines()]
for case in cases:
result, _rows = audit_problem(case["question"], case_id=case["case_id"])
assert isinstance(result, ReaderRefusal) or result is None, case["case_id"]
def test_load_lexicon_resolves_newly_ratified_lemma(pack_copy: Path) -> None:
apply_lexical_claim(
claim=_claim("ratifiables"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
comprehension_lexicon._CACHE.clear()
lex = comprehension_lexicon.load_lexicon(pack_copy)
resolved = comprehension_lexicon.lookup(lex, "ratifiables")
assert resolved is not None
assert resolved.lemma == "ratifiables"
assert resolved.category == "drain_token"
def test_alias_path_when_existing_lemma_is_stem(pack_copy: Path) -> None:
apply_lexical_claim(
claim=_claim("widget"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
receipt = apply_lexical_claim(
claim=_claim("widgets"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
assert receipt.is_alias is True
assert receipt.aliased_to == "widget"
entry = next(e for e in _entries(pack_copy / "lexicon" / "drain_token.jsonl") if e["lemma"] == "widget")
assert "widgets" in entry["aliases"]
lex = comprehension_lexicon.load_lexicon(pack_copy)
resolved = comprehension_lexicon.lookup(lex, "widgets")
assert resolved is not None
assert resolved.lemma == "widget"
def test_rejects_existing_frame_opener_surface(pack_copy: Path) -> None:
with pytest.raises(WrongZeroViolationCandidate, match="frame-opener"):
apply_lexical_claim(
claim=_claim("earn"),
category="drain_token",
reviewer="Ada",
pack_root=pack_copy,
)
def test_safe_categories_w2d_scope_is_drain_token_only() -> None:
assert SAFE_CATEGORIES == frozenset({"drain_token"})