core/tests/test_contemplation_quality_lane.py
Shay 8829529ed0
fix(W-025): polish contemplation-quality eval lane follow-ups (#290)
Three follow-ups raised in the W-025 PR #286 review, completed together so
the lane reaches its full mastery-level contract.

1. ``core eval`` failure-printer is now gated on ``lane_name == "cognition"``.
   Before this fix, every non-cognition lane that returned clean case_details
   without ``intent_correct``/``versor_closure`` keys triggered a spurious
   ``failures (N): <case_id>: intent, versor=0.00e+00`` block at the end of
   the human-readable output, even when every metric passed.  This matched
   the gating pattern already used for the workers preamble at the top of
   ``cmd_eval``.

2. EPILOG examples in ``core/cli.py`` now advertise
   ``core eval contemplation_quality`` and the ``--json --save`` form, so
   the lane is discoverable from ``core --help`` and not only from
   ``core eval --list``.

3. Tightened the learning-arc demo's Scene 5 to thread the demo's
   tempdir-scoped ``engine_state_dir`` into the second ``ChatRuntime``.
   The previous default-constructed runtime checkpointed to the repo's
   ``engine_state/``, which contradicted ADR-0159's read-only claim.
   ADR-0146/0150 still govern the runtime checkpoint path itself.

Tests:

- ``tests/test_contemplation_quality_lane.py`` (35 tests):
  case-set integrity, lane discovery, ``evaluate_report`` purity over
  well-formed / malformed / boundary-violating inputs, ``run_lane``
  invocation-contract enforcement (single case, supported source enum),
  and a read-only invariant snapshot on ``teaching/corpora``, ``packs/``,
  and ``language_packs/data/``.

- ``tests/test_eval_cli_failure_printer.py`` (4 tests): pins the
  cognition-only gating of the failure printer with stubbed
  ``evals.framework`` so the regression cannot return as a lane-blind
  condition.

Validation:

  uv run pytest tests/test_contemplation_quality_lane.py \
                tests/test_eval_cli_failure_printer.py \
                tests/test_learning_arc_demo.py -q   # 50 passed
  uv run core test --suite smoke -q                  # 67 passed
  uv run core eval contemplation_quality              # 9/9 passed, clean output
2026-05-26 09:39:18 -07:00

415 lines
15 KiB
Python

"""Contract tests for the ``contemplation_quality`` eval lane.
W-025 / ADR-0159. The lane scores the structured output from
``core demo learning-arc --json`` along nine deterministic, non-mutating
quality gates without widening the trust surface.
These tests pin:
- Case-set integrity (single invocation case, required schema).
- Lane discovery via the generic eval framework (no CLI wiring needed).
- ``evaluate_report`` purity over arbitrary dictionaries (well-formed,
malformed, empty, wrong types).
- ``run_lane`` input-shape enforcement (single case, source enum).
- Read-only invariant: lane execution must not produce filesystem writes
under teaching/, packs/, or engine_state/ during scoring.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from evals.framework import (
discover_lanes,
get_lane,
load_cases,
load_lane_runner,
)
from evals.contemplation_quality.runner import (
ContemplationQualityReport,
LaneReport,
QualityMetric,
evaluate_report,
run_lane,
)
LANE_NAME = "contemplation_quality"
_EVAL_ROOT = Path(__file__).resolve().parent.parent / "evals" / LANE_NAME
_PUBLIC_CASES = _EVAL_ROOT / "public" / "v1" / "cases.jsonl"
_DEV_CASES = _EVAL_ROOT / "dev" / "cases.jsonl"
_CONTRACT = _EVAL_ROOT / "contract.md"
_REQUIRED_METRIC_NAMES: frozenset[str] = frozenset({
"scene_contract",
"deterministic_replay_integrity",
"typed_contemplation_provenance",
"engine_authored_specificity",
"grounding_transition",
"downstream_gain_observed",
"active_corpus_boundary",
"pending_not_auto_accepted",
"stable_proposal_identity_present",
})
# ---------------------------------------------------------------------------
# Case-set integrity
# ---------------------------------------------------------------------------
class TestCaseSetIntegrity:
def test_public_cases_file_exists(self) -> None:
assert _PUBLIC_CASES.exists()
def test_dev_cases_file_exists(self) -> None:
assert _DEV_CASES.exists()
def test_contract_file_exists(self) -> None:
assert _CONTRACT.exists()
def test_public_case_count_is_one(self) -> None:
cases = load_cases(_PUBLIC_CASES)
assert len(cases) == 1
def test_dev_case_count_is_one(self) -> None:
cases = load_cases(_DEV_CASES)
assert len(cases) == 1
def test_case_required_fields(self) -> None:
for path in (_PUBLIC_CASES, _DEV_CASES):
for case in load_cases(path):
assert "case_id" in case and isinstance(case["case_id"], str)
assert "source" in case and isinstance(case["source"], str)
def test_case_source_is_supported_enum(self) -> None:
for path in (_PUBLIC_CASES, _DEV_CASES):
for case in load_cases(path):
assert case["source"] == "learning_arc_demo"
# ---------------------------------------------------------------------------
# Lane discovery via the generic framework
# ---------------------------------------------------------------------------
class TestLaneDiscovery:
def test_lane_is_discoverable(self) -> None:
names = {lane.name for lane in discover_lanes()}
assert LANE_NAME in names
def test_lane_has_v1_version(self) -> None:
lane = get_lane(LANE_NAME)
assert "v1" in lane.versions
def test_lane_runner_exposes_run_lane(self) -> None:
lane = get_lane(LANE_NAME)
runner = load_lane_runner(lane)
assert hasattr(runner, "run_lane")
assert hasattr(runner, "evaluate_report")
# ---------------------------------------------------------------------------
# evaluate_report — pure function over a dict
# ---------------------------------------------------------------------------
def _passing_report() -> dict:
"""Synthesize a minimal learning-arc report that satisfies every gate."""
return {
"engine_connective": "grounds",
"engine_object": "truth",
"learning_arc_closed": True,
"active_corpus_byte_identical": True,
"before": {"surface": "I don't know."},
"after": {"surface": "light grounds truth"},
"scenes": [
{
"scene": "S1_cold_session",
"detail": {"grounding_source": "none"},
},
{
"scene": "S2_checkpoint_enrichment",
"detail": {
"engine_chain_found": True,
"engine_chain": {"connective": "grounds", "object": "truth"},
},
},
{
"scene": "S3_engine_authored_proposal",
"detail": {
"source_kind": "contemplation",
"proposal_id": "proposal-abc123",
"state": "pending",
"replay_evidence": {
"replay_equivalent": True,
"regressed_metrics": [],
},
"proposed_chain": {
"connective": "grounds",
"object": "truth",
},
},
},
{
"scene": "S4_operator_ratifies",
"detail": {"active_corpus_byte_identical": True},
},
{
"scene": "S5_grounded_session",
"detail": {"grounding_source": "teaching"},
},
],
}
class TestEvaluateReportShape:
def test_returns_contemplation_quality_report(self) -> None:
report = evaluate_report(_passing_report())
assert isinstance(report, ContemplationQualityReport)
def test_lane_label_is_canonical(self) -> None:
report = evaluate_report(_passing_report())
assert report.lane == "contemplation-quality"
def test_source_label_is_canonical(self) -> None:
report = evaluate_report(_passing_report())
assert report.source == "core demo learning-arc --json"
def test_source_digest_is_sha256_hex(self) -> None:
report = evaluate_report(_passing_report())
assert isinstance(report.source_digest, str)
assert len(report.source_digest) == 64
int(report.source_digest, 16) # raises ValueError if non-hex
def test_all_nine_metrics_present(self) -> None:
report = evaluate_report(_passing_report())
names = {m.name for m in report.metrics}
assert names == _REQUIRED_METRIC_NAMES
def test_metrics_are_quality_metric_instances(self) -> None:
report = evaluate_report(_passing_report())
for metric in report.metrics:
assert isinstance(metric, QualityMetric)
class TestEvaluateReportDeterminism:
def test_same_input_yields_same_digest(self) -> None:
a = evaluate_report(_passing_report())
b = evaluate_report(_passing_report())
assert a.source_digest == b.source_digest
def test_well_formed_report_passes(self) -> None:
report = evaluate_report(_passing_report())
assert report.passed is True
def test_serializable_as_dict(self) -> None:
report = evaluate_report(_passing_report()).as_dict()
# Must be JSON-serializable without raising. Tuples in ``expected``
# values become lists after a JSON round-trip, which is fine for
# downstream consumers — the contract here is only serializability.
encoded = json.dumps(report)
decoded = json.loads(encoded)
assert decoded["lane"] == report["lane"]
assert decoded["source_digest"] == report["source_digest"]
assert decoded["passed"] is report["passed"]
assert decoded["score"] == report["score"]
assert len(decoded["metrics"]) == len(report["metrics"])
class TestEvaluateReportBoundaryViolations:
"""Each gate should fail when its specific invariant is broken."""
def _mutate_scene(
self,
report: dict,
scene_name: str,
**detail_overrides,
) -> dict:
for scene in report["scenes"]:
if scene["scene"] == scene_name:
scene["detail"] = {**scene["detail"], **detail_overrides}
return report
def _failed_metric(
self,
report: ContemplationQualityReport,
name: str,
) -> QualityMetric:
for metric in report.metrics:
if metric.name == name:
return metric
raise AssertionError(f"metric {name!r} not in report")
def test_scene_contract_fails_on_missing_scene(self) -> None:
report = _passing_report()
report["scenes"] = report["scenes"][:-1]
scored = evaluate_report(report)
assert self._failed_metric(scored, "scene_contract").passed is False
def test_replay_integrity_fails_when_not_equivalent(self) -> None:
report = self._mutate_scene(
_passing_report(),
"S3_engine_authored_proposal",
replay_evidence={
"replay_equivalent": False,
"regressed_metrics": ["surface_diff"],
},
)
scored = evaluate_report(report)
assert (
self._failed_metric(scored, "deterministic_replay_integrity").passed
is False
)
def test_pending_gate_fails_on_auto_acceptance(self) -> None:
report = self._mutate_scene(
_passing_report(),
"S3_engine_authored_proposal",
state="accepted",
)
scored = evaluate_report(report)
assert (
self._failed_metric(scored, "pending_not_auto_accepted").passed
is False
)
def test_active_corpus_boundary_fails_on_byte_drift(self) -> None:
report = _passing_report()
report["active_corpus_byte_identical"] = False
scored = evaluate_report(report)
assert (
self._failed_metric(scored, "active_corpus_boundary").passed
is False
)
def test_provenance_gate_fails_without_contemplation_kind(self) -> None:
report = self._mutate_scene(
_passing_report(),
"S3_engine_authored_proposal",
source_kind="seeded",
)
scored = evaluate_report(report)
assert (
self._failed_metric(scored, "typed_contemplation_provenance").passed
is False
)
class TestEvaluateReportMalformedInput:
"""The pure-function entry point must reject or absorb malformed shapes."""
def test_non_dict_input_raises_type_error(self) -> None:
with pytest.raises(TypeError):
evaluate_report([]) # type: ignore[arg-type]
def test_none_input_raises_type_error(self) -> None:
with pytest.raises(TypeError):
evaluate_report(None) # type: ignore[arg-type]
def test_empty_report_produces_all_failing_metrics(self) -> None:
scored = evaluate_report({})
assert scored.passed is False
# All nine metrics still emitted — none are silently skipped.
assert {m.name for m in scored.metrics} == _REQUIRED_METRIC_NAMES
def test_malformed_scenes_field_does_not_crash(self) -> None:
scored = evaluate_report({"scenes": "not-a-list"})
assert scored.passed is False
def test_malformed_scene_detail_does_not_crash(self) -> None:
scored = evaluate_report(
{"scenes": [{"scene": "S1_cold_session", "detail": "wrong-type"}]}
)
assert scored.passed is False
# ---------------------------------------------------------------------------
# run_lane — invocation-contract enforcement
# ---------------------------------------------------------------------------
class TestRunLaneInputContract:
def test_empty_case_list_rejected(self) -> None:
with pytest.raises(ValueError, match="exactly one"):
run_lane([])
def test_multiple_cases_rejected(self) -> None:
with pytest.raises(ValueError, match="exactly one"):
run_lane([
{"case_id": "a", "source": "learning_arc_demo"},
{"case_id": "b", "source": "learning_arc_demo"},
])
def test_non_list_input_rejected(self) -> None:
with pytest.raises(ValueError):
run_lane("not-a-list") # type: ignore[arg-type]
def test_non_dict_case_rejected(self) -> None:
with pytest.raises(TypeError):
run_lane(["not-a-dict"]) # type: ignore[list-item]
def test_unsupported_source_rejected(self) -> None:
with pytest.raises(ValueError, match="unsupported"):
run_lane([{"case_id": "x", "source": "external_dataset_v2"}])
# ---------------------------------------------------------------------------
# Read-only invariant — execution must not write outside tempdirs
# ---------------------------------------------------------------------------
class TestReadOnlyInvariant:
"""ADR-0159 read-only invariants.
The lane must never mutate the active teaching corpus or any pack data
file. These are the trust boundaries the proposal/teaching path
protects: corpus mutation requires ``accept_proposal``, pack mutation
requires a reviewed pack-mutation ADR path, and neither is exercised
by the eval lane.
Note on ``engine_state/``: the lane's downstream demo (run_demo) runs
a replay-equivalence gate that spawns the cognition lane, whose
per-case ``ChatRuntime`` instances checkpoint to ``engine_state/`` via
the runtime path already governed by ADR-0146/0150. That checkpoint
surface is a transient runtime artifact, not a teaching/pack write,
so it is explicitly out of scope for this invariant.
"""
def _snapshot(self, root: Path) -> dict[str, bytes]:
snap: dict[str, bytes] = {}
if not root.exists():
return snap
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(root)
# Skip Python bytecode caches and the package's own
# ``__init__.py`` — they are not corpus/pack content.
if "__pycache__" in rel.parts or rel.suffix in {".pyc", ".pyo"}:
continue
snap[str(rel)] = path.read_bytes()
return snap
def test_run_lane_does_not_mutate_teaching_or_packs(self) -> None:
repo_root = Path(__file__).resolve().parent.parent
guarded = {
"teaching/corpora": repo_root / "teaching" / "corpora",
"packs": repo_root / "packs",
"language_packs/data": repo_root / "language_packs" / "data",
}
before = {k: self._snapshot(v) for k, v in guarded.items()}
result = run_lane(load_cases(_PUBLIC_CASES))
after = {k: self._snapshot(v) for k, v in guarded.items()}
for key in guarded:
assert before[key] == after[key], (
f"lane execution mutated {key} — trust boundary violated"
)
assert isinstance(result, LaneReport)
assert result.metrics["all_passed"] is True