#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Regression suite for ``pdg_regression.py`` (the PDG regression engine). PM-AGENT governance honoured here: * RUNNABLE-FIRST — stdlib only (csv, json, hashlib, pathlib, subprocess, importlib, shutil, tempfile, sys, os). pytest is the only third-party requirement, and that is the test runner itself. The engine and its config parse with zero pip installs; a dedicated test forces the stdlib flat-YAML / config.json fallback path. * FAIL-CLOSED — ``test_fail_closed_missing_units`` and friends feed deliberately broken rows (missing units / missing claim_class / illegal claim_class / dangling evidence_id) and assert the engine produces a FAIL and exits non-zero (both in-process and via the CLI). * CITE EVERY CONST — the residual test re-derives Delta = T - O and z = (T-O)/sqrt(sigT^2+sigO^2) from the engine's own output bytes and checks them against the CSV's stored values; nothing is asserted trivially. * TESTS MANDATORY — every invariant assertion below runs the real engine (``run`` / ``main`` / a CLI subprocess) and asserts on the actual files written to disk. There are no stubbed or trivially-true assertions standing in for a check. Safe wording (binding, verbatim from the engine): The regression suite does not prove the theory true. It prevents the document from making claims that are unsupported by its own tables, methods, and declared evidence. """ from __future__ import annotations import csv import importlib.util import json import shutil import subprocess import sys from pathlib import Path from typing import Dict, List import pytest # --------------------------------------------------------------------------- # # Locate and import the engine by absolute path (no install / sys.path games). # # --------------------------------------------------------------------------- # TESTS_DIR = Path(__file__).resolve().parent PROJECT_DIR = TESTS_DIR.parent ENGINE_PATH = PROJECT_DIR / "pdg_regression.py" DATA_DIR = PROJECT_DIR / "data" CONFIG_YAML = PROJECT_DIR / "validation_config.yaml" CONFIG_JSON = PROJECT_DIR / "config.json" assert ENGINE_PATH.exists(), f"engine not found at {ENGINE_PATH}" assert DATA_DIR.exists(), f"frozen data dir not found at {DATA_DIR}" def _load_engine(): spec = importlib.util.spec_from_file_location("pdg_regression", ENGINE_PATH) assert spec and spec.loader mod = importlib.util.module_from_spec(spec) # Register before exec: @dataclass resolves cls.__module__ via sys.modules. sys.modules["pdg_regression"] = mod spec.loader.exec_module(mod) return mod eng = _load_engine() # The seven mandated outputs (mirrors eng.OUTPUT_FILES, asserted equal below). EXPECTED_OUTPUTS = ( "validation_report.md", "validation_report.json", "failed_claims.csv", "pending_claims.csv", "release_gate_summary.csv", "residuals_by_sector.csv", "claim_class_audit.csv", ) # --------------------------------------------------------------------------- # # Small helpers # # --------------------------------------------------------------------------- # def _read_csv(path: Path) -> List[Dict[str, str]]: with path.open("r", encoding="utf-8-sig", newline="") as fh: return list(csv.DictReader(fh)) def _copy_frozen_data(dst: Path) -> Path: """Copy the six frozen input CSVs into a scratch data dir.""" dst.mkdir(parents=True, exist_ok=True) for name in eng.INPUT_CSVS: shutil.copy(DATA_DIR / name, dst / name) return dst def _run_engine(tmp_path: Path, data_dir: Path = None, config_path: Path = None): """Run the engine in-process; return (exit_code, out_dir, json_report).""" data_dir = data_dir or _copy_frozen_data(tmp_path / "data") out_dir = tmp_path / "out" config_path = config_path or CONFIG_YAML code = eng.run(Path(data_dir), out_dir, Path(config_path)) report = None rp = out_dir / "validation_report.json" if rp.exists(): report = json.loads(rp.read_text(encoding="utf-8")) return code, out_dir, report # A single shared "clean run on the frozen data" fixture so the many tests that # inspect the real outputs do not each re-run the whole engine. @pytest.fixture(scope="module") def clean_run(tmp_path_factory): base = tmp_path_factory.mktemp("clean") code, out_dir, report = _run_engine(base) return {"code": code, "out_dir": out_dir, "report": report, "base": base} # =========================================================================== # # The engine MUST exit 0 (PASS) on the frozen, self-consistent dataset. # # =========================================================================== # def test_engine_passes_on_frozen_data(clean_run): """Baseline: the frozen ./data is self-consistent => exit 0, no FAILs.""" assert clean_run["code"] == 0, "engine must exit 0 on the clean frozen data" report = clean_run["report"] assert report is not None assert report["totals"]["fail"] == 0, ( f"expected 0 FAIL findings on frozen data, " f"got {report['totals']['fail']}" ) assert report["release_gate_pass"] is True # =========================================================================== # # Test 1 — required fields present (incl. units + 'source' citation). # # =========================================================================== # def test_t1_missing_units_fails(tmp_path): """A numeric theory row with its units blanked must FAIL Test 1.""" data = _copy_frozen_data(tmp_path / "data") theory = data / "theory_outputs.csv" rows = _read_csv(theory) # m_e(M_Z) is a numeric, non-pending row: blank its units. hit = False for r in rows: if r["observable"] == "m_e(M_Z)": r["units"] = "" hit = True assert hit, "fixture row m_e(M_Z) not found in frozen theory_outputs.csv" _write_theory(theory, rows) res = eng.Results() eng.test1_required_fields([_lc(r) for r in rows], res) t1_fails = [f for f in res.fails if f.test_id == 1 and "units" in f.reason] assert t1_fails, "blanking units on a numeric row must raise a Test-1 FAIL" def test_t1_missing_source_fails(): """A numeric value with no 'source' citation must FAIL Test 1 (cite every const).""" rows = [{ "particle_family": "charged_lepton", "observable": "m_e(M_Z)", "value_or_pending": "0.4869", "units": "MeV", "method": "frozen_O_e_chamber", "claim_class": "predicted", "status": "pass", "source": "", # <- missing }] res = eng.Results() eng.test1_required_fields(rows, res) assert any(f.test_id == 1 and "source" in f.reason for f in res.fails), \ "a numeric value without a source citation must FAIL Test 1" def test_t1_pending_value_is_allowed(): """A 'pending' value marker is acceptable and must not FAIL Test 1 for value/source.""" rows = [{ "particle_family": "meson", "observable": "m_pi_charged", "value_or_pending": "pending", "units": "MeV", "method": "lattice_import", "claim_class": "imported", "status": "pass", "source": "companion stage3.md sec2.x", }] res = eng.Results() eng.test1_required_fields(rows, res) assert not res.fails, f"a pending row should not FAIL Test 1, got {[f.reason for f in res.fails]}" # =========================================================================== # # Test 2 — claim-class validity. # # =========================================================================== # def test_t2_illegal_claim_class_fails(): cfg = eng.Config() rows_by_file = {"theory_outputs.csv": [{ "particle_family": "charged_lepton", "observable": "m_e(M_Z)", "claim_class": "totally_made_up_class", "status": "pass", }]} res = eng.Results() eng.test2_claim_class_validity(rows_by_file, cfg, res) assert any(f.test_id == 2 for f in res.fails), \ "an out-of-vocabulary claim_class must FAIL Test 2" def test_t2_alias_is_normalised_and_passes(): """'compatible only' / 'falsification target' are aliases, not failures.""" cfg = eng.Config() rows_by_file = {"theory_outputs.csv": [ {"observable": "x", "claim_class": "compatible only", "status": "pass"}, {"observable": "y", "claim_class": "falsification target", "status": "anomaly"}, ]} res = eng.Results() eng.test2_claim_class_validity(rows_by_file, cfg, res) assert not [f for f in res.fails if f.test_id == 2], \ "known aliases must normalise onto allowed classes, not FAIL" def test_t2_all_frozen_classes_are_legal(clean_run): """Every claim_class in the real audit output is in the allowed set.""" audit = _read_csv(clean_run["out_dir"] / "claim_class_audit.csv") allowed = set(clean_run["report"]["allowed_claim_classes"]) assert audit, "claim_class_audit.csv must not be empty" for r in audit: assert r["claim_class"] in allowed, \ f"audit row {r['claim_id']} has illegal class {r['claim_class']}" # =========================================================================== # # Test 3 — prediction / fit separation. # # =========================================================================== # def test_t3_predicted_marked_as_fit_input_fails(): cfg = eng.Config() # fail_on_prediction_fit_conflict defaults True theory = [{ "particle_family": "ckm", "observable": "V_ub", "claim_class": "predicted", "status": "pass", "fit_input": "true", "method": "diagonalized", }] res = eng.Results() eng.test3_prediction_fit_separation(theory, cfg, res) assert any(f.test_id == 3 for f in res.fails), \ "a 'predicted' observable flagged as a fit input must FAIL Test 3" def test_t3_clean_predicted_passes(): cfg = eng.Config() theory = [{ "particle_family": "ckm", "observable": "V_ub", "claim_class": "predicted", "status": "pass", "method": "diagonalized_frozen_Y_u_Y_d", }] res = eng.Results() eng.test3_prediction_fit_separation(theory, cfg, res) assert not res.fails, "a clean predicted row must not FAIL Test 3" # =========================================================================== # # Test 4 — residual calculation (Delta and z recomputed, not asserted-trivial).# # =========================================================================== # def test_t4_residuals_recomputed_from_output_bytes(clean_run): """Re-derive Delta = T - O from the residuals output and assert agreement. This reads the bytes the engine actually wrote and recomputes the residual independently; it is not a trivially-true stub. """ residuals = _read_csv(clean_run["out_dir"] / "residuals_by_sector.csv") assert residuals, "residuals_by_sector.csv must contain numeric rows" checked = 0 for r in residuals: t = float(r["T_value"]) o = float(r["O_value"]) expected_delta = t - o assert abs(float(r["delta"]) - expected_delta) <= 1e-6 + 1e-6 * abs(expected_delta), ( f"delta mismatch for {r['particle']}/{r['observable']}: " f"stored {r['delta']} vs recomputed {expected_delta}" ) # z = (T - O) / sqrt(sigT^2 + sigO^2) when both sigmas are present. if r["z"] not in ("", None) and r["T_sigma"] not in ("", None) and r["O_sigma"] not in ("", None): st = float(r["T_sigma"]) so = float(r["O_sigma"]) denom = (st * st + so * so) ** 0.5 if denom > 0: expected_z = expected_delta / denom assert abs(float(r["z"]) - expected_z) <= 1e-6 + 1e-6 * abs(expected_z), ( f"z mismatch for {r['particle']}/{r['observable']}: " f"stored {r['z']} vs recomputed {expected_z}" ) checked += 1 assert checked >= 10, f"expected many numeric residual rows, only checked {checked}" def test_t4_missing_residual_inputs_fail(): """A predicted/computed comparison row with non-numeric T or O must FAIL Test 4.""" comp = [{ "particle": "electron", "observable": "m_e(M_Z)", "t_value": "pending", "o_value": "0.48657", "units": "MeV", "claim_class": "predicted", "status": "pass", }] res = eng.Results() eng.test4_residual_calculation(comp, res) assert any(f.test_id == 4 and f.severity == "FAIL" for f in res.fails), \ "a predicted comparison row that cannot form a residual must FAIL Test 4" def test_t4_known_residual_value(): """Spot-check one residual against a hand value: T=0.4869, O=0.48657 => 0.00033.""" comp = [{ "particle": "electron", "observable": "m_e(M_Z)", "t_value": "0.4869", "t_sigma": "0.0050", "o_value": "0.48657", "o_sigma": "0.00007", "units": "MeV", "claim_class": "predicted", "status": "pass", }] res = eng.Results() eng.test4_residual_calculation(comp, res) assert res.residual_rows, "expected a residual row" delta = res.residual_rows[0]["delta"] assert abs(delta - 0.00033) < 1e-9, f"hand-computed delta 0.00033 != {delta}" # =========================================================================== # # Test 5 — pending cannot be pass. # # =========================================================================== # def test_t5_pending_with_pass_status_fails(): rows_by_file = {"theory_outputs.csv": [{ "observable": "x", "claim_class": "pending", "status": "pass", }]} res = eng.Results() eng.test5_pending_not_pass(rows_by_file, res) assert any(f.test_id == 5 for f in res.fails), \ "claim_class='pending' with status='pass' must FAIL Test 5" def test_t5_pending_with_pending_status_ok(): rows_by_file = {"theory_outputs.csv": [{ "observable": "x", "claim_class": "pending", "status": "pending", }]} res = eng.Results() eng.test5_pending_not_pass(rows_by_file, res) assert not res.fails, "pending+pending must not FAIL Test 5" # =========================================================================== # # Test 6 — tentative state marked final needs a confidence note. # # =========================================================================== # def test_t6_tentative_final_without_note_fails(): rows_by_file = {"claims_registry.csv": [{ "claim_id": "CLM-X", "observable": "exotic_state", "statement": "tentative single-experiment state, closure final", "claim_class": "predicted", "status": "pass", # final closure }]} res = eng.Results() eng.test6_tentative_needs_note(rows_by_file, res) assert any(f.test_id == 6 for f in res.fails), \ "a tentative state marked as a final closure with no note must FAIL Test 6" def test_t6_tentative_with_confidence_note_ok(): rows_by_file = {"claims_registry.csv": [{ "claim_id": "CLM-X", "observable": "exotic_state", "statement": "tentative single-experiment state", "confidence_note": "tentative; quarantined with low confidence pending PDG upgrade", "claim_class": "predicted", "status": "pass", }]} res = eng.Results() eng.test6_tentative_needs_note(rows_by_file, res) assert not res.fails, \ "a tentative state carrying a confidence note must not FAIL Test 6" # =========================================================================== # # Test 7 — broad resonance must not be treated as an exact stable mass. # # =========================================================================== # def test_t7_broad_resonance_exact_warns_by_default(): """With fail_on_broad_resonance_exact=False (default) a zero-width broad resonance is a WARN, not a FAIL.""" cfg = eng.Config(fail_on_broad_resonance_exact=False) rows_by_file = {"pdg_comparison_master.csv": [{ "particle": "rho_770", "observable": "broad resonance pole_mass", "t_value": "775.0", "t_sigma": "0", "o_value": "775.26", "o_sigma": "0", "claim_class": "compatible_only", "status": "pass", }]} res = eng.Results() eng.test7_broad_resonance(rows_by_file, cfg, res) assert any(f.test_id == 7 and f.severity == "WARN" for f in res.warns), \ "a zero-width broad resonance must at least WARN under Test 7" assert not [f for f in res.fails if f.test_id == 7], \ "default config must NOT make Test 7 a hard FAIL" def test_t7_broad_resonance_exact_fails_when_configured(): """With fail_on_broad_resonance_exact=True the same row is a hard FAIL.""" cfg = eng.Config(fail_on_broad_resonance_exact=True) rows_by_file = {"pdg_comparison_master.csv": [{ "particle": "rho_770", "observable": "broad resonance pole_mass exact stable", "t_value": "775.0", "t_sigma": "0", "o_value": "775.26", "o_sigma": "0", "claim_class": "compatible_only", "status": "pass", }]} res = eng.Results() eng.test7_broad_resonance(rows_by_file, cfg, res) assert any(f.test_id == 7 and f.severity == "FAIL" for f in res.fails), \ "with fail_on_broad_resonance_exact=True the broad resonance must FAIL" # =========================================================================== # # Test 8 — release-gate coverage. # # =========================================================================== # def test_t8_all_required_sectors_pass_on_frozen_data(clean_run): """Every config-required sector reaches gate=PASS on the frozen data.""" gate = _read_csv(clean_run["out_dir"] / "release_gate_summary.csv") by_sector = {g["sector"]: g for g in gate} cfg = eng.load_config(CONFIG_YAML) for sector in cfg.required_sectors: assert sector in by_sector, f"required sector {sector} missing from gate summary" assert by_sector[sector]["gate"] == "PASS", ( f"required sector {sector} did not PASS the gate: {by_sector[sector]}" ) def test_t8_missing_evidence_breaks_gate(): """Drop a sector's evidence link and the gate must FAIL for that sector.""" cfg = eng.load_config(CONFIG_YAML) rows_by_file = { "theory_outputs.csv": [{"particle_family": "charged_lepton", "observable": "m_e(M_Z)", "value_or_pending": "0.4869", "units": "MeV", "claim_class": "predicted", "status": "pass"}], "pdg_comparison_master.csv": [{"particle": "electron", "observable": "m_e(M_Z)", "claim_class": "predicted", "status": "pass"}], # claim has NO evidence_id -> evidence completeness fails for leptons. "claims_registry.csv": [{"claim_id": "CLM-001", "sector": "leptons", "claim_class": "predicted", "status": "pass", "evidence_id": ""}], "evidence_register.csv": [], } res = eng.Results() gate_ok = eng.test8_release_gate(rows_by_file, cfg, res) assert gate_ok is False, "a sector with no evidence must make the gate unsatisfiable" assert any(f.test_id == 8 for f in res.fails) # =========================================================================== # # INTEGRATION — run the engine on the frozen CSVs; all 7 outputs + consistency.# # =========================================================================== # def test_integration_all_seven_outputs_and_self_consistent(clean_run): out_dir = clean_run["out_dir"] report = clean_run["report"] # 1. Exactly the seven mandated outputs exist and are non-empty. assert set(EXPECTED_OUTPUTS) == set(eng.OUTPUT_FILES), \ "engine OUTPUT_FILES drifted from the documented seven" for name in EXPECTED_OUTPUTS: p = out_dir / name assert p.exists(), f"missing mandated output {name}" assert p.stat().st_size > 0, f"output {name} is empty" # 2. failed_claims.csv row count == report FAIL total. failed_rows = _read_csv(out_dir / "failed_claims.csv") assert len(failed_rows) == report["totals"]["fail"], ( f"failed_claims.csv has {len(failed_rows)} rows but report says " f"{report['totals']['fail']} FAILs" ) # 3. release_gate_summary.csv covers every required sector exactly once. gate_rows = _read_csv(out_dir / "release_gate_summary.csv") cfg = eng.load_config(CONFIG_YAML) gate_sectors = [g["sector"] for g in gate_rows] for s in cfg.required_sectors: assert gate_sectors.count(s) == 1, f"sector {s} not covered once in gate summary" # 4. release_gate_pass in JSON is consistent with the exit code and the gate rows. all_gate_pass = all(g["gate"] == "PASS" for g in gate_rows) assert report["release_gate_pass"] == (all_gate_pass and report["totals"]["fail"] == 0) assert (clean_run["code"] == 0) == report["release_gate_pass"] # 5. pending_claims.csv only contains genuinely pending rows, and the count # matches the JSON report's pending list length. pending_rows = _read_csv(out_dir / "pending_claims.csv") assert len(pending_rows) == len(report["pending_claims"]) for r in pending_rows: assert r["claim_class"] == "pending" or r["status"] == "pending", ( f"pending_claims row {r} is neither class nor status pending" ) # 6. claim_class_audit.csv has one row per claim in claims_registry.csv. audit_rows = _read_csv(out_dir / "claim_class_audit.csv") claims = _read_csv(DATA_DIR / "claims_registry.csv") assert len(audit_rows) == len(claims), ( f"audit has {len(audit_rows)} rows vs {len(claims)} claims" ) # 7. residuals_by_sector.csv: every row has a numeric delta and a known sector. residuals = _read_csv(out_dir / "residuals_by_sector.csv") assert residuals, "expected residual rows on the frozen data" for r in residuals: float(r["delta"]) # raises if non-numeric assert r["sector"], "residual row missing sector" # 8. validation_report.md carries the binding safe-wording disclaimer. md = (out_dir / "validation_report.md").read_text(encoding="utf-8") assert eng.SAFE_WORDING in md assert eng.SAFE_WORDING == report["safe_wording"] def test_integration_json_md_agree(clean_run): """The markdown verdict and the JSON release_gate_pass agree.""" report = clean_run["report"] md = (clean_run["out_dir"] / "validation_report.md").read_text(encoding="utf-8") if report["release_gate_pass"]: assert "PASS — releasable" in md else: assert "FAIL — not releasable" in md # =========================================================================== # # FAIL-CLOSED — broken inputs must produce a FAIL and a non-zero exit. # # =========================================================================== # def test_fail_closed_missing_units_in_process(tmp_path): """Blank the units on a numeric theory row => engine must exit non-zero.""" data = _copy_frozen_data(tmp_path / "data") theory = data / "theory_outputs.csv" rows = _read_csv(theory) n = 0 for r in rows: # strip units from every numeric, non-pending row to be unambiguous if r["value_or_pending"] and r["value_or_pending"].lower() != "pending": try: float(str(r["value_or_pending"]).replace(",", "").split()[0]) except (ValueError, IndexError): continue r["units"] = "" n += 1 assert n > 0, "fixture should have blanked at least one numeric row's units" _write_theory(theory, rows) code, out_dir, report = _run_engine(tmp_path, data_dir=data) assert code != 0, "missing units on numeric rows MUST fail closed (non-zero exit)" assert report["totals"]["fail"] > 0 failed = _read_csv(out_dir / "failed_claims.csv") assert any("units" in r["reason"] or r["test_id"] == "1" for r in failed), \ "the failure report must name the missing-units cause" def test_fail_closed_missing_units_via_cli(tmp_path): """Same broken row, but exercised through the real CLI subprocess.""" data = _copy_frozen_data(tmp_path / "data") theory = data / "theory_outputs.csv" rows = _read_csv(theory) for r in rows: if r["observable"] == "m_e(M_Z)": r["units"] = "" _write_theory(theory, rows) out_dir = tmp_path / "out" proc = subprocess.run( [sys.executable, str(ENGINE_PATH), "--data", str(data), "--out", str(out_dir), "--config", str(CONFIG_YAML)], capture_output=True, text=True, cwd=str(PROJECT_DIR), ) assert proc.returncode != 0, ( f"CLI must exit non-zero on a missing-units row; " f"got {proc.returncode}\nSTDOUT:{proc.stdout}\nSTDERR:{proc.stderr}" ) assert (out_dir / "failed_claims.csv").exists() def test_fail_closed_missing_claim_class(tmp_path): """Blank a claim_class on a theory row => Test 1 FAIL => non-zero exit.""" data = _copy_frozen_data(tmp_path / "data") theory = data / "theory_outputs.csv" rows = _read_csv(theory) for r in rows: if r["observable"] == "m_e(M_Z)": r["claim_class"] = "" _write_theory(theory, rows) code, out_dir, report = _run_engine(tmp_path, data_dir=data) assert code != 0, "a missing claim_class MUST fail closed" assert report["totals"]["fail"] > 0 def test_fail_closed_illegal_claim_class(tmp_path): """An out-of-vocabulary claim_class => Test 2 FAIL => non-zero exit.""" data = _copy_frozen_data(tmp_path / "data") theory = data / "theory_outputs.csv" rows = _read_csv(theory) for r in rows: if r["observable"] == "m_e(M_Z)": r["claim_class"] = "definitely_not_allowed" _write_theory(theory, rows) code, _, report = _run_engine(tmp_path, data_dir=data) assert code != 0, "an illegal claim_class MUST fail closed" assert any(f["test_id"] == 2 for f in report["failed_claims"]) def test_fail_closed_dangling_evidence_id(tmp_path): """Point a claim at a non-existent evidence_id => referential-integrity FAIL.""" data = _copy_frozen_data(tmp_path / "data") claims = data / "claims_registry.csv" rows = _read_csv(claims) rows[0]["evidence_id"] = "EVID-DOES-NOT-EXIST" fieldnames = list(rows[0].keys()) with claims.open("w", encoding="utf-8", newline="") as fh: w = csv.DictWriter(fh, fieldnames=fieldnames) w.writeheader() w.writerows(rows) code, out_dir, report = _run_engine(tmp_path, data_dir=data) assert code != 0, "a dangling evidence_id MUST fail closed" failed = _read_csv(out_dir / "failed_claims.csv") assert any("evidence_id" in r["reason"] for r in failed) def test_fail_closed_missing_input_file(tmp_path): """A missing required input CSV must fail closed (non-zero), not crash silently.""" data = _copy_frozen_data(tmp_path / "data") (data / "theory_outputs.csv").unlink() out_dir = tmp_path / "out" code = eng.main(["--data", str(data), "--out", str(out_dir), "--config", str(CONFIG_YAML)]) assert code != 0, "a missing required input file MUST fail closed" # =========================================================================== # # RUNNABLE-FIRST — stdlib config fallbacks (no PyYAML required). # # =========================================================================== # def test_config_json_mirror_matches_yaml(): """The config.json mirror parses to the same Config as the YAML.""" cfg_yaml = eng.load_config(CONFIG_YAML) cfg_json = eng.load_config(CONFIG_JSON) assert cfg_yaml.pdg_version == cfg_json.pdg_version assert tuple(cfg_yaml.allowed_claim_classes) == tuple(cfg_json.allowed_claim_classes) assert tuple(cfg_yaml.required_sectors) == tuple(cfg_json.required_sectors) def test_stdlib_flat_yaml_fallback_parses_config(): """The pure-stdlib flat-YAML parser handles validation_config.yaml correctly. This exercises the zero-pip-install path directly (independent of whether PyYAML happens to be importable in this environment). """ raw = eng._parse_flat_yaml(CONFIG_YAML.read_text(encoding="utf-8")) assert raw["pdg_version"] == "PDG 2024" assert "predicted" in raw["allowed_claim_classes"] assert "leptons" in raw["required_sectors"] assert raw["release_gate"]["require_units"] is True assert raw["fail_on_broad_resonance_exact"] is False def test_engine_runs_with_json_config_only(tmp_path): """The engine must run end-to-end using only the config.json mirror.""" data = _copy_frozen_data(tmp_path / "data") out_dir = tmp_path / "out" code = eng.run(data, out_dir, CONFIG_JSON) assert code == 0, "engine must PASS using the config.json mirror on frozen data" for name in EXPECTED_OUTPUTS: assert (out_dir / name).exists() def test_missing_config_fails_closed(tmp_path): """No config and no config.json mirror => fail closed.""" data = _copy_frozen_data(tmp_path / "data") # An isolated dir with neither config file present. bogus = tmp_path / "nowhere" / "validation_config.yaml" out_dir = tmp_path / "out" code = eng.main(["--data", str(data), "--out", str(out_dir), "--config", str(bogus)]) assert code != 0, "a missing config (and missing mirror) MUST fail closed" # =========================================================================== # # Local writer used by several fixtures. # # =========================================================================== # def _write_theory(path: Path, rows: List[Dict[str, str]]) -> None: fieldnames = list(rows[0].keys()) with path.open("w", encoding="utf-8", newline="") as fh: w = csv.DictWriter(fh, fieldnames=fieldnames) w.writeheader() w.writerows(rows) def _lc(row: Dict[str, str]) -> Dict[str, str]: """Lower-case the header keys the way the engine's load_csv does.""" return {(k or "").strip().lower(): (v.strip() if isinstance(v, str) else v) for k, v in row.items()}