core/tests/test_cli_demo.py
Shay 36aad75202 feat(cli): ADR-0024 chain test-suite aliases + core demo subcommand
Two layers of CLI surface so reviewers / investors / industry
observers can run the ADR-0024 chain evidence end-to-end without
typing test file paths or hunting for runner scripts.

Layer 1 — test-suite aliases:
  core test --suite refusal     (Phase 2 typed refusals)
  core test --suite margin      (Phase 3 / ADR-0026 ranked-with-margin)
  core test --suite rotor       (Phase 4 / ADR-0025 rotor admissibility)
  core test --suite inner-loop  (ADR-0024 inner-loop, all 4 sub-tests)
  core test --suite phase5      (stratified mechanism-isolation)
  core test --suite phase6      (3-condition comparative demo)
  core test --suite adr-0024    (full chain, 98 tests, ~2 min)

Layer 2 — `core demo` subcommand:
  core demo phase5              stratified pass/refuse table + per-family
                                breakdown (5 families, both modes)
  core demo phase6              three head-to-head verdicts vs baseline
                                (replay determinism / traced rejection /
                                coherent refusal)
  core demo all                 both phases + combined summary
  core demo list-results        index every JSON report in the central
                                results directory with headline metrics

All demo runs:
  - Write fresh JSON to evals/forward_semantic_control/results/
  - Refresh the results/index.json manifest so reviewers see every
    available report in one place
  - Accept --json for machine-readable output

Central results directory: evals/forward_semantic_control/results/
  phase2_inner_loop_report.json
  phase3_v2_report.json
  phase4_characterization_*.json
  phase5_report.json
  phase5_benign_inner_loop_report.json
  phase6_demo_report.json
  index.json (auto-generated manifest)

Files:
  core/cli.py                   — +9 suite aliases, +cmd_demo (3 targets +
                                  list-results), +index manifest writer
  tests/test_cli_demo.py        — 14 contract tests pinning Layer 1 + 2
  evals/forward_semantic_control/results/index.json — auto-generated

Tests: 1099 passed, 2 skipped (+14 from Phase 6 baseline).
2026-05-17 16:21:37 -07:00

134 lines
4.9 KiB
Python

"""CLI tests for the ADR-0024 chain demo subcommand."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from core import cli
class TestADR0024SuiteAliases:
"""Layer 1: pin the new suite aliases so they don't drift."""
@pytest.mark.parametrize(
"suite,must_contain",
[
("refusal", "tests/test_refusal_contract.py"),
("margin", "tests/test_margin_admissibility.py"),
("rotor", "tests/test_rotor_admissibility.py"),
("inner-loop", "tests/test_inner_loop_admissibility.py"),
("phase5", "tests/test_phase5_corpus.py"),
("phase6", "tests/test_phase6_demo.py"),
],
)
def test_suite_alias_resolves(
self, monkeypatch, suite: str, must_contain: str
) -> None:
calls: list[tuple[str, ...]] = []
def fake_run(*args: str, check: bool = False, cwd=None) -> int:
calls.append(args)
return 0
monkeypatch.setattr(cli, "_run", fake_run)
rc = cli.main(["test", "--suite", suite, "-q"])
assert rc == 0
assert calls, f"no _run invocation for suite {suite!r}"
assert must_contain in calls[0]
def test_adr_0024_alias_runs_full_chain(self, monkeypatch) -> None:
calls: list[tuple[str, ...]] = []
monkeypatch.setattr(
cli, "_run", lambda *a, **kw: (calls.append(a) or 0)
)
rc = cli.main(["test", "--suite", "adr-0024", "-q"])
assert rc == 0
command = calls[0]
# All six Phase 2-6 contract files must be present.
for path in (
"tests/test_refusal_contract.py",
"tests/test_margin_admissibility.py",
"tests/test_rotor_admissibility.py",
"tests/test_phase5_corpus.py",
"tests/test_phase6_demo.py",
):
assert path in command, f"missing {path} in adr-0024 expansion"
def test_list_suites_includes_new_aliases(self, capsys) -> None:
rc = cli.main(["test", "--list-suites"])
captured = capsys.readouterr()
assert rc == 0
out = captured.out.splitlines()
for alias in (
"adr-0024",
"refusal",
"margin",
"rotor",
"inner-loop",
"phase5",
"phase6",
):
assert alias in out, f"alias {alias!r} missing from --list-suites"
class TestDemoSubcommand:
"""Layer 2: pin the `core demo` subcommand surface."""
def test_demo_help_lists_targets(self, capsys) -> None:
with pytest.raises(SystemExit) as exc:
cli.main(["demo", "--help"])
assert exc.value.code == 0
captured = capsys.readouterr()
for target in ("phase5", "phase6", "all", "list-results"):
assert target in captured.out
def test_demo_phase6_runs_and_writes_report(self, capsys) -> None:
rc = cli.main(["demo", "phase6"])
assert rc == 0
captured = capsys.readouterr()
# Headline text on stdout.
assert "ALL THREE CONDITIONS" in captured.out
assert "PASS" in captured.out
# Report file present and well-formed.
report = Path("evals/forward_semantic_control/results/phase6_demo_report.json")
assert report.exists()
data = json.loads(report.read_text())
assert data["metrics"]["all_three_conditions_pass"] is True
def test_demo_phase6_json_emits_machine_readable(self, capsys) -> None:
rc = cli.main(["demo", "phase6", "--json"])
assert rc == 0
captured = capsys.readouterr()
# First non-blank chunk of stdout must be a parseable JSON
# object containing the headline keys.
payload = json.loads(captured.out.split("\n\n")[0])
assert "metrics" in payload
assert "all_three_conditions_pass" in payload["metrics"]
def test_demo_list_results_indexes_reports(self, capsys) -> None:
rc = cli.main(["demo", "list-results"])
assert rc == 0
captured = capsys.readouterr()
assert "results directory" in captured.out
assert "phase6_demo_report.json" in captured.out
def test_demo_list_results_json_well_formed(self, capsys) -> None:
rc = cli.main(["demo", "list-results", "--json"])
assert rc == 0
captured = capsys.readouterr()
data = json.loads(captured.out)
assert "results_dir" in data
assert isinstance(data["reports"], list)
names = [e["file"] for e in data["reports"]]
assert "phase6_demo_report.json" in names
def test_demo_index_file_refreshed_after_run(self) -> None:
cli.main(["demo", "phase6"])
index_path = Path("evals/forward_semantic_control/results/index.json")
assert index_path.exists()
data = json.loads(index_path.read_text())
names = [e["file"] for e in data["reports"]]
assert "phase6_demo_report.json" in names