Tests on main had drifted from intentional substrate changes that
weren't propagated to their fixtures or pinned values. Categories:
1. PackMutationProposal missing source= arg (3 tests across
test_mutation_proposal_type, test_provenance, test_expert_demo_runnable):
add ProposalSource(kind="operator", source_id="", emitted_at_revision="test")
to the shared fixture. test_expert_demo_runnable also retargets the
"unpromoted domain" example from systems_software (now promoted) to
arithmetic (real but unpromoted).
2. Pack content grew (test_en_core_meta_v1_pack 73→77 entries, 49→53 verbs;
test_en_core_spatial_v1_pack 24→25 entries adding "places" plural surface):
bump expected counts; allow new provenance shapes from the
adr-0085-style-v2 review (including the seed:core_meta/seed:core_spatial
author-time typos on two entries each — documented inline rather than
masked).
3. Registry self-documenting "add names to the set" failures
(test_lane_sha_verifier: add curriculum_loop_closure;
test_register_runtime_threading: add gloss_aware_cause_surface,
pack_grounded_unknown_surface, teaching_grounded_surface_transitive).
4. Gloss content was seeded where tests pinned None
(test_pack_resolver_glosses TestMissingGlossesIsBackCompat): switch
the no-glosses pack from en_core_relations_v1 (since glossed) to
en_minimal_v1 (still gloss-free); narrow resolve_gloss probe to that
pack so other packs' glosses can't shadow.
5. Entry-id renumber from cognition-pack expansion
(test_language_pack_cache): en-core-cog-085 → en-core-cog-091.
6. Holdout tests fail without CORE_HOLDOUT_KEY or local plaintext
(test_eval_holdout_split + test_transitive_surface): add
_requires_holdout skip-marker mirroring _decrypt_holdout's contract;
gate the transitive_surface holdout iteration on the same check.
7. Byte-identity surface guards regressed after the gloss-aware
composer landed (test_realizer_guard_holdout, test_prompt_diversity_runner,
test_register_substantive_consumption): re-pin to current surfaces
("Light is a visible medium that reveals truth." replaces "Light is a
source of revelation that makes things knowable.", etc.). The guard's
regression-catching role is preserved by pinning current output going
forward; the new gloss-driven phrasings are visibly more grounded.
Touched 14 test files: 176 passed, 4 skipped (holdout-gated), 0 failed
on a targeted re-run. No production code touched.
287 lines
12 KiB
Python
287 lines
12 KiB
Python
"""Tests for the gloss-resolver branch on chat/pack_resolver.py.
|
|
|
|
Five hardening items from the 2026-05-19 design review:
|
|
|
|
1. Lexicon-residency check on resolve_gloss().
|
|
2. Dual-checksum manifest support (separate glosses_checksum field).
|
|
3. clear_resolver_cache() clears BOTH lexicon AND glosses caches.
|
|
4. Malformed JSONL lines are silently skipped (defensive parsing).
|
|
5. Missing glosses.jsonl is back-compat (returns empty dict, no raise).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from chat.pack_resolver import (
|
|
DEFAULT_RESOLVABLE_PACK_IDS,
|
|
_pack_glosses_for,
|
|
_pack_lexicon_for,
|
|
clear_resolver_cache,
|
|
resolve_gloss,
|
|
resolve_lemma,
|
|
)
|
|
|
|
|
|
_PACK_ROOT = Path(__file__).resolve().parent.parent / "language_packs" / "data"
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_pack(tmp_path):
|
|
"""Create a minimal lexicon-only pack on disk for fixture tests.
|
|
|
|
Builds the pack inside the real ``language_packs/data`` tree so the
|
|
resolver's hard-coded _PACK_ROOT finds it; tears down on exit.
|
|
Tests using this fixture should clear the resolver cache.
|
|
"""
|
|
pack_id = "test_gloss_fixture_pack_v1"
|
|
pack_dir = _PACK_ROOT / pack_id
|
|
pack_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
lexicon = [
|
|
{"entry_id": "test-001", "surface": "alpha", "lemma": "alpha",
|
|
"language": "en", "pos": "NOUN",
|
|
"semantic_domains": ["test.alpha"], "morphology_tags": ["noun"],
|
|
"provenance_ids": ["test"]},
|
|
{"entry_id": "test-002", "surface": "beta", "lemma": "beta",
|
|
"language": "en", "pos": "NOUN",
|
|
"semantic_domains": ["test.beta"], "morphology_tags": ["noun"],
|
|
"provenance_ids": ["test"]},
|
|
]
|
|
lex_path = pack_dir / "lexicon.jsonl"
|
|
lex_path.write_text(
|
|
"\n".join(json.dumps(e, separators=(",", ":")) for e in lexicon) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
clear_resolver_cache()
|
|
yield pack_dir, pack_id
|
|
clear_resolver_cache()
|
|
shutil.rmtree(pack_dir, ignore_errors=True)
|
|
|
|
|
|
class TestLexiconResidencyEnforced:
|
|
"""resolve_gloss() must reject any gloss for a lemma not present
|
|
in the same pack's lexicon. Without this, glosses.jsonl becomes a
|
|
parallel surface-authoring channel that bypasses the lexicon seal.
|
|
"""
|
|
|
|
def test_gloss_for_unratified_lemma_is_rejected(self, temp_pack) -> None:
|
|
pack_dir, pack_id = temp_pack
|
|
glosses_path = pack_dir / "glosses.jsonl"
|
|
# Authoring a gloss for ``gamma`` — a lemma NOT in the lexicon.
|
|
glosses_path.write_text(
|
|
json.dumps({"lemma": "gamma", "gloss": "an unratified atom",
|
|
"pos": "NOUN", "provenance_ids": ["test"]})
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
clear_resolver_cache()
|
|
assert resolve_gloss("gamma", (pack_id,)) is None
|
|
|
|
def test_gloss_for_ratified_lemma_resolves(self, temp_pack) -> None:
|
|
pack_dir, pack_id = temp_pack
|
|
glosses_path = pack_dir / "glosses.jsonl"
|
|
glosses_path.write_text(
|
|
json.dumps({"lemma": "alpha", "gloss": "the first letter",
|
|
"pos": "NOUN", "provenance_ids": ["test"]})
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
clear_resolver_cache()
|
|
resolved = resolve_gloss("alpha", (pack_id,))
|
|
assert resolved is not None
|
|
assert resolved == (pack_id, "NOUN", "the first letter")
|
|
|
|
def test_first_match_wins_with_lexicon_residency(self) -> None:
|
|
"""When the lemma is in pack A's lexicon but not B's, even if
|
|
B has a (forbidden) gloss for it, A's gloss wins (or None if A
|
|
has no gloss). Real-packs invariant: no two packs in
|
|
DEFAULT_RESOLVABLE_PACK_IDS share a lemma.
|
|
"""
|
|
# Smoke check on the real packs: every lemma resolves to
|
|
# exactly one pack across DEFAULT_RESOLVABLE_PACK_IDS.
|
|
seen: dict[str, str] = {}
|
|
for pack_id in DEFAULT_RESOLVABLE_PACK_IDS:
|
|
for lemma in _pack_lexicon_for(pack_id):
|
|
if lemma in seen and seen[lemma] != pack_id:
|
|
# Some lemmas may legitimately appear in multiple
|
|
# packs (e.g. cause/NOUN in cognition vs an
|
|
# alternative). This is just a smoke check that
|
|
# the resolver doesn't crash.
|
|
pass
|
|
seen.setdefault(lemma, pack_id)
|
|
|
|
|
|
class TestMissingGlossesIsBackCompat:
|
|
"""When a pack ships no glosses.jsonl the resolver must treat that
|
|
as the default and return None / empty, never raise.
|
|
|
|
en_minimal_v1 is the canonical no-glosses pack used here; most
|
|
content packs have since had glosses seeded under ADR-0085."""
|
|
|
|
def test_pack_with_no_glosses_returns_empty(self) -> None:
|
|
glosses = _pack_glosses_for("en_minimal_v1")
|
|
assert glosses == {}
|
|
|
|
def test_resolve_gloss_on_lemma_without_gloss_file_returns_none(self) -> None:
|
|
# ``yes`` is in en_minimal_v1 lexicon; that pack ships no
|
|
# glosses.jsonl today. Restrict resolution to that pack so a
|
|
# gloss seeded for "yes" elsewhere can't shadow the test.
|
|
assert resolve_gloss("yes", ("en_minimal_v1",)) is None
|
|
|
|
|
|
class TestClearResolverCacheClearsBoth:
|
|
"""clear_resolver_cache() must invalidate BOTH the lexicon AND
|
|
the glosses LRU caches. Without the glosses clear, a test that
|
|
writes a glosses.jsonl mid-run would see stale (empty) gloss data
|
|
on subsequent resolve_gloss() calls."""
|
|
|
|
def test_clears_both_caches(self, temp_pack) -> None:
|
|
pack_dir, pack_id = temp_pack
|
|
# No glosses yet — resolve_gloss returns None.
|
|
assert resolve_gloss("alpha", (pack_id,)) is None
|
|
# Now author a gloss.
|
|
glosses_path = pack_dir / "glosses.jsonl"
|
|
glosses_path.write_text(
|
|
json.dumps({"lemma": "alpha", "gloss": "the first letter",
|
|
"pos": "NOUN", "provenance_ids": ["test"]})
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
# Without clearing, the empty result is still cached.
|
|
assert resolve_gloss("alpha", (pack_id,)) is None
|
|
# After clearing both caches, the new gloss is visible.
|
|
clear_resolver_cache()
|
|
resolved = resolve_gloss("alpha", (pack_id,))
|
|
assert resolved == (pack_id, "NOUN", "the first letter")
|
|
|
|
|
|
class TestMalformedJsonlSkippedSilently:
|
|
"""A single malformed line must not break gloss resolution for the
|
|
rest of the pack."""
|
|
|
|
def test_malformed_line_skipped(self, temp_pack) -> None:
|
|
pack_dir, pack_id = temp_pack
|
|
glosses_path = pack_dir / "glosses.jsonl"
|
|
body = "\n".join([
|
|
json.dumps({"lemma": "alpha", "gloss": "first letter",
|
|
"pos": "NOUN", "provenance_ids": ["test"]}),
|
|
"this is not valid json {{{",
|
|
json.dumps({"lemma": "beta", "gloss": "second letter",
|
|
"pos": "NOUN", "provenance_ids": ["test"]}),
|
|
]) + "\n"
|
|
glosses_path.write_text(body, encoding="utf-8")
|
|
clear_resolver_cache()
|
|
# Both well-formed lines must resolve; the malformed line is
|
|
# silently skipped.
|
|
assert resolve_gloss("alpha", (pack_id,)) is not None
|
|
assert resolve_gloss("beta", (pack_id,)) is not None
|
|
|
|
def test_entry_missing_required_field_skipped(self, temp_pack) -> None:
|
|
pack_dir, pack_id = temp_pack
|
|
glosses_path = pack_dir / "glosses.jsonl"
|
|
body = "\n".join([
|
|
json.dumps({"lemma": "alpha"}), # no gloss
|
|
json.dumps({"gloss": "anonymous"}), # no lemma
|
|
json.dumps({"lemma": "", "gloss": "empty lemma"}), # empty lemma
|
|
json.dumps({"lemma": "beta", "gloss": ""}), # empty gloss
|
|
json.dumps({"lemma": "beta", "gloss": "valid", # the only valid one
|
|
"pos": "NOUN", "provenance_ids": ["test"]}),
|
|
]) + "\n"
|
|
glosses_path.write_text(body, encoding="utf-8")
|
|
clear_resolver_cache()
|
|
glosses = _pack_glosses_for(pack_id)
|
|
assert "alpha" not in glosses
|
|
assert "beta" in glosses
|
|
assert glosses["beta"][1] == "valid"
|
|
|
|
|
|
class TestDualChecksumManifest:
|
|
"""The compiler must verify glosses.jsonl bytes-on-disk against the
|
|
manifest's optional ``glosses_checksum`` field — same discipline as
|
|
the lexicon checksum. Missing field = back-compat (no verification)."""
|
|
|
|
def test_back_compat_pack_without_glosses_loads_clean(self) -> None:
|
|
"""A pack that ships no glosses.jsonl and no glosses_checksum
|
|
in its manifest must continue to load (back-compat invariant).
|
|
We use en_minimal_v1 which deliberately ships no glosses."""
|
|
from language_packs.compiler import load_pack
|
|
manifest, _ = load_pack("en_minimal_v1")
|
|
assert manifest.glosses_checksum is None
|
|
|
|
def test_glossed_pack_carries_checksum(self) -> None:
|
|
"""Packs that DO ship glosses (en_core_cognition_v1 et al.
|
|
after Phase C) must carry a non-None glosses_checksum on the
|
|
loaded manifest."""
|
|
from language_packs.compiler import load_pack
|
|
manifest, _ = load_pack("en_core_cognition_v1")
|
|
assert isinstance(manifest.glosses_checksum, str)
|
|
assert len(manifest.glosses_checksum) == 64
|
|
|
|
def test_checksum_mismatch_raises(self, temp_pack) -> None:
|
|
pack_dir, pack_id = temp_pack
|
|
# Write a glosses.jsonl
|
|
glosses_text = json.dumps({"lemma": "alpha", "gloss": "first",
|
|
"pos": "NOUN", "provenance_ids": ["test"]}) + "\n"
|
|
glosses_path = pack_dir / "glosses.jsonl"
|
|
glosses_path.write_text(glosses_text, encoding="utf-8")
|
|
# Write a manifest pinning a WRONG glosses_checksum
|
|
manifest_path = pack_dir / "manifest.json"
|
|
# Compute the actual lexicon checksum so the lexicon gate passes
|
|
lex_bytes = (pack_dir / "lexicon.jsonl").read_bytes()
|
|
lex_checksum = hashlib.sha256(lex_bytes).hexdigest()
|
|
wrong_checksum = "0" * 64
|
|
manifest_path.write_text(json.dumps({
|
|
"pack_id": pack_id,
|
|
"language": "en",
|
|
"role": "operational_base",
|
|
"script": "Latin",
|
|
"normalization_policy": "unitize_versor",
|
|
"source_manifest": f"{pack_id}.lexicon.jsonl",
|
|
"determinism_class": "D0",
|
|
"checksum": lex_checksum,
|
|
"glosses_checksum": wrong_checksum,
|
|
"version": "1.0.0",
|
|
"gate_engaged": True,
|
|
"oov_policy": "tagged_fallback",
|
|
}) + "\n", encoding="utf-8")
|
|
clear_resolver_cache()
|
|
from language_packs.compiler import load_pack, _load_pack_cached
|
|
_load_pack_cached.cache_clear()
|
|
with pytest.raises(ValueError, match="Glosses checksum mismatch"):
|
|
load_pack(pack_id)
|
|
|
|
def test_matching_glosses_checksum_loads_clean(self, temp_pack) -> None:
|
|
pack_dir, pack_id = temp_pack
|
|
glosses_text = json.dumps({"lemma": "alpha", "gloss": "first",
|
|
"pos": "NOUN", "provenance_ids": ["test"]}) + "\n"
|
|
glosses_path = pack_dir / "glosses.jsonl"
|
|
glosses_path.write_text(glosses_text, encoding="utf-8")
|
|
right_checksum = hashlib.sha256(glosses_text.encode("utf-8")).hexdigest()
|
|
lex_bytes = (pack_dir / "lexicon.jsonl").read_bytes()
|
|
lex_checksum = hashlib.sha256(lex_bytes).hexdigest()
|
|
manifest_path = pack_dir / "manifest.json"
|
|
manifest_path.write_text(json.dumps({
|
|
"pack_id": pack_id,
|
|
"language": "en",
|
|
"role": "operational_base",
|
|
"script": "Latin",
|
|
"normalization_policy": "unitize_versor",
|
|
"source_manifest": f"{pack_id}.lexicon.jsonl",
|
|
"determinism_class": "D0",
|
|
"checksum": lex_checksum,
|
|
"glosses_checksum": right_checksum,
|
|
"version": "1.0.0",
|
|
"gate_engaged": True,
|
|
"oov_policy": "tagged_fallback",
|
|
}) + "\n", encoding="utf-8")
|
|
clear_resolver_cache()
|
|
from language_packs.compiler import load_pack, _load_pack_cached
|
|
_load_pack_cached.cache_clear()
|
|
manifest, _ = load_pack(pack_id)
|
|
assert manifest.glosses_checksum == right_checksum
|