feat(#74): IMP-45 u1~u8 slide-level CSS override (frontmatter slide_overrides.css + --override-slide-css/--slide-css-file + idempotent Step 13 injector)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 22s

u1 KNOWN_AXES tuple gains slide_css entry in src/user_overrides_io.py
(snake_case parity with image_overrides); round-trip test extends
to 6 axes.
u2 src/mdx_normalizer.py surfaces nested slide_overrides.css from the
MDX frontmatter into the normalize_mdx_content return dict; absent
key -> {}, non-string css drops. 4 unit cases in tests/test_mdx_normalizer.py
(present / absent / non-string / title-only).
u3 src/slide_css_injector.py NEW (88 lines) mirrors the
inject_image_overrides_style contract from src/image_id_stamper.py:
marker pair <!--IMP45-SLIDE-CSS:OPEN--> / <!--IMP45-SLIDE-CSS:CLOSE-->,
idempotent re-injection, </head> > <body> > document-start three-tier
fallback, empty/None -> unchanged. 8 fixtures in
tests/test_slide_css_injector.py mirror test_image_id_stamper.py.
u4 run_phase_z2_mvp1 accepts override_slide_css: Optional[str] = None;
None -> frontmatter slide_overrides.css fallback. Step 13 calls
inject_slide_css after image override injection and before the
final.html disk write, so CLI/CI/regression renders observe the same
backend artifact.
u5 argparse adds mutually-exclusive --override-slide-css TEXT (inline
CSS, <style> wrapper optional) and --slide-css-file PATH (UTF-8 read,
fail-closed sys.exit(2) on missing path / decode error / both flags
present). Resolved string is forwarded as override_slide_css kwarg.
6 cases in tests/test_phase_z2_cli_overrides.py (inline / file / both
/ missing / non-utf8 / neither).
u6 samples/mdx_batch/04.mdx frontmatter gains slide_overrides.css
block (verbatim of the former MDX04_DEFAULT_OVERRIDE_CSS constant,
no sample/frame gate). Subprocess smoke in
tests/test_phase_z2_slide_css_smoke.py verifies the marker pair and
CSS substring land in final.html.
u7 Front/client removes the sample/frame-gated frontend-only injection:
Home.tsx drops the MDX04_DEFAULT_OVERRIDE_CSS constant and the
sample==="04"+frame==="process_product_two_way" branch (-28 lines);
SlideCanvas.tsx drops the iframe contentDocument.head injection of
that prop (-14 lines). Live preview now reads backend final.html only.
u8 tests/regression/fixtures/89a_pre_baseline_sha.json 04.mdx entry
resyncs to the live SHA ddb6bf2f... / 28042 bytes (overwrites the
earlier 5-byte-drift d02c76fd... / 28047). Other entries untouched.
Note: 01.mdx baseline drift (ad6f16a3... / 29089 -> live f26a7fac...
/ 29084) predates this branch and is split to a follow-up issue per
the closed-issue fresh validation rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 03:26:03 +09:00
parent b4be6c1cd0
commit 9062931863
14 changed files with 740 additions and 55 deletions

View File

@@ -40,8 +40,8 @@
"04.mdx": {
"mdx_file": "04.mdx",
"run_id": "89a_baseline_04",
"final_html_size_bytes": 27707,
"sha256": "2bce45041cdcca6518cd92586c1be9e051a5c98f5a0ad61fdde02604618a1d80",
"final_html_size_bytes": 28042,
"sha256": "ddb6bf2f8d76ca1f56588a50dd4af5aeb5f45e0a83d5241b83b5932d0c66d41c",
"pipeline_exit_code": null
},
"05.mdx": {

View File

@@ -0,0 +1,107 @@
"""IMP-45 (#74) u2 — frontmatter ``slide_overrides`` surfacing.
Covers ``src.mdx_normalizer.normalize_mdx_content`` and the helper
``_extract_slide_overrides``. The Stage 2 plan enumerates four cases:
1. Present — nested ``slide_overrides.css`` survives normalization verbatim.
2. Absent — return dict carries an empty ``slide_overrides`` mapping.
3. Non-string ``css`` — dropped (fail-closed against typo'd YAML shapes).
4. Title-only frontmatter — no ``slide_overrides`` key in metadata → ``{}``.
Scope-lock: this unit only adds the new key to the return dict. The four
pre-existing return keys (``clean_text``/``title``/``images``/``popups``/
``tables``/``sections``) are asserted unchanged at the structural level.
"""
from __future__ import annotations
from src.mdx_normalizer import _extract_slide_overrides, normalize_mdx_content
_CSS_BLOCK = "<style>.f29b__col_right { width: 320px; }</style>"
def _mdx_with_frontmatter(fm_body: str, body: str = "# 제목\n\n본문 한 줄.\n") -> str:
return f"---\n{fm_body}---\n{body}"
# -- case 1: present (nested css string survives verbatim) ------------------
def test_normalize_surfaces_nested_slide_overrides_css():
raw = _mdx_with_frontmatter(
"title: 04 sample\n"
"slide_overrides:\n"
f" css: \"{_CSS_BLOCK}\"\n"
)
result = normalize_mdx_content(raw)
assert result["slide_overrides"] == {"css": _CSS_BLOCK}
# Other axes unaffected by the new key.
assert result["title"] == "04 sample"
assert "본문" in result["clean_text"]
# -- case 2: absent (no slide_overrides key in frontmatter) -----------------
def test_normalize_returns_empty_slide_overrides_when_key_absent():
raw = _mdx_with_frontmatter("title: 03 sample\n")
result = normalize_mdx_content(raw)
assert result["slide_overrides"] == {}
# Confirm key is always present (callers can rely on .get without default).
assert "slide_overrides" in result
# -- case 3: non-string css (fail-closed drop) ------------------------------
def test_normalize_drops_non_string_css_under_slide_overrides():
# YAML list under .css should be dropped; sibling unknown keys survive
# so the future generalization path (e.g., slide_overrides.js) stays
# forward-compatible per the Stage 2 plan.
raw = _mdx_with_frontmatter(
"title: typo case\n"
"slide_overrides:\n"
" css:\n"
" - .f29b__col_right { width: 320px; }\n"
" note: experimental sibling\n"
)
result = normalize_mdx_content(raw)
assert "css" not in result["slide_overrides"]
assert result["slide_overrides"].get("note") == "experimental sibling"
# -- case 4: title-only frontmatter (no slide_overrides at all) -------------
def test_normalize_title_only_frontmatter_yields_empty_slide_overrides():
raw = _mdx_with_frontmatter("title: title only\n")
result = normalize_mdx_content(raw)
assert result["title"] == "title only"
assert result["slide_overrides"] == {}
# -- direct helper coverage (defensive against future return-shape drift) ---
def test_extract_slide_overrides_non_mapping_returns_empty_dict():
# Frontmatter parsers can yield odd types if the user writes
# ``slide_overrides: 42`` or ``slide_overrides: ".x{}"``. The helper
# must coerce to ``{}`` rather than raise.
for bad in (None, 42, "literal string", ["css"]):
assert _extract_slide_overrides({"slide_overrides": bad}) == {}
def test_extract_slide_overrides_passes_through_unknown_siblings():
payload = {"slide_overrides": {"css": ".a{}", "js": "console.log(1)"}}
assert _extract_slide_overrides(payload) == {
"css": ".a{}",
"js": "console.log(1)",
}

View File

@@ -56,6 +56,8 @@ def _exec_main_block(
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
@@ -64,6 +66,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)
@@ -346,3 +350,136 @@ def test_image_override_does_not_leak_into_sibling_axes(tmp_path, monkeypatch):
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 `<style>` wrap)."""
_redirect_overrides_root(tmp_path, monkeypatch)
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-slide-css",
".slide { background: red; }",
],
monkeypatch,
)
assert captured["override_slide_css"] == ".slide { background: red; }"
def test_slide_css_file_override_reads_utf8(tmp_path, monkeypatch):
"""--slide-css-file PATH → kwarg = UTF-8 decoded file contents."""
_redirect_overrides_root(tmp_path, monkeypatch)
css_payload = ".slide-body { color: #1e293b; } /* 한글 주석 */\n"
css_path = tmp_path / "slide_override.css"
css_path.write_text(css_payload, encoding="utf-8")
captured: dict[str, Any] = {}
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--slide-css-file",
str(css_path),
],
monkeypatch,
)
assert captured["override_slide_css"] == css_payload
def test_slide_css_both_flags_set_exits(tmp_path, monkeypatch, capsys):
"""--override-slide-css + --slide-css-file → sys.exit(2)."""
_redirect_overrides_root(tmp_path, monkeypatch)
css_path = tmp_path / "slide_override.css"
css_path.write_text(".slide { color: red; }\n", encoding="utf-8")
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--override-slide-css",
".slide { color: blue; }",
"--slide-css-file",
str(css_path),
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "--override-slide-css and --slide-css-file are mutually exclusive" in err
def test_slide_css_file_missing_path_exits(tmp_path, monkeypatch, capsys):
"""--slide-css-file with non-existent PATH → sys.exit(2)."""
_redirect_overrides_root(tmp_path, monkeypatch)
missing_path = tmp_path / "does_not_exist.css"
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--slide-css-file",
str(missing_path),
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "--slide-css-file path does not exist" in err
assert str(missing_path) in err
def test_slide_css_file_non_utf8_exits(tmp_path, monkeypatch, capsys):
"""--slide-css-file with non-UTF-8 bytes → sys.exit(2)."""
_redirect_overrides_root(tmp_path, monkeypatch)
bad_path = tmp_path / "latin1.css"
# 0xff is a stand-alone invalid UTF-8 start byte; strict decode raises.
bad_path.write_bytes(b".slide { color: \xff; }\n")
captured: dict[str, Any] = {}
with pytest.raises(SystemExit) as excinfo:
_exec_main_block(
captured,
[
"src.phase_z2_pipeline",
"03.mdx",
"--slide-css-file",
str(bad_path),
],
monkeypatch,
)
assert excinfo.value.code == 2
err = capsys.readouterr().err
assert "--slide-css-file must be UTF-8 encoded" in err

View File

@@ -62,6 +62,7 @@ def _exec_main_block(
override_zone_geometries=None,
override_section_assignments=None,
override_image_overrides=None,
override_slide_css=None,
reuse_from=None,
):
captured["called"] = True
@@ -72,6 +73,7 @@ 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)

View File

@@ -0,0 +1,101 @@
"""IMP-45 (#74) u6 — subprocess smoke for the slide-level CSS override axis.
End-to-end guard that the ``slide_overrides.css`` frontmatter axis added
in u2 propagates through the pipeline (u4) and lands in ``final.html``
via :func:`src.slide_css_injector.inject_slide_css` (u3).
The fixture is the new ``slide_overrides.css`` frontmatter block in
``samples/mdx_batch/04.mdx`` (u6 migration of the legacy frontend-only
``MDX04_DEFAULT_OVERRIDE_CSS`` constant). The pipeline is spawned via
``python -m src.phase_z2_pipeline 04.mdx <run_id>`` so the assertion
applies to the on-disk artifact CI / CLI / regression all observe, not
to a live iframe view.
The subprocess returncode is intentionally NOT asserted: mdx04 has known
downstream issues (see ``test_pipeline_smoke_imp85.py`` —
``test_mdx04_no_longer_emits_imp85_crash_signature``) that are tracked
on a separate axis. Step 13 runs before the downstream failure surface,
so ``final.html`` is written with the injected slide CSS marker
regardless. The test asserts ``final.html`` exists and contains both
the IMP-45 sentinel marker and a distinctive CSS substring from the
migrated frontmatter block.
"""
from __future__ import annotations
import subprocess
import sys
import uuid
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SAMPLES_DIR = REPO_ROOT / "samples" / "mdx_batch"
RUNS_DIR = REPO_ROOT / "data" / "runs"
# IMP-45 (#74) u3 marker sentinel emitted by
# :func:`src.slide_css_injector.inject_slide_css` around the injected
# ``<style>`` block. Match the ``_IMP45_STYLE_MARKER_OPEN`` constant in
# ``src/slide_css_injector.py`` byte-for-byte.
IMP45_OPEN_MARKER = "<!--IMP45-SLIDE-CSS:OPEN-->"
IMP45_CLOSE_MARKER = "<!--IMP45-SLIDE-CSS:CLOSE-->"
# Distinctive substring from the migrated frontmatter block in
# ``samples/mdx_batch/04.mdx``. ``f29b__cell:nth-child(n+3)`` is unique
# to the MDX04 slide-level override CSS and does not appear elsewhere in
# the slide_base / partial templates, so its presence in ``final.html``
# is sufficient evidence that the frontmatter axis fed the injector.
MDX04_DISTINCTIVE_CSS_SUBSTRING = ".f29b__cell:nth-child(n+3)"
def _run_pipeline(mdx_name: str, run_id: str, timeout: int = 240) -> subprocess.CompletedProcess:
"""Spawn ``python -m src.phase_z2_pipeline <mdx> <run_id>``."""
return subprocess.run(
[
sys.executable,
"-m",
"src.phase_z2_pipeline",
str(SAMPLES_DIR / mdx_name),
run_id,
],
capture_output=True,
text=True,
timeout=timeout,
cwd=str(REPO_ROOT),
)
def _unique_run_id(prefix: str) -> str:
return f"{prefix}_imp45_slide_css_smoke_{uuid.uuid4().hex[:8]}"
def test_mdx04_slide_overrides_css_lands_in_final_html() -> None:
"""mdx04 ``slide_overrides.css`` frontmatter must reach ``final.html``.
Contract pins both the IMP-45 marker sentinel and a distinctive CSS
substring from the migrated frontmatter block so a regression in
either the frontmatter extractor (u2), the kwarg forwarding (u4),
or the injector (u3) is caught by this single smoke.
"""
run_id = _unique_run_id("mdx04")
cp = _run_pipeline("04.mdx", run_id)
final_html_path = RUNS_DIR / run_id / "phase_z2" / "final.html"
assert final_html_path.is_file(), (
f"final.html not found at {final_html_path}\n"
f"--- stderr tail ---\n{cp.stderr[-1500:]}\n"
f"--- stdout tail ---\n{cp.stdout[-1500:]}"
)
html = final_html_path.read_text(encoding="utf-8")
assert IMP45_OPEN_MARKER in html, (
f"IMP-45 open marker missing from mdx04 final.html ({final_html_path}).\n"
f"slide_overrides.css frontmatter axis did not reach the injector."
)
assert IMP45_CLOSE_MARKER in html, (
f"IMP-45 close marker missing from mdx04 final.html ({final_html_path}).\n"
f"Marker wrap appears unbalanced — check inject_slide_css() output."
)
assert MDX04_DISTINCTIVE_CSS_SUBSTRING in html, (
f"Migrated frontmatter CSS substring "
f"{MDX04_DISTINCTIVE_CSS_SUBSTRING!r} missing from mdx04 final.html "
f"({final_html_path}). The slide_overrides.css block did not propagate."
)

View File

@@ -0,0 +1,100 @@
"""IMP-45 (#74) u3 — tests for :mod:`src.slide_css_injector`.
Mirrors the inject_image_overrides_style test pattern in
``tests/test_image_id_stamper.py`` (lines 306-377) for the new
``slide_css`` axis. Eight cases :
1. empty css returns html unchanged
2. None css returns html unchanged (mirror of empty-css guard for the
typed ``str | None`` signature)
3. inserts before </head> (precedence path 1)
4. case-insensitive head close (precedence path 1, uppercase variant)
5. body-open fallback when no </head> (precedence path 2)
6. document-start fallback when neither head nor body present (path 3)
7. idempotent on second call with identical css
8. replaces existing block when re-called with different css (latest-wins)
Plus one additional guard :
9. marker comments wrap the injected <style> block (open precedes close,
<style> sits between them)
"""
from __future__ import annotations
from src.slide_css_injector import inject_slide_css
_OPEN_MARKER = "<!--IMP45-SLIDE-CSS:OPEN-->"
_CLOSE_MARKER = "<!--IMP45-SLIDE-CSS:CLOSE-->"
def test_inject_slide_css_empty_string_returns_html_unchanged():
html = "<html><head></head><body>x</body></html>"
assert inject_slide_css(html, "") == html
def test_inject_slide_css_none_returns_html_unchanged():
html = "<html><head></head><body>x</body></html>"
assert inject_slide_css(html, None) == html
def test_inject_slide_css_inserts_before_head_close():
html = "<html><head><title>t</title></head><body>x</body></html>"
out = inject_slide_css(html, ".x { color: red; }")
assert "<style>" in out
head_idx = out.lower().index("</head>")
body_idx = out.lower().index("<body")
style_idx = out.index("<style>")
assert style_idx < head_idx < body_idx
def test_inject_slide_css_case_insensitive_head_close():
html = "<HTML><HEAD></HEAD><BODY>x</BODY></HTML>"
out = inject_slide_css(html, ".x { color: red; }")
assert "<style>" in out
assert out.index("<style>") < out.upper().index("</HEAD>")
def test_inject_slide_css_falls_back_to_body_open_when_no_head():
html = "<body>x</body>"
out = inject_slide_css(html, ".x { color: red; }")
assert "<style>" in out
body_open_end = out.index(">", out.index("<body")) + 1
assert out[body_open_end:].lstrip().startswith(_OPEN_MARKER)
def test_inject_slide_css_falls_back_to_document_start_when_no_head_or_body():
html = "<div>fragment</div>"
out = inject_slide_css(html, ".x { color: red; }")
assert out.startswith(_OPEN_MARKER)
assert "<style>" in out
assert out.rstrip().endswith("</div>")
def test_inject_slide_css_is_idempotent_on_second_call():
html = "<html><head></head><body>x</body></html>"
css = ".x { color: red; }"
once = inject_slide_css(html, css)
twice = inject_slide_css(once, css)
assert twice == once
assert once.count(_OPEN_MARKER) == 1
assert twice.count(_OPEN_MARKER) == 1
def test_inject_slide_css_replaces_existing_block_with_new_css():
html = "<html><head></head><body>x</body></html>"
first = inject_slide_css(html, ".old { color: red; }")
second = inject_slide_css(first, ".new { color: blue; }")
assert ".old" not in second
assert ".new" in second
assert second.count(_OPEN_MARKER) == 1
def test_inject_slide_css_wraps_block_with_marker_comments():
html = "<html><head></head><body>x</body></html>"
out = inject_slide_css(html, ".x { color: red; }")
assert _OPEN_MARKER in out
assert _CLOSE_MARKER in out
s = out.index(_OPEN_MARKER)
e = out.index(_CLOSE_MARKER)
assert s < out.index("<style>") < out.index("</style>") < e

View File

@@ -1,9 +1,10 @@
"""IMP-52 (#80) u8 — backend tests for ``src.user_overrides_io``.
Covers the persisted axes called out in the Stage 2 plan
(IMP-51 #79 u1 extended this to 5 axes by adding ``image_overrides``):
(IMP-51 #79 u1 extended this to 5 axes by adding ``image_overrides``;
IMP-45 #74 u1 extended to 6 axes by adding ``slide_css``):
1. Round-trip ``save`` → ``load`` (5 KNOWN_AXES + foreign top-level keys).
1. Round-trip ``save`` → ``load`` (6 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 /
@@ -119,13 +120,20 @@ def _full_payload() -> dict:
"image_overrides": {
"img-1": {"x": 10.0, "y": 20.0, "w": 30.0, "h": 25.0},
},
"slide_css": "<style>.slide .frame-process-product .label { font-size: 14px; }</style>",
}
def test_known_axes_includes_image_overrides():
"""IMP-51 #79 u1 — ``image_overrides`` is a known axis (5 total)."""
"""IMP-51 #79 u1 — ``image_overrides`` is a known axis (now 6 total)."""
assert "image_overrides" in KNOWN_AXES
assert len(KNOWN_AXES) == 5
assert len(KNOWN_AXES) == 6
def test_known_axes_includes_slide_css():
"""IMP-45 #74 u1 — ``slide_css`` is a known axis (6 total)."""
assert "slide_css" in KNOWN_AXES
assert len(KNOWN_AXES) == 6
def test_save_then_load_round_trip(tmp_path):
@@ -152,6 +160,7 @@ def test_save_partial_payload_preserves_other_axes(tmp_path):
assert loaded["zone_sections"] == _full_payload()["zone_sections"]
assert loaded["frames"] == _full_payload()["frames"]
assert loaded["image_overrides"] == _full_payload()["image_overrides"]
assert loaded["slide_css"] == _full_payload()["slide_css"]
def test_save_partial_image_overrides_preserves_other_axes(tmp_path):
@@ -173,6 +182,7 @@ def test_save_partial_image_overrides_preserves_other_axes(tmp_path):
assert loaded["zone_geometries"] == _full_payload()["zone_geometries"]
assert loaded["zone_sections"] == _full_payload()["zone_sections"]
assert loaded["frames"] == _full_payload()["frames"]
assert loaded["slide_css"] == _full_payload()["slide_css"]
def test_save_axis_replaces_not_deep_merges(tmp_path):
@@ -231,9 +241,17 @@ def test_save_writes_pretty_sorted_json_for_diffability(tmp_path):
pos_frames = raw.index('"frames"')
pos_image_overrides = raw.index('"image_overrides"')
pos_layout = raw.index('"layout"')
pos_slide_css = raw.index('"slide_css"')
pos_zg = raw.index('"zone_geometries"')
pos_zs = raw.index('"zone_sections"')
assert pos_frames < pos_image_overrides < pos_layout < pos_zg < pos_zs
assert (
pos_frames
< pos_image_overrides
< pos_layout
< pos_slide_css
< pos_zg
< pos_zs
)
def test_save_leaves_no_tmp_file_on_success(tmp_path):