"""Tests for grounding — the semantic-poisoning seam (BRIEF §7/§10, PLAN §67/§94). Interface only in the stdlib core. lexicon and entropy see *surface* signals; they are structurally blind to a factually false claim in clean prose. Detecting that needs a model call, which never lives in this core — so grounding ships a `text -> Report` protocol plus a pass-through default, and a `[judge]` implementation plugs in behind an extra. These tests pin the seam: the default is silent (and honest about why), a conforming impl satisfies the protocol, and its findings compose through :mod:`disposition` unchanged. """ from __future__ import annotations from llm_ingestion_guard.grounding import ( DEFAULT_GROUNDING_CHECK, SourceGroundingCheck, no_grounding_check, ) from llm_ingestion_guard.report import Finding, Report, Severity, Source from llm_ingestion_guard.disposition import ( Disposition, PRESET_USER_UPLOAD, decide, ) # A factually false claim in clean prose: no injection token, no obfuscation, no # invisible carrier. lexicon and entropy — and therefore the deterministic core # — are structurally blind to it. Only a grounding check against a source of # truth can catch it. _SEMANTIC_POISON = "The capital of France is Berlin." # --- the pass-through default ---------------------------------------------- def test_no_grounding_check_returns_empty_report(): report = no_grounding_check("any content", source=Source.OUTPUT) assert isinstance(report, Report) assert not report.found assert report.findings == [] def test_no_grounding_check_default_source_is_output_and_accepts_source(): # source is part of the seam signature; the default ignores it but accepts it. assert not no_grounding_check("x").found # default source assert not no_grounding_check("x", source=Source.INPUT).found def test_no_grounding_check_is_silent_on_semantic_poisoning(): # The honest scope note, made a test: semantic poisoning is NOT solved at the # text layer, and the core does not pretend to. An empty report here is "no # check ran", not "verified clean". assert not no_grounding_check(_SEMANTIC_POISON).found def test_default_grounding_check_is_the_no_op(): assert DEFAULT_GROUNDING_CHECK is no_grounding_check # --- the protocol (structural seam) ---------------------------------------- def test_no_grounding_check_satisfies_protocol(): assert isinstance(no_grounding_check, SourceGroundingCheck) def test_non_callable_does_not_satisfy_protocol(): assert not isinstance(object(), SourceGroundingCheck) assert not isinstance("not a check", SourceGroundingCheck) class _FactJudge: """A stand-in `[judge]` SourceGroundingCheck: a stateful callable that flags one known-false claim. Real impls hold a retriever / model client; the grounding source of truth is the impl's concern, never a seam parameter.""" def __call__(self, text: str, *, source: Source = Source.OUTPUT) -> Report: report = Report() if "capital of france is berlin" in text.lower(): report.add(Finding( label="grounding:factual-contradiction", severity=Severity.HIGH, source=source, detector="grounding", evidence="claim contradicts trusted source", )) return report def test_stateful_judge_impl_satisfies_protocol(): assert isinstance(_FactJudge(), SourceGroundingCheck) # --- the seam is real: findings flow through disposition ------------------- def test_grounding_findings_compose_with_disposition(): judge = _FactJudge() # the core misses it; the plugged-in judge catches the same text. assert not no_grounding_check(_SEMANTIC_POISON).found report = judge(_SEMANTIC_POISON, source=Source.OUTPUT) assert report.found assert report.findings[0].detector == "grounding" # and its finding disposes like any other detector's: HIGH under an untrusted # upload halts. No grounding-specific plumbing in disposition. result = decide(report, PRESET_USER_UPLOAD) assert result.disposition is Disposition.FAIL_SECURE