"""IMP-51 (#79) u5 — focused tests for the ``--override-image`` CLI surface. Stage 2 u5 scope (per the Exit Report): - Successful parse: single flag + multiple flags accumulate. - Forwarding: parsed mapping reaches ``run_phase_z2_mvp1`` as ``override_image_overrides={image_id: {"x", "y", "w", "h"}}``. - Empty payload: omitting ``--override-image`` forwards ``None`` (CLI ``or None`` collapse, sibling pattern to other axes). - Hard-error cases (each must ``sys.exit(2)`` with a stderr message): * missing ``=`` * empty ``IMAGE_ID`` * duplicate ``IMAGE_ID`` * wrong float count (not 4) * non-numeric float component The harness mirrors ``tests/test_user_overrides_pipeline_fallback.py`` — the ``if __name__ == "__main__"`` block of ``src.phase_z2_pipeline`` is exec'd inside the module's namespace after monkeypatching ``run_phase_z2_mvp1`` with a recording stub. This exercises the actual production parser without invoking the real pipeline. The persistence fallback is silenced by redirecting ``src.user_overrides_io.DEFAULT_OVERRIDES_ROOT`` to a clean tmp directory so persisted state from prior runs cannot bleed into the parser-only assertions here. """ from __future__ import annotations import ast 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, override_image_overrides=None, override_slide_css=None, reuse_from=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 captured["override_image_overrides"] = override_image_overrides captured["override_slide_css"] = override_slide_css captured["reuse_from"] = reuse_from 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: """Isolate the persistence fallback so file state never leaks in.""" monkeypatch.setattr(_io, "DEFAULT_OVERRIDES_ROOT", tmp_path) # -- success paths -------------------------------------------------------- def test_no_image_override_forwards_none(tmp_path, monkeypatch): """When ``--override-image`` is omitted, the kwarg must be ``None`` (the parser's accumulator stays empty → ``overrides_images or None``).""" _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} _exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch) assert captured["override_image_overrides"] is None def test_single_image_override_parses_and_forwards(tmp_path, monkeypatch): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc=10,15,30.5,25", ], monkeypatch, ) assert captured["override_image_overrides"] == { "img-abc": {"x": 10.0, "y": 15.0, "w": 30.5, "h": 25.0}, } def test_multiple_image_overrides_accumulate(tmp_path, monkeypatch): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc=10,15,30,25", "--override-image", "img-def=50,15,40,40", ], monkeypatch, ) assert captured["override_image_overrides"] == { "img-abc": {"x": 10.0, "y": 15.0, "w": 30.0, "h": 25.0}, "img-def": {"x": 50.0, "y": 15.0, "w": 40.0, "h": 40.0}, } def test_image_override_strips_whitespace_in_image_id(tmp_path, monkeypatch): """``iid.strip()`` is intentional — match sibling --override-frame and --override-zone-geometry leniency on surrounding whitespace.""" _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", " img-pad =5,5,10,10", ], monkeypatch, ) assert captured["override_image_overrides"] == { "img-pad": {"x": 5.0, "y": 5.0, "w": 10.0, "h": 10.0}, } # -- hard-error paths ----------------------------------------------------- def test_image_override_missing_equals_exits(tmp_path, monkeypatch, capsys): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} with pytest.raises(SystemExit) as excinfo: _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc10,15,30,25", ], monkeypatch, ) assert excinfo.value.code == 2 err = capsys.readouterr().err assert "--override-image must be IMAGE_ID=X,Y,W,H" in err def test_image_override_empty_image_id_exits(tmp_path, monkeypatch, capsys): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} with pytest.raises(SystemExit) as excinfo: _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "=10,15,30,25", ], monkeypatch, ) assert excinfo.value.code == 2 err = capsys.readouterr().err assert "IMAGE_ID must be non-empty" in err def test_image_override_whitespace_only_image_id_exits( tmp_path, monkeypatch, capsys ): """``iid.strip()`` must collapse whitespace-only IDs into the empty-ID error path (otherwise a spurious key would land in the mapping).""" _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} with pytest.raises(SystemExit) as excinfo: _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", " =10,15,30,25", ], monkeypatch, ) assert excinfo.value.code == 2 err = capsys.readouterr().err assert "IMAGE_ID must be non-empty" in err def test_image_override_duplicate_image_id_exits( tmp_path, monkeypatch, capsys ): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} with pytest.raises(SystemExit) as excinfo: _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc=10,15,30,25", "--override-image", "img-abc=20,25,30,35", ], monkeypatch, ) assert excinfo.value.code == 2 err = capsys.readouterr().err assert "duplicate IMAGE_ID 'img-abc'" in err def test_image_override_wrong_float_count_exits( tmp_path, monkeypatch, capsys ): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} with pytest.raises(SystemExit) as excinfo: _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc=10,15,30", ], monkeypatch, ) assert excinfo.value.code == 2 err = capsys.readouterr().err assert "expects 4 floats X,Y,W,H" in err def test_image_override_too_many_floats_exits( tmp_path, monkeypatch, capsys ): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} with pytest.raises(SystemExit) as excinfo: _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc=10,15,30,25,99", ], monkeypatch, ) assert excinfo.value.code == 2 err = capsys.readouterr().err assert "expects 4 floats X,Y,W,H" in err def test_image_override_non_numeric_value_exits( tmp_path, monkeypatch, capsys ): _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} with pytest.raises(SystemExit) as excinfo: _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc=10,abc,30,25", ], monkeypatch, ) assert excinfo.value.code == 2 err = capsys.readouterr().err assert "floats parse fail" in err # -- isolation guard ------------------------------------------------------ def test_image_override_does_not_leak_into_sibling_axes(tmp_path, monkeypatch): """A populated image override must not perturb the other four axes.""" _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} _exec_main_block( captured, [ "src.phase_z2_pipeline", "03.mdx", "--override-image", "img-abc=10,15,30,25", ], monkeypatch, ) assert captured["override_image_overrides"] == { "img-abc": {"x": 10.0, "y": 15.0, "w": 30.0, "h": 25.0}, } 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 # -- IMP-45 (#74) u5 — slide-level CSS override CLI surface ---------------- # # Six focused cases mirror the --override-image pattern above: # 1. neither flag → kwarg None (fall-back to MDX frontmatter at u4) # 2. --override-slide-css inline TEXT → kwarg passes verbatim # 3. --slide-css-file PATH UTF-8 read → kwarg = file contents # 4. both flags set → sys.exit(2) with mutual-exclusion stderr # 5. --slide-css-file missing path → sys.exit(2) with not-found stderr # 6. --slide-css-file non-UTF-8 bytes → sys.exit(2) with utf-8 stderr def test_no_slide_css_override_forwards_none(tmp_path, monkeypatch): """Neither --override-slide-css nor --slide-css-file → kwarg = None.""" _redirect_overrides_root(tmp_path, monkeypatch) captured: dict[str, Any] = {} _exec_main_block( captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch, ) assert captured["override_slide_css"] is None def test_inline_slide_css_override_forwards_verbatim(tmp_path, monkeypatch): """--override-slide-css TEXT → kwarg = TEXT (verbatim, no `