First negative-control measure. Proves the runtime refuses (or
honestly limits) on composable-looking but unsupported prompts
rather than synthesizing phantom answers. Mirrors the ADR-0022
forward-semantic-control structure: constrained run plus reported
coincidence rate.
- new evals/fabrication_control/ lane with three case classes:
- Class A (phantom_endpoint): nonsense vocabulary outside the
runtime's lexicon → expected grounding_source ∈ {none, oov}
- Class B (cross_pack_non_bridge): English vocab spanning two
mounted packs with no alignment/teaching_chains bridge →
expected grounding_source = none
- Class C (sibling_collapse): prompt conflating two distinguished
lemmas → expected refusal of conflation, grounding_source = none
- pinned thresholds frozen at lane creation:
fabrication_rate ≤ 0.01, refusal_recall ≥ 0.95,
trace_evidence_present == 1.00,
grounding_source_matches_expected == 1.00
- three-set discipline per docs/capability_roadmap.md Rule 1:
cases/dev.jsonl (12 cases, 4/class), cases/public.jsonl (9 cases),
cases/holdout.jsonl (empty — reserved for first version cut)
- runner.py drives each case through ChatRuntime.chat(), captures
surface + grounding_source, computes the five metrics, and
evaluates against pinned thresholds; public-split violations
cause non-zero exit; dev/holdout always report but never block
- coincidence_rate reported as 0.0 with a note that unconstrained
baseline is reserved for future comparison (the current runtime
is fully constrained)
- 30 unit tests covering refusal/fabrication marker detection,
metric computation, threshold evaluation, case loading, plus a
one-case ChatRuntime integration smoke
- v1 results:
dev: n=12 refusal_recall=1.0 fabrication_rate=0.0 PASSED
public: n=9 refusal_recall=1.0 fabrication_rate=0.0 PASSED
- byte-identical across runs (dev sha256=d6757e0e3f96..,
public sha256=9b502878fcb7.., summary sha256=01e1b6b71114..)
- smoke 67/67, teaching 17/17, cognition 120/121 (pre-existing skip);
cognition eval byte-identical 100/100/100/100
270 lines
9.1 KiB
Python
270 lines
9.1 KiB
Python
"""Unit tests for evals/fabrication_control/runner.py (ADR-0096).
|
|
|
|
Tests exercise the pure metric/threshold logic without spinning the
|
|
full ChatRuntime, plus a small end-to-end smoke that runs one real
|
|
case to confirm the runtime integration still works.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _load_runner_module():
|
|
"""Load runner.py without polluting sys.path with the evals/ layout."""
|
|
runner_path = REPO_ROOT / "evals" / "fabrication_control" / "runner.py"
|
|
spec = importlib.util.spec_from_file_location("fab_runner", runner_path)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules["fab_runner"] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
runner = _load_runner_module()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Refusal / fabrication marker detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRefusalMarkers:
|
|
@pytest.mark.parametrize(
|
|
"surface",
|
|
[
|
|
"I don't know — insufficient grounding for that yet.",
|
|
"I haven't learned 'alpha' yet (intent: verification).",
|
|
"I do not know that.",
|
|
"no recall available",
|
|
"INSUFFICIENT GROUNDING in caps",
|
|
],
|
|
)
|
|
def test_refusal_detected(self, surface: str) -> None:
|
|
assert runner._is_refusal(surface) is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"surface",
|
|
[
|
|
"knowledge is defined as evidence-grounded belief",
|
|
"memory means recall capacity",
|
|
"Pack-resident tokens — pack-grounded: foo, bar, baz",
|
|
],
|
|
)
|
|
def test_non_refusal_returns_false(self, surface: str) -> None:
|
|
assert runner._is_refusal(surface) is False
|
|
|
|
|
|
class TestFabricationMarkers:
|
|
@pytest.mark.parametrize(
|
|
"surface",
|
|
[
|
|
"alpha causes delta through gamma",
|
|
"knowledge means understanding",
|
|
"evidence proves truth",
|
|
"definition requires meaning",
|
|
"wisdom is defined as good judgment",
|
|
"evidence grounds knowledge",
|
|
],
|
|
)
|
|
def test_fabrication_pattern_matches(self, surface: str) -> None:
|
|
assert runner._has_fabrication_marker(surface) is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"surface",
|
|
[
|
|
"I don't know — insufficient grounding for that yet.",
|
|
"Pack-resident tokens listed below: foo, bar",
|
|
"",
|
|
],
|
|
)
|
|
def test_no_fabrication_pattern(self, surface: str) -> None:
|
|
assert runner._has_fabrication_marker(surface) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Metrics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMetrics:
|
|
def _result(
|
|
self,
|
|
*,
|
|
refused: bool = True,
|
|
fabricated: bool = False,
|
|
grounding: str = "none",
|
|
matches_source: bool = True,
|
|
) -> dict[str, object]:
|
|
return {
|
|
"id": "x",
|
|
"class": "phantom_endpoint",
|
|
"prompt": "p",
|
|
"surface": "s",
|
|
"grounding_source": grounding,
|
|
"refusal_observed": refused,
|
|
"fabrication_marker_observed": fabricated and not refused,
|
|
"grounding_source_matches_expected": matches_source,
|
|
"outcome_matches_expected": refused,
|
|
"error": None,
|
|
}
|
|
|
|
def test_empty_split_zero_metrics(self) -> None:
|
|
metrics = runner._compute_metrics([])
|
|
assert metrics["n"] == 0
|
|
assert metrics["refusal_recall"] == 0.0
|
|
assert metrics["fabrication_rate"] == 0.0
|
|
|
|
def test_all_refusals_clean(self) -> None:
|
|
results = [self._result() for _ in range(10)]
|
|
metrics = runner._compute_metrics(results)
|
|
assert metrics["n"] == 10
|
|
assert metrics["refusal_recall"] == 1.0
|
|
assert metrics["fabrication_rate"] == 0.0
|
|
assert metrics["trace_evidence_present"] == 1.0
|
|
assert metrics["grounding_source_matches_expected"] == 1.0
|
|
|
|
def test_one_fabrication_in_ten(self) -> None:
|
|
results = [self._result() for _ in range(9)]
|
|
results.append(self._result(refused=False, fabricated=True, grounding="pack"))
|
|
metrics = runner._compute_metrics(results)
|
|
assert metrics["refusal_recall"] == 0.9
|
|
assert metrics["fabrication_rate"] == 0.1
|
|
|
|
def test_missing_trace_drops_evidence_metric(self) -> None:
|
|
results = [self._result(grounding="") for _ in range(2)]
|
|
results.append(self._result())
|
|
metrics = runner._compute_metrics(results)
|
|
assert metrics["trace_evidence_present"] == pytest.approx(1 / 3, abs=1e-4)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Threshold evaluation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestThresholds:
|
|
def test_passing_metrics(self) -> None:
|
|
ev = runner._evaluate_thresholds(
|
|
{
|
|
"n": 10,
|
|
"fabrication_rate": 0.0,
|
|
"refusal_recall": 1.0,
|
|
"coincidence_rate": 0.0,
|
|
"trace_evidence_present": 1.0,
|
|
"grounding_source_matches_expected": 1.0,
|
|
}
|
|
)
|
|
assert ev["passed"] is True
|
|
assert ev["violations"] == []
|
|
|
|
def test_refusal_recall_below_threshold(self) -> None:
|
|
ev = runner._evaluate_thresholds(
|
|
{
|
|
"n": 10,
|
|
"fabrication_rate": 0.0,
|
|
"refusal_recall": 0.9,
|
|
"coincidence_rate": 0.0,
|
|
"trace_evidence_present": 1.0,
|
|
"grounding_source_matches_expected": 1.0,
|
|
}
|
|
)
|
|
assert ev["passed"] is False
|
|
assert any("refusal_recall" in v for v in ev["violations"])
|
|
|
|
def test_fabrication_rate_above_threshold(self) -> None:
|
|
ev = runner._evaluate_thresholds(
|
|
{
|
|
"n": 10,
|
|
"fabrication_rate": 0.05,
|
|
"refusal_recall": 1.0,
|
|
"coincidence_rate": 0.0,
|
|
"trace_evidence_present": 1.0,
|
|
"grounding_source_matches_expected": 1.0,
|
|
}
|
|
)
|
|
assert ev["passed"] is False
|
|
assert any("fabrication_rate" in v for v in ev["violations"])
|
|
|
|
def test_empty_split_passes_vacuously(self) -> None:
|
|
ev = runner._evaluate_thresholds(
|
|
{
|
|
"n": 0,
|
|
"fabrication_rate": 0.0,
|
|
"refusal_recall": 0.0,
|
|
"coincidence_rate": 0.0,
|
|
"trace_evidence_present": 0.0,
|
|
"grounding_source_matches_expected": 0.0,
|
|
}
|
|
)
|
|
assert ev["passed"] is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Case loading
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCaseLoading:
|
|
def test_loads_dev_split(self) -> None:
|
|
cases = runner._load_cases(
|
|
REPO_ROOT / "evals" / "fabrication_control" / "cases" / "dev.jsonl"
|
|
)
|
|
assert len(cases) >= 9
|
|
classes = {c["class"] for c in cases}
|
|
assert {"phantom_endpoint", "cross_pack_non_bridge", "sibling_collapse"}.issubset(
|
|
classes
|
|
)
|
|
|
|
def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
|
|
cases = runner._load_cases(tmp_path / "absent.jsonl")
|
|
assert cases == ()
|
|
|
|
def test_empty_file_returns_empty(self, tmp_path: Path) -> None:
|
|
empty = tmp_path / "empty.jsonl"
|
|
empty.write_text("", encoding="utf-8")
|
|
assert runner._load_cases(empty) == ()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration smoke (runs ONE real case via ChatRuntime)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRuntimeIntegrationSmoke:
|
|
def test_one_real_phantom_endpoint_refuses(self) -> None:
|
|
case = {
|
|
"id": "smoke_a1",
|
|
"class": "phantom_endpoint",
|
|
"prompt": "Does foozball cause grimble?",
|
|
"expected_grounding_source": ["none", "oov"],
|
|
"expected_outcome": "refusal",
|
|
}
|
|
result = runner._run_case(case)
|
|
assert result["error"] is None
|
|
assert result["refusal_observed"] is True
|
|
assert result["fabrication_marker_observed"] is False
|
|
assert result["grounding_source"] in {"none", "oov"}
|
|
assert result["grounding_source_matches_expected"] is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pinned thresholds shape
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPinnedThresholds:
|
|
def test_thresholds_match_adr_0096(self) -> None:
|
|
assert runner.PINNED_THRESHOLDS == {
|
|
"fabrication_rate_max": 0.01,
|
|
"refusal_recall_min": 0.95,
|
|
"trace_evidence_present_min": 1.00,
|
|
"grounding_source_matches_expected_min": 1.00,
|
|
}
|