"""IMP-52 (#80) u9 — backend tests for the argparse persistence fallback. Stage 2 u9 scope (per the Exit Report): 1. Per-axis fill — file value flows through when CLI omits the axis. 2. CLI-wins — CLI value beats file value on the same axis. 3. No-file noop — missing file → ``run_phase_z2_mvp1`` gets all-None. 4. Corrupt-file warn — invalid JSON / non-object → stderr warning + skip. 5. Invalid stem warn — ``Path(args.mdx_path).stem`` rejected by validator → warning + fallback skipped wholesale. We exec the ``if __name__ == "__main__"`` block of ``src.phase_z2_pipeline`` directly inside the module's namespace, after (a) monkeypatching ``src.user_overrides_io.DEFAULT_OVERRIDES_ROOT`` to a tmp directory and (b) replacing ``run_phase_z2_mvp1`` with a recording stub. This exercises the production fallback verbatim without the cost of a real pipeline invocation. """ from __future__ import annotations import ast import json import sys from pathlib import Path from typing import Any import pytest import src.phase_z2_pipeline as _pz2 import src.user_overrides_io as _io # -- harness --------------------------------------------------------------- def _exec_main_block( captured: dict[str, Any], argv: list[str], monkeypatch ) -> None: """Run the ``__main__`` body of phase_z2_pipeline.py with a fake ``run_phase_z2_mvp1`` so its kwargs are observable.""" def _fake_run( mdx_path, run_id, *, override_layout=None, override_frames=None, override_zone_geometries=None, override_section_assignments=None, ): captured["mdx_path"] = mdx_path captured["run_id"] = run_id captured["override_layout"] = override_layout captured["override_frames"] = override_frames captured["override_zone_geometries"] = override_zone_geometries captured["override_section_assignments"] = override_section_assignments monkeypatch.setattr(_pz2, "run_phase_z2_mvp1", _fake_run) monkeypatch.setattr(sys, "argv", argv) src_path = Path(_pz2.__file__) source = src_path.read_text(encoding="utf-8") tree = ast.parse(source) for node in tree.body: if ( isinstance(node, ast.If) and isinstance(node.test, ast.Compare) and isinstance(node.test.left, ast.Name) and node.test.left.id == "__name__" ): block = ast.Module(body=node.body, type_ignores=[]) exec(compile(block, str(src_path), "exec"), _pz2.__dict__) return raise AssertionError("no `if __name__ == '__main__'` block found") def _redirect_overrides_root(tmp_path: Path, monkeypatch) -> None: """Redirect the on-disk persistence root so tests never touch ``data/user_overrides/``.""" monkeypatch.setattr(_io, "DEFAULT_OVERRIDES_ROOT", tmp_path) def _write_full_payload(tmp_path: Path, stem: str = "03") -> Path: path = tmp_path / f"{stem}.json" path.write_text( json.dumps( { "layout": "sidebar-right", "frames": {"03-1": "frame_file_a", "03-1+03-2": "frame_file_b"}, "zone_geometries": { "top": {"x": 0.0, "y": 0.0, "w": 1.0, "h": 0.3}, "bottom": {"x": 0.0, "y": 0.3, "w": 1.0, "h": 0.7}, }, "zone_sections": { "top": ["03-1"], "bottom": ["03-2", "03-3"], }, } ), encoding="utf-8", ) return path # -- 1. no-file noop ------------------------------------------------------- def test_no_overrides_file_passes_none_overrides(tmp_path, monkeypatch): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} _exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch) assert captured["override_layout"] is None assert captured["override_frames"] is None assert captured["override_zone_geometries"] is None assert captured["override_section_assignments"] is None # MDX path / run_id propagate untouched. assert captured["mdx_path"] == Path("03.mdx") assert captured["run_id"] is None # -- 2. file fills every axis when CLI is empty ---------------------------- def test_file_only_fills_all_four_axes_when_cli_empty(tmp_path, monkeypatch): _redirect_overrides_root(tmp_path, monkeypatch) _write_full_payload(tmp_path, "03") captured: dict[str, Any] = {} _exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch) assert captured["override_layout"] == "sidebar-right" assert captured["override_frames"] == { "03-1": "frame_file_a", "03-1+03-2": "frame_file_b", } assert captured["override_zone_geometries"] == { "top": {"x": 0.0, "y": 0.0, "w": 1.0, "h": 0.3}, "bottom": {"x": 0.0, "y": 0.3, "w": 1.0, "h": 0.7}, } assert captured["override_section_assignments"] == { "top": ["03-1"], "bottom": ["03-2", "03-3"], } # -- 3. CLI beats file on the same axis ----------------------------------- def test_cli_layout_overrides_file_layout(tmp_path, monkeypatch): _redirect_overrides_root(tmp_path, monkeypatch) _write_full_payload(tmp_path, "03") captured: dict[str, Any] = {} _exec_main_block( captured, ["src.phase_z2_pipeline", "03.mdx", "--override-layout", "two-column"], monkeypatch, ) # layout from CLI; remaining axes still filled from file. assert captured["override_layout"] == "two-column" assert captured["override_frames"] == { "03-1": "frame_file_a", "03-1+03-2": "frame_file_b", } assert captured["override_zone_geometries"] is not None assert captured["override_section_assignments"] is not None def test_cli_frames_overrides_file_frames(tmp_path, monkeypatch): _redirect_overrides_root(tmp_path, monkeypatch) _write_full_payload(tmp_path, "03") captured: dict[str, Any] = {} _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-frame", "03-1=cli_frame_x", ], monkeypatch, ) # CLI ``frames`` payload wholly replaces file ``frames`` (per-axis win). assert captured["override_frames"] == {"03-1": "cli_frame_x"} # Other axes still come from the file. assert captured["override_layout"] == "sidebar-right" assert captured["override_zone_geometries"] is not None assert captured["override_section_assignments"] is not None # -- 4. corrupt / non-object file warns and skips fallback ---------------- def test_corrupt_json_warns_and_skips_fallback(tmp_path, monkeypatch, capsys): _redirect_overrides_root(tmp_path, monkeypatch) (tmp_path / "03.json").write_text("{ not valid json", encoding="utf-8") captured: dict[str, Any] = {} _exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch) err = capsys.readouterr().err assert "failed to read" in err # ``or None`` collapses empty dicts back to None on the call site. assert captured["override_layout"] is None assert captured["override_frames"] is None assert captured["override_zone_geometries"] is None assert captured["override_section_assignments"] is None def test_non_object_top_level_warns_and_skips_fallback( tmp_path, monkeypatch, capsys ): _redirect_overrides_root(tmp_path, monkeypatch) (tmp_path / "03.json").write_text("[1, 2, 3]", encoding="utf-8") captured: dict[str, Any] = {} _exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch) err = capsys.readouterr().err assert "not a JSON object" in err assert captured["override_layout"] is None assert captured["override_frames"] is None assert captured["override_zone_geometries"] is None assert captured["override_section_assignments"] is None # -- 5. invalid MDX stem warns and skips fallback wholesale --------------- def test_invalid_mdx_stem_warns_and_skips_fallback( tmp_path, monkeypatch, capsys ): _redirect_overrides_root(tmp_path, monkeypatch) # Seed a file the loader would otherwise consume; the invalid stem must # short-circuit before any read happens. _write_full_payload(tmp_path, "03") captured: dict[str, Any] = {} # ``Path(".hidden.mdx").stem`` == ".hidden" → leading dot → InvalidOverrideKey. _exec_main_block( captured, ["src.phase_z2_pipeline", ".hidden.mdx"], monkeypatch ) err = capsys.readouterr().err assert "cannot derive persistence key" in err assert captured["override_layout"] is None assert captured["override_frames"] is None assert captured["override_zone_geometries"] is None assert captured["override_section_assignments"] is None # -- 6. per-axis partial fill (file fills only what CLI omits) ------------ def test_per_axis_partial_fill_mixes_cli_and_file(tmp_path, monkeypatch): """File carries frames + zone_geometries; CLI supplies layout only. Expected: ``override_layout`` = CLI value, ``override_frames`` and ``override_zone_geometries`` = file values, ``override_section_assignments`` = None (neither side provided it). """ _redirect_overrides_root(tmp_path, monkeypatch) (tmp_path / "03.json").write_text( json.dumps( { "frames": {"03-1": "frame_only_file"}, "zone_geometries": { "top": {"x": 0.0, "y": 0.0, "w": 1.0, "h": 0.5}, }, } ), encoding="utf-8", ) captured: dict[str, Any] = {} _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-layout", "sidebar-right", ], monkeypatch, ) assert captured["override_layout"] == "sidebar-right" assert captured["override_frames"] == {"03-1": "frame_only_file"} assert captured["override_zone_geometries"] == { "top": {"x": 0.0, "y": 0.0, "w": 1.0, "h": 0.5}, } assert captured["override_section_assignments"] is None