- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규 - Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가 - templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신 - tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분) - tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가 - docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서 - scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸 - .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외 미완성 작업의 보존용 스냅샷 커밋 (2026-07-02) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
390 lines
13 KiB
Python
390 lines
13 KiB
Python
"""IMP-43 (#72) u1 + u5 — focused tests for the ``--reuse-from`` CLI surface.
|
|
|
|
u1 scope (per the Stage 2 Exit Report):
|
|
|
|
- argparse flag ``--reuse-from PREV_RUN_ID`` parses without error.
|
|
- Fail-closed precondition guard runs AFTER the ``user_overrides.json``
|
|
merge and BEFORE dispatch. With ``--reuse-from`` set, the guard
|
|
must:
|
|
* accept frame-only overrides (or no overrides at all);
|
|
* reject layout / zone-geometry / zone-section / image overrides
|
|
with ``sys.exit(2)`` whose stderr names every rejected axis.
|
|
|
|
u5 scope (added 2026-05-24):
|
|
|
|
- ``reuse_from`` is keyword-only on ``run_phase_z2_mvp1`` and defaults
|
|
to ``None`` so the absent-flag path preserves pre-u5 behaviour.
|
|
- The CLI dispatch forwards ``args.reuse_from`` verbatim — both
|
|
``None`` (flag absent) and ``"PREV_RUN_ID"`` (flag present) reach
|
|
the kwarg unchanged.
|
|
- The fake ``run_phase_z2_mvp1`` stub below mirrors the production
|
|
signature so the forwarding lock would fail loudly on any
|
|
forwarding regression.
|
|
|
|
The harness mirrors ``tests/test_phase_z2_cli_overrides.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. 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. Captures the
|
|
presence of the call (``called=True``) so guard-driven early exits
|
|
can be distinguished from a successful parse + dispatch."""
|
|
|
|
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,
|
|
override_text_overrides=None,
|
|
override_structure_overrides=None,
|
|
reuse_from=None,
|
|
):
|
|
captured["called"] = True
|
|
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["override_text_overrides"] = override_text_overrides
|
|
captured["override_structure_overrides"] = override_structure_overrides
|
|
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_reuse_from_alone_parses_and_dispatches(tmp_path, monkeypatch):
|
|
"""``--reuse-from`` with no other overrides must parse cleanly and
|
|
fall through to dispatch (frame-only / empty override is allowed).
|
|
u5 (2026-05-24): also asserts the CLI threads ``args.reuse_from``
|
|
verbatim into the ``reuse_from`` kwarg."""
|
|
_redirect_overrides_root(tmp_path, monkeypatch)
|
|
captured: dict[str, Any] = {}
|
|
_exec_main_block(
|
|
captured,
|
|
[
|
|
"src.phase_z2_pipeline",
|
|
"03.mdx",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert captured.get("called") is True
|
|
# u5 — verbatim threading.
|
|
assert captured["reuse_from"] == "03__DX_20260508025134"
|
|
|
|
|
|
def test_reuse_from_with_frame_override_dispatches(tmp_path, monkeypatch):
|
|
"""Frame overrides ARE preserved across Step 0/1/2/5/6 reuse, so
|
|
``--reuse-from`` + ``--override-frame`` must reach dispatch.
|
|
u5: forwards both ``reuse_from`` and ``override_frames`` in the
|
|
same call."""
|
|
_redirect_overrides_root(tmp_path, monkeypatch)
|
|
captured: dict[str, Any] = {}
|
|
_exec_main_block(
|
|
captured,
|
|
[
|
|
"src.phase_z2_pipeline",
|
|
"03.mdx",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
"--override-frame",
|
|
"03-1=frame_foo",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert captured.get("called") is True
|
|
assert captured["override_frames"] == {"03-1": "frame_foo"}
|
|
# u5 — frame override + reuse_from reach the kwarg simultaneously.
|
|
assert captured["reuse_from"] == "03__DX_20260508025134"
|
|
|
|
|
|
# -- u5 — flag-absent default + signature surface ------------------------
|
|
|
|
|
|
def test_no_reuse_from_threads_none_kwarg(tmp_path, monkeypatch):
|
|
"""u5 — when ``--reuse-from`` is absent, the kwarg must reach
|
|
``run_phase_z2_mvp1`` as ``None`` (not omitted, not ``""``). This
|
|
locks the "default None preserves current behavior" requirement
|
|
from the Stage 2 plan §u5."""
|
|
_redirect_overrides_root(tmp_path, monkeypatch)
|
|
captured: dict[str, Any] = {}
|
|
_exec_main_block(
|
|
captured,
|
|
["src.phase_z2_pipeline", "03.mdx"],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert captured.get("called") is True
|
|
assert captured["reuse_from"] is None
|
|
|
|
|
|
def test_run_phase_z2_mvp1_signature_includes_reuse_from():
|
|
"""Production signature lock — ``reuse_from`` must be a keyword-only
|
|
parameter with default ``None``. Mirror of the entry-tests
|
|
invariant; kept here so the CLI-surface test file fails loudly if
|
|
the production signature drifts away from the dispatch contract."""
|
|
import inspect
|
|
|
|
sig = inspect.signature(_pz2.run_phase_z2_mvp1)
|
|
assert "reuse_from" in sig.parameters, list(sig.parameters)
|
|
param = sig.parameters["reuse_from"]
|
|
assert param.kind is inspect.Parameter.KEYWORD_ONLY, param.kind
|
|
assert param.default is None, param.default
|
|
|
|
|
|
# -- fail-closed (single-axis rejection) ----------------------------------
|
|
|
|
|
|
def test_reuse_from_with_layout_override_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",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
"--override-layout",
|
|
"horizontal-2",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert excinfo.value.code == 2
|
|
err = capsys.readouterr().err
|
|
assert "--reuse-from incompatible with override axes" in err
|
|
assert "layout" in err
|
|
assert captured.get("called") is not True
|
|
|
|
|
|
def test_reuse_from_with_zone_geometry_override_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",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
"--override-zone-geometry",
|
|
"top=0,0,1,0.3",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert excinfo.value.code == 2
|
|
err = capsys.readouterr().err
|
|
assert "--reuse-from incompatible with override axes" in err
|
|
assert "zone_geometry" in err
|
|
assert captured.get("called") is not True
|
|
|
|
|
|
def test_reuse_from_with_zone_section_override_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",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
"--override-section-assignment",
|
|
"top=03-1",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert excinfo.value.code == 2
|
|
err = capsys.readouterr().err
|
|
assert "--reuse-from incompatible with override axes" in err
|
|
assert "zone_section" in err
|
|
assert captured.get("called") is not True
|
|
|
|
|
|
def test_reuse_from_with_image_override_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",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
"--override-image",
|
|
"img-abc=10,15,30,25",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert excinfo.value.code == 2
|
|
err = capsys.readouterr().err
|
|
assert "--reuse-from incompatible with override axes" in err
|
|
assert "image" in err
|
|
assert captured.get("called") is not True
|
|
|
|
|
|
# -- fail-closed (multi-axis aggregation) ---------------------------------
|
|
|
|
|
|
def test_reuse_from_with_multiple_rejected_axes_lists_all(
|
|
tmp_path, monkeypatch, capsys
|
|
):
|
|
"""Stderr must enumerate every rejected axis (not stop at first)."""
|
|
_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",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
"--override-layout",
|
|
"horizontal-2",
|
|
"--override-zone-geometry",
|
|
"top=0,0,1,0.3",
|
|
"--override-image",
|
|
"img-abc=10,15,30,25",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert excinfo.value.code == 2
|
|
err = capsys.readouterr().err
|
|
assert "layout" in err
|
|
assert "zone_geometry" in err
|
|
assert "image" in err
|
|
assert captured.get("called") is not True
|
|
|
|
|
|
# -- guard inactive when --reuse-from absent ------------------------------
|
|
|
|
|
|
def test_no_reuse_from_layout_override_still_dispatches(
|
|
tmp_path, monkeypatch
|
|
):
|
|
"""Without ``--reuse-from``, the guard must be silent — existing
|
|
override behaviour is preserved end-to-end."""
|
|
_redirect_overrides_root(tmp_path, monkeypatch)
|
|
captured: dict[str, Any] = {}
|
|
_exec_main_block(
|
|
captured,
|
|
[
|
|
"src.phase_z2_pipeline",
|
|
"03.mdx",
|
|
"--override-layout",
|
|
"horizontal-2",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert captured.get("called") is True
|
|
assert captured["override_layout"] == "horizontal-2"
|
|
|
|
|
|
# -- fail-closed honours persisted overrides ------------------------------
|
|
|
|
|
|
def test_reuse_from_with_persisted_layout_override_exits(
|
|
tmp_path, monkeypatch, capsys
|
|
):
|
|
"""The guard runs AFTER the user_overrides.json merge, so a layout
|
|
persisted on disk (not on the CLI) must still reject when
|
|
``--reuse-from`` is set. This locks the Stage 2 placement rule."""
|
|
_redirect_overrides_root(tmp_path, monkeypatch)
|
|
# Persist a layout override keyed by the MDX stem ``03``.
|
|
overrides_dir = tmp_path
|
|
overrides_dir.mkdir(parents=True, exist_ok=True)
|
|
(overrides_dir / "03.json").write_text(
|
|
'{"layout": "vertical-2"}', encoding="utf-8"
|
|
)
|
|
captured: dict[str, Any] = {}
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
_exec_main_block(
|
|
captured,
|
|
[
|
|
"src.phase_z2_pipeline",
|
|
"03.mdx",
|
|
"--reuse-from",
|
|
"03__DX_20260508025134",
|
|
],
|
|
monkeypatch,
|
|
)
|
|
|
|
assert excinfo.value.code == 2
|
|
err = capsys.readouterr().err
|
|
assert "--reuse-from incompatible with override axes" in err
|
|
assert "layout" in err
|
|
assert captured.get("called") is not True
|