feat(#93): IMP-55 u1~u12 frontend manual section swap detection (manual_section_assignment bool axis + drag-only marker gate + dual-axis persistence + backend manual-true gate)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 9s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 08:27:09 +09:00
parent 9062931863
commit 4e281a20d8
13 changed files with 834 additions and 52 deletions

View File

@@ -48,6 +48,14 @@ def _exec_main_block(
override_zone_geometries=None,
override_section_assignments=None,
override_image_overrides=None,
# IMP-55 (#93) u9 — mirror the live ``run_phase_z2_mvp1`` signature so
# the __main__ dispatch in src/phase_z2_pipeline.py:8332 does not raise
# TypeError on kwargs added by IMP-45 #74 (``override_slide_css``) and
# IMP-43 #72 (``reuse_from``). The u9 truth-table assertions only read
# the section-assignment axis; the new kwargs are captured here so any
# follow-up test can pin them without re-touching this harness.
override_slide_css=None,
reuse_from=None,
):
captured["mdx_path"] = mdx_path
captured["run_id"] = run_id
@@ -56,6 +64,8 @@ def _exec_main_block(
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)
@@ -97,6 +107,13 @@ def _write_full_payload(tmp_path: Path, stem: str = "03") -> Path:
"top": ["03-1"],
"bottom": ["03-2", "03-3"],
},
# IMP-55 (#93) u9 — seed the new ``manual_section_assignment``
# marker True so the per-axis / CLI-wins / partial-merge tests
# below continue to exercise the file→fallback path under the
# gate added in src/phase_z2_pipeline.py (``is True`` identity
# check). Truth-table coverage for False / absent / non-bool
# belongs to u10 and lives in its own test section.
"manual_section_assignment": True,
"image_overrides": {
"img-file-a": {"x": 10.0, "y": 15.0, "w": 30.0, "h": 25.0},
"img-file-b": {"x": 50.0, "y": 50.0, "w": 40.0, "h": 40.0},
@@ -408,3 +425,178 @@ def test_image_overrides_fallback_coerces_int_values_to_float(tmp_path, monkeypa
assert coerced == {"img-int": {"x": 10.0, "y": 20.0, "w": 30.0, "h": 40.0}}
for axis_value in coerced["img-int"].values():
assert isinstance(axis_value, float)
# -- 8. IMP-55 (#93) u10 — manual_section_assignment marker truth-table ----
#
# The backend file-fallback gate added in u9
# (``src/phase_z2_pipeline.py`` ``_persisted.get("manual_section_assignment")
# is True``) is exercised here with the full truth-table of marker values a
# stale or hand-edited overrides file could legally contain. The gate is
# fail-closed by ``is True`` identity, so only literal Python ``True``
# (JSON ``true``) propagates ``zone_sections`` into
# ``override_section_assignments``; everything else — absent, ``False``,
# truthy non-bool (``"true"``, ``1``, ``[]``, ``{}``), and ``None`` — must
# leave the axis as ``None`` (``or None`` collapses an empty dict on the
# call site). No hardcoding of section IDs in the assertion logic; the
# values ``03-1`` / ``03-2`` here are sample payload literals, not pinned
# behavior. ``_write_marker_payload`` reuses the IO loader by way of the
# ``__main__`` block (no direct import of the pipeline gate).
_MARKER_ABSENT = object()
def _write_marker_payload(
tmp_path: Path, marker: Any, stem: str = "03"
) -> Path:
"""Write a minimal overrides file with ``zone_sections`` + optional marker.
``marker is _MARKER_ABSENT`` → omit ``manual_section_assignment`` key.
Any other value (including ``None``) → write it verbatim as JSON.
"""
payload: dict[str, Any] = {
"zone_sections": {"top": ["03-1"], "bottom": ["03-2"]},
}
if marker is not _MARKER_ABSENT:
payload["manual_section_assignment"] = marker
path = tmp_path / f"{stem}.json"
path.write_text(json.dumps(payload), encoding="utf-8")
return path
def test_marker_true_fills_section_assignments(tmp_path, monkeypatch):
"""marker=True + zone_sections in file + CLI empty → file value flows."""
_redirect_overrides_root(tmp_path, monkeypatch)
_write_marker_payload(tmp_path, True, "03")
captured: dict[str, Any] = {}
_exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch)
assert captured["override_section_assignments"] == {
"top": ["03-1"],
"bottom": ["03-2"],
}
@pytest.mark.parametrize(
"marker",
[
_MARKER_ABSENT,
False,
"true", # JSON-string truthy must NOT pass the ``is True`` gate.
1, # int truthy must NOT pass.
[],
{},
None,
],
ids=["absent", "false", "string_true", "int_one", "empty_list", "empty_dict", "null"],
)
def test_marker_non_true_skips_section_assignments(
tmp_path, monkeypatch, marker
):
"""marker absent / False / non-bool → gate fail-closed → axis is None.
Even though ``zone_sections`` is present in the file, the gate refuses
to forward it because ``manual_section_assignment`` is not literal
``True``. ``or None`` on the call site collapses the empty dict back
to ``None``.
"""
_redirect_overrides_root(tmp_path, monkeypatch)
_write_marker_payload(tmp_path, marker, "03")
captured: dict[str, Any] = {}
_exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch)
assert captured["override_section_assignments"] is None
# -- 9. IMP-55 (#93) u11 — CLI ``--override-section-assignment`` wins over
# persisted manual-marker fallback ----------------------------------------
#
# The u9 gate only fires on the file→fallback branch
# (``not overrides_section_assignments and _persisted.get(...) is True``).
# When the CLI supplies ``--override-section-assignment`` the
# ``overrides_section_assignments`` dict is truthy before the gate is
# evaluated, so the persisted ``zone_sections`` axis (and the
# ``manual_section_assignment`` marker that would otherwise unlock it) is
# bypassed entirely — CLI is authoritative on the section-assignment
# axis. The three cases below pin this contract:
#
# 1. CLI + persisted marker True (file value present) → CLI wins.
# 2. CLI + persisted marker False (file value present) → CLI wins; the
# marker is irrelevant on the CLI-wins path (no truth-value coupling).
# 3. CLI with no overrides file at all → CLI value flows through; the
# marker is a gate on file→fallback only, never a precondition for
# any CLI ``cli_override`` to take effect.
#
# Cross-axis bystanders (layout / frames / geometries / images) are
# intentionally not seeded here; this section locks CLI-vs-marker
# semantics on the section axis only. Cross-axis CLI-wins behavior is
# already covered by the IMP-52 #80 tests in section 3 / 3b above.
def test_cli_section_assignment_wins_over_persisted_marker_true(
tmp_path, monkeypatch
):
"""marker=True + file zone_sections + CLI value → CLI wins per-axis."""
_redirect_overrides_root(tmp_path, monkeypatch)
_write_marker_payload(tmp_path, True, "03")
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-section-assignment",
"top=cli-section",
],
monkeypatch,
)
# CLI value wholly replaces the file zone_sections (per-axis win); the
# gate's ``not overrides_section_assignments`` precondition is false.
assert captured["override_section_assignments"] == {"top": ["cli-section"]}
def test_cli_section_assignment_wins_with_persisted_marker_false(
tmp_path, monkeypatch
):
"""marker=False + file zone_sections + CLI value → CLI wins; marker unread."""
_redirect_overrides_root(tmp_path, monkeypatch)
_write_marker_payload(tmp_path, False, "03")
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-section-assignment",
"bottom=cli-section",
],
monkeypatch,
)
assert captured["override_section_assignments"] == {"bottom": ["cli-section"]}
def test_cli_section_assignment_works_without_persisted_file(
tmp_path, monkeypatch
):
"""No overrides file → CLI value flows; marker is not a CLI precondition."""
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-section-assignment",
"top=cli-only",
],
monkeypatch,
)
assert captured["override_section_assignments"] == {"top": ["cli-only"]}