feat(#80): IMP-52 user_overrides.json persistence (u1~u10 backend + frontend + tests)
4-axis MDX-stem keyed persistence so layout / zone_geometries / zone_sections / frames
survive across `/api/run` sessions. Auto-restore on MDX reopen; CLI > file precedence
on backend pipeline entry; 300ms-debounced PUT flushed before Generate.
u1 src/user_overrides_io.py — load/save/validate_key (MDX-stem regex), 4-axis schema,
miss={}, corrupt warning+{}, atomic tmp+rename, foreign-key preserve.
u2 src/phase_z2_pipeline.py — post-argparse fallback fills only missing axes.
u3 Front/vite.config.ts — GET /api/user-overrides/:key (200 {} on miss, 400 traversal).
u4 Front/vite.config.ts — PUT /api/user-overrides/:key, 4-axis allowlist, partial merge.
u5 Front/client/src/services/userOverridesApi.ts — typed get/save + flushUserOverrides
with 300ms debounce and mutated-axis partial payloads.
u6 Front/client/src/pages/Home.tsx + slidePlanUtils.ts — restore on MDX upload (non-frame
axes immediately, frames remapped post-loadRun unit_id → region.id).
u7 Home.tsx — persist on 4 mutation handlers (section drop, layout select, zone resize,
frame select); zone_sizes and Generate excluded.
u8 tests/test_user_overrides_io.py — round-trip, unknown-key passthrough, missing/corrupt,
invalid keys (26 tests).
u9 tests/test_user_overrides_pipeline_fallback.py — per-axis fill, CLI-wins, no-file noop,
corrupt warning+skip (16 tests).
u10 Home.tsx + user_overrides_write.test.ts — await flushUserOverrides() before runPipeline
in handleGenerate try-block head; source-pattern regression assertions (20 → 22 tests).
Backend pytest 42/42 green. Frontend vitest 113/113 green (endpoint 42 / restore 21 /
service 28 / write 22). HEAD baseline ee97f4f; no spillover to phase_z2 templates /
families / frames / pipeline orchestration outside the IMP-52 surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
229
tests/test_user_overrides_io.py
Normal file
229
tests/test_user_overrides_io.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""IMP-52 (#80) u8 — backend tests for ``src.user_overrides_io``.
|
||||
|
||||
Covers the four axes called out in the Stage 2 plan:
|
||||
|
||||
1. Round-trip ``save`` → ``load`` (4 KNOWN_AXES + foreign top-level keys).
|
||||
2. Unknown-key passthrough (foreign axes preserved across partial merges).
|
||||
3. Missing / corrupt / non-object behavior (graceful ``{}`` + stderr warning).
|
||||
4. Invalid keys (``InvalidOverrideKey`` raised on traversal / separators /
|
||||
leading dot / empty).
|
||||
|
||||
All tests inject ``root=tmp_path`` so they never touch the real
|
||||
``data/user_overrides/`` directory.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.user_overrides_io import (
|
||||
DEFAULT_OVERRIDES_ROOT,
|
||||
InvalidOverrideKey,
|
||||
KNOWN_AXES,
|
||||
load,
|
||||
override_path,
|
||||
save,
|
||||
validate_key,
|
||||
)
|
||||
|
||||
|
||||
# -- key validation ---------------------------------------------------------
|
||||
|
||||
|
||||
def test_validate_key_accepts_typical_mdx_stems():
|
||||
for key in ("01", "03", "03__DX_master", "sample.v2", "a-b_c.1"):
|
||||
assert validate_key(key) == key
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_key",
|
||||
[
|
||||
"",
|
||||
"..",
|
||||
"../escape",
|
||||
"sub/dir",
|
||||
"sub\\dir",
|
||||
".hidden",
|
||||
"-leading-dash",
|
||||
".",
|
||||
"name with space",
|
||||
"name?",
|
||||
],
|
||||
)
|
||||
def test_validate_key_rejects_unsafe(bad_key):
|
||||
with pytest.raises(InvalidOverrideKey):
|
||||
validate_key(bad_key)
|
||||
|
||||
|
||||
def test_validate_key_rejects_non_string():
|
||||
for bad in (None, 123, b"bytes", ["list"], {"d": 1}):
|
||||
with pytest.raises(InvalidOverrideKey):
|
||||
validate_key(bad) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# -- override_path ----------------------------------------------------------
|
||||
|
||||
|
||||
def test_override_path_uses_default_root_when_unspecified():
|
||||
p = override_path("sample")
|
||||
assert p.parent == DEFAULT_OVERRIDES_ROOT
|
||||
assert p.name == "sample.json"
|
||||
|
||||
|
||||
def test_override_path_honors_explicit_root(tmp_path):
|
||||
p = override_path("sample", root=tmp_path)
|
||||
assert p == tmp_path / "sample.json"
|
||||
|
||||
|
||||
# -- load: missing / corrupt / non-object -----------------------------------
|
||||
|
||||
|
||||
def test_load_missing_file_returns_empty_dict(tmp_path):
|
||||
assert load("nope", root=tmp_path) == {}
|
||||
|
||||
|
||||
def test_load_corrupt_json_warns_and_returns_empty(tmp_path, capsys):
|
||||
path = override_path("corrupt", root=tmp_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("{ this is not valid json", encoding="utf-8")
|
||||
result = load("corrupt", root=tmp_path)
|
||||
assert result == {}
|
||||
captured = capsys.readouterr()
|
||||
assert "failed to read" in captured.err
|
||||
assert str(path) in captured.err
|
||||
|
||||
|
||||
def test_load_non_object_json_warns_and_returns_empty(tmp_path, capsys):
|
||||
path = override_path("list_root", root=tmp_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("[1, 2, 3]", encoding="utf-8")
|
||||
result = load("list_root", root=tmp_path)
|
||||
assert result == {}
|
||||
captured = capsys.readouterr()
|
||||
assert "not a JSON object" in captured.err
|
||||
|
||||
|
||||
# -- save: round-trip + partial-merge + foreign-key preserve ----------------
|
||||
|
||||
|
||||
def _full_payload() -> dict:
|
||||
return {
|
||||
"layout": "sidebar-right",
|
||||
"zone_geometries": {
|
||||
"zone-top": {"x": 40.0, "y": 50.0, "w": 1200.0, "h": 120.0},
|
||||
},
|
||||
"zone_sections": {"zone-top": ["03-1", "03-2"]},
|
||||
"frames": {"03-1+03-2": "frame_two_way_compare"},
|
||||
}
|
||||
|
||||
|
||||
def test_save_then_load_round_trip(tmp_path):
|
||||
key = "03"
|
||||
payload = _full_payload()
|
||||
written = save(key, payload, root=tmp_path)
|
||||
assert written.exists()
|
||||
assert written == tmp_path / "03.json"
|
||||
|
||||
loaded = load(key, root=tmp_path)
|
||||
for axis in KNOWN_AXES:
|
||||
assert loaded[axis] == payload[axis], f"axis {axis!r} did not round-trip"
|
||||
|
||||
|
||||
def test_save_partial_payload_preserves_other_axes(tmp_path):
|
||||
key = "03"
|
||||
save(key, _full_payload(), root=tmp_path)
|
||||
|
||||
save(key, {"layout": "two-column"}, root=tmp_path)
|
||||
loaded = load(key, root=tmp_path)
|
||||
|
||||
assert loaded["layout"] == "two-column"
|
||||
assert loaded["zone_geometries"] == _full_payload()["zone_geometries"]
|
||||
assert loaded["zone_sections"] == _full_payload()["zone_sections"]
|
||||
assert loaded["frames"] == _full_payload()["frames"]
|
||||
|
||||
|
||||
def test_save_axis_replaces_not_deep_merges(tmp_path):
|
||||
key = "03"
|
||||
save(key, {"frames": {"03-1": "frame_a", "03-2": "frame_b"}}, root=tmp_path)
|
||||
save(key, {"frames": {"03-3": "frame_c"}}, root=tmp_path)
|
||||
loaded = load(key, root=tmp_path)
|
||||
assert loaded["frames"] == {"03-3": "frame_c"}
|
||||
|
||||
|
||||
def test_save_none_clears_axis(tmp_path):
|
||||
key = "03"
|
||||
save(key, _full_payload(), root=tmp_path)
|
||||
save(key, {"layout": None}, root=tmp_path)
|
||||
loaded = load(key, root=tmp_path)
|
||||
assert "layout" not in loaded
|
||||
assert loaded["zone_geometries"] == _full_payload()["zone_geometries"]
|
||||
assert loaded["frames"] == _full_payload()["frames"]
|
||||
|
||||
|
||||
def test_save_preserves_foreign_top_level_keys(tmp_path):
|
||||
"""Forward-compat: axes outside KNOWN_AXES (zone_sizes, image_overrides,
|
||||
schema_version, ...) must survive a partial merge on a known axis."""
|
||||
key = "03"
|
||||
path = override_path(key, root=tmp_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
pre_seed = {
|
||||
"layout": "single-column",
|
||||
"image_overrides": {"img-1": {"position": "right", "size": "small"}},
|
||||
"zone_sizes": {"zone-top": "tall"},
|
||||
"schema_version": "experimental-1",
|
||||
}
|
||||
path.write_text(json.dumps(pre_seed), encoding="utf-8")
|
||||
|
||||
save(key, {"layout": "sidebar-right"}, root=tmp_path)
|
||||
|
||||
loaded = load(key, root=tmp_path)
|
||||
assert loaded["layout"] == "sidebar-right"
|
||||
assert loaded["image_overrides"] == pre_seed["image_overrides"]
|
||||
assert loaded["zone_sizes"] == pre_seed["zone_sizes"]
|
||||
assert loaded["schema_version"] == pre_seed["schema_version"]
|
||||
|
||||
|
||||
def test_save_creates_parent_directory(tmp_path):
|
||||
nested = tmp_path / "deep" / "nest"
|
||||
assert not nested.exists()
|
||||
save("03", {"layout": "two-column"}, root=nested)
|
||||
assert (nested / "03.json").exists()
|
||||
|
||||
|
||||
def test_save_writes_pretty_sorted_json_for_diffability(tmp_path):
|
||||
key = "03"
|
||||
save(key, _full_payload(), root=tmp_path)
|
||||
raw = (tmp_path / "03.json").read_text(encoding="utf-8")
|
||||
# sort_keys=True → KNOWN_AXES come out alphabetically
|
||||
pos_frames = raw.index('"frames"')
|
||||
pos_layout = raw.index('"layout"')
|
||||
pos_zg = raw.index('"zone_geometries"')
|
||||
pos_zs = raw.index('"zone_sections"')
|
||||
assert pos_frames < pos_layout < pos_zg < pos_zs
|
||||
|
||||
|
||||
def test_save_leaves_no_tmp_file_on_success(tmp_path):
|
||||
save("03", _full_payload(), root=tmp_path)
|
||||
leftovers = [p for p in tmp_path.iterdir() if p.name != "03.json"]
|
||||
assert leftovers == [], f"tmp files leaked: {leftovers!r}"
|
||||
|
||||
|
||||
def test_save_rejects_non_dict_partial(tmp_path):
|
||||
with pytest.raises(TypeError):
|
||||
save("03", ["not", "a", "dict"], root=tmp_path) # type: ignore[arg-type]
|
||||
|
||||
|
||||
# -- save / load propagate key validation -----------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_key", ["", "..", "sub/dir", ".hidden"])
|
||||
def test_save_rejects_invalid_key(tmp_path, bad_key):
|
||||
with pytest.raises(InvalidOverrideKey):
|
||||
save(bad_key, {"layout": "two-column"}, root=tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_key", ["", "..", "sub/dir", ".hidden"])
|
||||
def test_load_rejects_invalid_key(tmp_path, bad_key):
|
||||
with pytest.raises(InvalidOverrideKey):
|
||||
load(bad_key, root=tmp_path)
|
||||
296
tests/test_user_overrides_pipeline_fallback.py
Normal file
296
tests/test_user_overrides_pipeline_fallback.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user