core/tests/test_cli_demo.py
Shay 531d4aa0d1 fix(tests): isolate three xdist polluter clusters blocking -n auto default
Hunt for the -n auto fast-lane polluters flagged in docs/testing-lanes.md
("Follow-up: xdist by default"). Three root causes, all shared-repo-path
writers with no per-test isolation, following the #782 monkeypatch idiom:

1. tests/test_workbench_replay.py::test_replay_leaves_no_trace hardcoded
   `Path("engine_state")` (the real shared dir) instead of reading
   `engine_state._DEFAULT_DIR`, which the root-conftest autouse fixture
   already redirects per-test. It was a victim, not a polluter: any
   concurrent worker writing the real dir made this snapshot-diff flake.
   Fixed by reading `engine_state._DEFAULT_DIR` dynamically.

2. evals/gsm8k_math/train_sample/v1/runner.py hardcoded its report.json
   output to the committed repo path with no override. Two test files
   (test_rat1_end_to_end_admission.py, test_wave_a_multiplicative_
   aggregation_injector.py; 4 tests total) spawn it as a subprocess and
   read the same file back — a write race under -n auto, and a confirmed
   downstream victim (test_gsm8k_sealed_attempt_scout.py::
   test_report_json_mtime_unchanged_by_scout_import asserts the file's
   mtime is stable). Added an optional CORE_GSM8K_TRAIN_SAMPLE_REPORT_PATH
   env override (default unchanged) and pointed the 4 call sites at
   tmp_path.

3. core/cli.py's `_DEMO_RESULTS_DIR` (evals/forward_semantic_control/
   results/) is written, glob-scanned, and index.json-rebuilt by ~11 tests
   across tests/test_cli_demo.py's TestDemoSubcommand and TestDemoPreambles
   classes. Added an autouse fixture monkeypatching `cli._DEMO_RESULTS_DIR`
   to a per-test tmp dir. This also unmasked a latent order-dependent
   coupling: test_demo_list_results_indexes_reports and
   test_demo_list_results_json_well_formed never wrote their own report,
   relying on a sibling test's leftover file in the shared dir (silently
   correct only because pytest ran the file in declaration order). Made
   both self-contained.

No assertion weakening, no test deletion, no global autouse fixture masking
real bugs. All three fixes preserve the real (non-test) default behavior
byte-for-byte when unpatched/env-unset.

Verification: 8x targeted -n 8 loop over the 7 affected files (before and
after) did not force-reproduce the underlying race live (narrow timing
window, small-scale run) — confirmation is source-level (hardcoded shared
paths bypassing the established isolation idiom) plus the prior documented
flake for test_replay_leaves_no_trace in docs/testing-lanes.md. Full
fast-lane -n auto run recorded in the PR description.
2026-07-15 16:46:08 -07:00

250 lines
9.8 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
@pytest.fixture(autouse=True)
def _isolate_demo_results_dir(tmp_path, monkeypatch):
"""Redirect the demo results dir to an isolated per-test tmp dir.
``core.cli._DEMO_RESULTS_DIR`` is a module-level constant pointing at the
shared repo dir ``evals/forward_semantic_control/results/`` that every
``core demo`` subcommand reads, writes, and glob-scans (including the
``index.json`` rebuild in ``_write_results_index``). Multiple tests below
invoke ``cli.main(["demo", ...])``; left unpatched they race on that one
shared directory under ``-n auto`` (concurrent report writes plus a
scan-then-rewrite of ``index.json``). Follows the same monkeypatch-the-
module-attribute isolation idiom as the root-conftest engine-state fixture
(#782). ``TestResultsReadme`` reads the real checked-in README by a
hardcoded path, so it is unaffected by this redirection.
"""
monkeypatch.setattr(cli, "_DEMO_RESULTS_DIR", tmp_path / "results")
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 = cli._DEMO_RESULTS_DIR / "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:
# Self-contained: write a report before indexing rather than relying
# on a sibling test's shared-directory side effect (that implicit
# ordering coupling only "worked" by accident of file-declaration
# order and would break under `-n auto` reordering).
cli.main(["demo", "phase6"])
capsys.readouterr()
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:
# Self-contained for the same reason as
# test_demo_list_results_indexes_reports above.
cli.main(["demo", "phase6"])
capsys.readouterr()
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 = cli._DEMO_RESULTS_DIR / "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
class TestDemoPreambles:
"""Pin the preamble explanations so they don't drift silently."""
def test_phase6_preamble_explains_three_conditions(self, capsys) -> None:
cli.main(["demo", "phase6"])
out = capsys.readouterr().out
assert "WHAT THIS DEMO TESTS" in out
assert "C1 Replay determinism" in out
assert "C2 Traced rejection" in out
assert "C3 Coherent refusal" in out
assert "WHAT TO EXPECT" in out
assert "WHEN TO TWEAK" in out
def test_phase6_preamble_states_in_system_baseline(self, capsys) -> None:
cli.main(["demo", "phase6"])
out = capsys.readouterr().out
# The "why not a transformer LLM" explanation must be present.
assert "ADR-0023 ablation" in out
assert "non-deterministic" in out or "Non-deterministic" in out
def test_phase5_preamble_explains_five_families(self, capsys) -> None:
cli.main(["demo", "phase5"])
out = capsys.readouterr().out
assert "WHAT THIS DEMO TESTS" in out
for family in (
"near_forbidden_correct_endpoint",
"near_equal_admissible",
"no_admissible_path",
"multi_step_admissibility",
"heterogeneous_relation",
):
assert family in out
assert "WHAT TO LOOK FOR" in out
def test_phase5_preamble_states_delta_falsifiable(self, capsys) -> None:
cli.main(["demo", "phase5"])
out = capsys.readouterr().out
assert "FALSIFIABLE" in out or "falsifiable" in out
def test_preamble_suppressed_under_json(self, capsys) -> None:
cli.main(["demo", "phase6", "--json"])
out = capsys.readouterr().out
# No preamble text should leak into --json mode.
assert "WHAT THIS DEMO TESTS" not in out
# Output must be parseable JSON from the first character.
payload = json.loads(out.split("\n\n")[0])
assert "metrics" in payload
def test_all_preamble_explains_combined_run(self, capsys) -> None:
cli.main(["demo", "all"])
out = capsys.readouterr().out
assert "Combined Demo" in out
# Both phase preambles fire for `demo all`.
assert "Phase 5 Demo" in out
assert "Phase 6 Demo" in out
# Combined summary at the end.
assert "Combined demo summary" in out
assert "load-bearing claim of the ADR-0024 chain" in out
class TestResultsReadme:
"""The results/ directory ships with an explanatory README so cold readers
can interpret each report without spelunking the runner source."""
def test_results_readme_exists(self) -> None:
readme = Path("evals/forward_semantic_control/results/README.md")
assert readme.exists()
text = readme.read_text()
# The README must explicitly call out each phase's report file.
for fname in (
"phase5_report.json",
"phase6_demo_report.json",
"phase5_benign_inner_loop_report.json",
"phase4_characterization",
"phase3_v2_report.json",
"phase2_inner_loop_report.json",
):
assert fname in text, f"{fname} missing from results/README.md"
def test_corpus_readmes_exist(self) -> None:
for path in (
"evals/forward_semantic_control/public/v2_phase5/README.md",
"evals/forward_semantic_control/public/v2_phase6_demo/README.md",
"evals/forward_semantic_control/public/inner_loop_benign/README.md",
):
assert Path(path).exists(), f"{path} missing"