feat(#79): IMP-51 image_overrides axis (u1~u11 backend stamp+CLI+CSS inject + frontend drag/resize+persistence + tests)
This commit is contained in:
@@ -47,6 +47,7 @@ def _exec_main_block(
|
||||
override_frames=None,
|
||||
override_zone_geometries=None,
|
||||
override_section_assignments=None,
|
||||
override_image_overrides=None,
|
||||
):
|
||||
captured["mdx_path"] = mdx_path
|
||||
captured["run_id"] = run_id
|
||||
@@ -54,6 +55,7 @@ def _exec_main_block(
|
||||
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
|
||||
|
||||
monkeypatch.setattr(_pz2, "run_phase_z2_mvp1", _fake_run)
|
||||
monkeypatch.setattr(sys, "argv", argv)
|
||||
@@ -95,6 +97,10 @@ def _write_full_payload(tmp_path: Path, stem: str = "03") -> Path:
|
||||
"top": ["03-1"],
|
||||
"bottom": ["03-2", "03-3"],
|
||||
},
|
||||
"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},
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
@@ -114,6 +120,7 @@ def test_no_overrides_file_passes_none_overrides(tmp_path, monkeypatch):
|
||||
assert captured["override_frames"] is None
|
||||
assert captured["override_zone_geometries"] is None
|
||||
assert captured["override_section_assignments"] is None
|
||||
assert captured["override_image_overrides"] is None
|
||||
# MDX path / run_id propagate untouched.
|
||||
assert captured["mdx_path"] == Path("03.mdx")
|
||||
assert captured["run_id"] is None
|
||||
@@ -122,7 +129,7 @@ def test_no_overrides_file_passes_none_overrides(tmp_path, monkeypatch):
|
||||
# -- 2. file fills every axis when CLI is empty ----------------------------
|
||||
|
||||
|
||||
def test_file_only_fills_all_four_axes_when_cli_empty(tmp_path, monkeypatch):
|
||||
def test_file_only_fills_all_five_axes_when_cli_empty(tmp_path, monkeypatch):
|
||||
_redirect_overrides_root(tmp_path, monkeypatch)
|
||||
_write_full_payload(tmp_path, "03")
|
||||
|
||||
@@ -142,6 +149,10 @@ def test_file_only_fills_all_four_axes_when_cli_empty(tmp_path, monkeypatch):
|
||||
"top": ["03-1"],
|
||||
"bottom": ["03-2", "03-3"],
|
||||
}
|
||||
assert captured["override_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},
|
||||
}
|
||||
|
||||
|
||||
# -- 3. CLI beats file on the same axis -----------------------------------
|
||||
@@ -190,6 +201,37 @@ def test_cli_frames_overrides_file_frames(tmp_path, monkeypatch):
|
||||
assert captured["override_layout"] == "sidebar-right"
|
||||
assert captured["override_zone_geometries"] is not None
|
||||
assert captured["override_section_assignments"] is not None
|
||||
assert captured["override_image_overrides"] is not None
|
||||
|
||||
|
||||
# -- 3b. CLI image override beats file image override (IMP-51 #79 u6) -----
|
||||
|
||||
|
||||
def test_cli_image_override_overrides_file_image_overrides(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-image",
|
||||
"img-cli=70,80,20,15",
|
||||
],
|
||||
monkeypatch,
|
||||
)
|
||||
|
||||
# CLI ``image_overrides`` payload wholly replaces file payload (per-axis).
|
||||
assert captured["override_image_overrides"] == {
|
||||
"img-cli": {"x": 70.0, "y": 80.0, "w": 20.0, "h": 15.0},
|
||||
}
|
||||
# Other axes still come from the file.
|
||||
assert captured["override_layout"] == "sidebar-right"
|
||||
assert captured["override_frames"] is not None
|
||||
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 ----------------
|
||||
@@ -209,6 +251,7 @@ def test_corrupt_json_warns_and_skips_fallback(tmp_path, monkeypatch, capsys):
|
||||
assert captured["override_frames"] is None
|
||||
assert captured["override_zone_geometries"] is None
|
||||
assert captured["override_section_assignments"] is None
|
||||
assert captured["override_image_overrides"] is None
|
||||
|
||||
|
||||
def test_non_object_top_level_warns_and_skips_fallback(
|
||||
@@ -226,6 +269,7 @@ def test_non_object_top_level_warns_and_skips_fallback(
|
||||
assert captured["override_frames"] is None
|
||||
assert captured["override_zone_geometries"] is None
|
||||
assert captured["override_section_assignments"] is None
|
||||
assert captured["override_image_overrides"] is None
|
||||
|
||||
|
||||
# -- 5. invalid MDX stem warns and skips fallback wholesale ---------------
|
||||
@@ -251,6 +295,7 @@ def test_invalid_mdx_stem_warns_and_skips_fallback(
|
||||
assert captured["override_frames"] is None
|
||||
assert captured["override_zone_geometries"] is None
|
||||
assert captured["override_section_assignments"] is None
|
||||
assert captured["override_image_overrides"] is None
|
||||
|
||||
|
||||
# -- 6. per-axis partial fill (file fills only what CLI omits) ------------
|
||||
@@ -294,3 +339,72 @@ def test_per_axis_partial_fill_mixes_cli_and_file(tmp_path, monkeypatch):
|
||||
"top": {"x": 0.0, "y": 0.0, "w": 1.0, "h": 0.5},
|
||||
}
|
||||
assert captured["override_section_assignments"] is None
|
||||
assert captured["override_image_overrides"] is None
|
||||
|
||||
|
||||
# -- 7. image_overrides fallback edge cases (IMP-51 #79 u6) ---------------
|
||||
|
||||
|
||||
def test_image_overrides_fallback_drops_malformed_entries(tmp_path, monkeypatch):
|
||||
"""File carries a mix of valid + malformed image_overrides entries.
|
||||
|
||||
Expected: valid entry survives; malformed entries (non-string id,
|
||||
empty id, non-dict value, missing key, non-numeric value) are silently
|
||||
dropped — no exception propagates.
|
||||
"""
|
||||
_redirect_overrides_root(tmp_path, monkeypatch)
|
||||
(tmp_path / "03.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"image_overrides": {
|
||||
"img-valid": {"x": 1.0, "y": 2.0, "w": 3.0, "h": 4.0},
|
||||
"": {"x": 1.0, "y": 2.0, "w": 3.0, "h": 4.0},
|
||||
"img-not-dict": "oops",
|
||||
"img-missing-h": {"x": 1.0, "y": 2.0, "w": 3.0},
|
||||
"img-bad-value": {"x": "abc", "y": 2.0, "w": 3.0, "h": 4.0},
|
||||
}
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
captured: dict[str, Any] = {}
|
||||
_exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch)
|
||||
|
||||
assert captured["override_image_overrides"] == {
|
||||
"img-valid": {"x": 1.0, "y": 2.0, "w": 3.0, "h": 4.0},
|
||||
}
|
||||
|
||||
|
||||
def test_image_overrides_fallback_non_dict_axis_is_ignored(tmp_path, monkeypatch):
|
||||
"""File ``image_overrides`` is a non-dict (list); fallback silently skips."""
|
||||
_redirect_overrides_root(tmp_path, monkeypatch)
|
||||
(tmp_path / "03.json").write_text(
|
||||
json.dumps({"image_overrides": ["not", "a", "dict"]}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
captured: dict[str, Any] = {}
|
||||
_exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch)
|
||||
|
||||
# ``overrides_images`` stays empty; ``or None`` collapses on call site.
|
||||
assert captured["override_image_overrides"] is None
|
||||
|
||||
|
||||
def test_image_overrides_fallback_coerces_int_values_to_float(tmp_path, monkeypatch):
|
||||
"""JSON-loaded ints (e.g. ``10`` not ``10.0``) must coerce to float."""
|
||||
_redirect_overrides_root(tmp_path, monkeypatch)
|
||||
(tmp_path / "03.json").write_text(
|
||||
json.dumps(
|
||||
{"image_overrides": {"img-int": {"x": 10, "y": 20, "w": 30, "h": 40}}}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
captured: dict[str, Any] = {}
|
||||
_exec_main_block(captured, ["src.phase_z2_pipeline", "03.mdx"], monkeypatch)
|
||||
|
||||
coerced = captured["override_image_overrides"]
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user