Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
u1: text_overrides axis in user_overrides_io u2: structure_overrides axis in user_overrides_io u3: vite allowlist for new endpoints u4: text_override_resolver u5: Step 12 text_overrides apply in phase_z2_pipeline u6: structure_override_resolver u7: text_path_stamper u8: SlideCanvas text-edit capture u9: SlideCanvas structure-edit overlay u10: userOverridesApi service extension u11: designAgent types extension u12: slidePlanUtils restore u13: user_overrides endpoint tests u14: user_overrides restore tests u15: pipeline fallback tests u16: edit-mode state + gating tests u17: slide_base print mode CSS u18: /api/connect endpoint (vite) u19: /api/export endpoint (vite) Recovery scope: 29 files (12 modified + 17 new). u20 already pushed in 9439575; this commit lands u1-u19 that were authored but not committed before #90 was externally closed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
393 lines
15 KiB
Python
393 lines
15 KiB
Python
"""IMP-56 (#90) u6 — tests for ``src.structure_override_resolver``.
|
|
|
|
Covers the resolver contract called out in the Stage 2 plan :
|
|
|
|
1. ``validate_structure_overrides`` returns ``{}`` for non-mapping input.
|
|
2. ``validate_structure_overrides`` preserves well-formed entries.
|
|
3. ``validate_structure_overrides`` drops malformed per-entry rows without
|
|
rejecting the whole batch (per-entry tolerance — mirrors u4
|
|
text_override_resolver contract).
|
|
4. ``validate_structure_overrides`` REJECTS frame swap (any inner key
|
|
other than slot_order / hidden_slots is silently dropped — SCOPE LOCK).
|
|
5. ``validate_structure_overrides`` drops non-list slot_order / hidden_slots
|
|
values.
|
|
6. ``validate_structure_overrides`` drops non-string or empty slot_keys
|
|
inside slot_order / hidden_slots.
|
|
7. ``validate_structure_overrides`` de-duplicates slot_key entries within
|
|
each list.
|
|
8. ``validate_structure_overrides`` returns fresh nested dicts AND lists
|
|
(caller can mutate without aliasing the source).
|
|
9. ``validate_structure_overrides`` drops per-zone payloads that contain
|
|
neither a non-empty slot_order nor a non-empty hidden_slots after
|
|
sanitization.
|
|
10. ``apply_structure_override`` removes hidden_slots in-place and returns
|
|
``True``.
|
|
11. ``apply_structure_override`` reorders the slot-payload mapping per
|
|
slot_order (partial reorder; unmentioned slots keep tail order).
|
|
12. ``apply_structure_override`` combines hide + reorder atomically.
|
|
13. ``apply_structure_override`` silently skips stale slot_keys (frame
|
|
swap / layout regression) without raising.
|
|
14. ``apply_structure_override`` returns ``False`` (no mutation) on a
|
|
no-op override (empty, or all stale).
|
|
15. ``apply_structure_override`` preserves the caller's reference identity
|
|
on ``zone`` (in-place mutation via clear + update).
|
|
16. ``apply_structure_override`` NEVER inspects or mutates per-slot
|
|
payload values — only top-level key membership / ordering.
|
|
17. ``apply_structure_override`` is defensive against non-list
|
|
slot_order / hidden_slots leaking through (treats as empty, no raise).
|
|
|
|
All tests are pure-Python — no filesystem, no Selenium, no fixtures.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from src.structure_override_resolver import (
|
|
InvalidStructureOverride,
|
|
apply_structure_override,
|
|
validate_structure_overrides,
|
|
)
|
|
|
|
|
|
# -- module surface ---------------------------------------------------------
|
|
|
|
|
|
def test_invalid_structure_override_is_value_error_subclass():
|
|
# Reserved future strict-mode exception — kept as a public surface so
|
|
# u7 / strict callers can branch on source-malformation vs stale-DOM.
|
|
assert issubclass(InvalidStructureOverride, ValueError)
|
|
|
|
|
|
# -- validate_structure_overrides ------------------------------------------
|
|
|
|
|
|
def test_validate_structure_overrides_non_mapping_returns_empty():
|
|
for bad_input in [None, [], "string", 42, 1.5]:
|
|
assert validate_structure_overrides(bad_input) == {}
|
|
|
|
|
|
def test_validate_structure_overrides_passes_well_formed():
|
|
payload = {
|
|
"zone-top": {
|
|
"slot_order": ["slot_title", "slot_body"],
|
|
"hidden_slots": ["slot_caption"],
|
|
},
|
|
"zone-bottom": {"slot_order": ["slot_a", "slot_b"]},
|
|
"zone-only-hide": {"hidden_slots": ["slot_x"]},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {
|
|
"zone-top": {
|
|
"slot_order": ["slot_title", "slot_body"],
|
|
"hidden_slots": ["slot_caption"],
|
|
},
|
|
"zone-bottom": {"slot_order": ["slot_a", "slot_b"]},
|
|
"zone-only-hide": {"hidden_slots": ["slot_x"]},
|
|
}
|
|
assert out is not payload # fresh dict
|
|
|
|
|
|
def test_validate_structure_overrides_per_entry_tolerance():
|
|
payload = {
|
|
"zone-top": {
|
|
"slot_order": ["slot_title", "slot_body"],
|
|
"hidden_slots": ["slot_caption"],
|
|
},
|
|
"": {"slot_order": ["x"]}, # empty zone_id dropped
|
|
42: {"slot_order": ["y"]}, # non-string zone_id dropped
|
|
"zone-non-mapping": "not a dict", # non-mapping payload dropped
|
|
"zone-bottom": {"hidden_slots": ["slot_aux"]},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {
|
|
"zone-top": {
|
|
"slot_order": ["slot_title", "slot_body"],
|
|
"hidden_slots": ["slot_caption"],
|
|
},
|
|
"zone-bottom": {"hidden_slots": ["slot_aux"]},
|
|
}
|
|
|
|
|
|
def test_validate_structure_overrides_rejects_frame_swap_inner_keys():
|
|
# SCOPE LOCK — frame swap attempts MUST be silently dropped. The only
|
|
# mechanism for swapping a frame is the existing ``frames`` axis; this
|
|
# resolver intentionally has no escape hatch so the Phase Z
|
|
# no-AI-HTML-structure invariant stays intact.
|
|
payload = {
|
|
"zone-top": {
|
|
"slot_order": ["slot_title"],
|
|
# The following 4 keys are all frame-swap / DOM-rebuild
|
|
# attempts and MUST be dropped by validate.
|
|
"frame_id": "compare_v2",
|
|
"template_id": "topic_left_right",
|
|
"unit_id": "03-1+03-2",
|
|
"slot_payload": {"injected_slot": ["unsafe"]},
|
|
},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {"zone-top": {"slot_order": ["slot_title"]}}
|
|
|
|
|
|
def test_validate_structure_overrides_rejects_frame_swap_zone_with_no_lock_keys():
|
|
# If a per-zone payload contains ONLY frame-swap attempts (no
|
|
# slot_order / hidden_slots), the whole zone gets dropped after
|
|
# sanitization (no signal remains).
|
|
payload = {
|
|
"zone-attempt-swap": {
|
|
"frame_id": "compare_v2",
|
|
"template_id": "topic_left_right",
|
|
},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {}
|
|
|
|
|
|
def test_validate_structure_overrides_drops_non_list_slot_arrays():
|
|
payload = {
|
|
"zone-top": {
|
|
"slot_order": "not a list",
|
|
"hidden_slots": {"also": "not a list"},
|
|
},
|
|
"zone-bottom": {
|
|
"slot_order": 42,
|
|
"hidden_slots": None,
|
|
},
|
|
"zone-good": {"slot_order": ["slot_title"]},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {"zone-good": {"slot_order": ["slot_title"]}}
|
|
|
|
|
|
def test_validate_structure_overrides_drops_bad_slot_key_entries():
|
|
payload = {
|
|
"zone-top": {
|
|
"slot_order": [
|
|
"good_slot",
|
|
"", # empty string dropped
|
|
42, # non-string dropped
|
|
None, # non-string dropped
|
|
{"nested": "obj"}, # non-string dropped
|
|
"another_good",
|
|
],
|
|
"hidden_slots": ["", "valid_hide", 99, "valid_hide_2"],
|
|
},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {
|
|
"zone-top": {
|
|
"slot_order": ["good_slot", "another_good"],
|
|
"hidden_slots": ["valid_hide", "valid_hide_2"],
|
|
},
|
|
}
|
|
|
|
|
|
def test_validate_structure_overrides_dedupes_slot_key_entries():
|
|
payload = {
|
|
"zone-top": {
|
|
"slot_order": ["a", "b", "a", "c", "b"],
|
|
"hidden_slots": ["x", "x", "y", "x"],
|
|
},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {
|
|
"zone-top": {
|
|
"slot_order": ["a", "b", "c"],
|
|
"hidden_slots": ["x", "y"],
|
|
},
|
|
}
|
|
|
|
|
|
def test_validate_structure_overrides_drops_empty_payload_after_sanitization():
|
|
# Per-zone payloads that have empty slot_order AND empty hidden_slots
|
|
# after sanitization carry no signal → drop the zone entirely.
|
|
payload = {
|
|
"zone-empty-lists": {"slot_order": [], "hidden_slots": []},
|
|
"zone-only-bad-entries": {"slot_order": ["", None, 99]},
|
|
"zone-good": {"slot_order": ["slot_title"]},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
assert out == {"zone-good": {"slot_order": ["slot_title"]}}
|
|
|
|
|
|
def test_validate_structure_overrides_returns_fresh_nested_dicts_and_lists():
|
|
# Mutating the returned dict's per-zone payload (or any list inside)
|
|
# must not leak back into the source.
|
|
payload = {
|
|
"zone-top": {
|
|
"slot_order": ["a", "b"],
|
|
"hidden_slots": ["x"],
|
|
},
|
|
}
|
|
out = validate_structure_overrides(payload)
|
|
out["zone-top"]["slot_order"].append("mutated")
|
|
out["zone-top"]["hidden_slots"].append("mutated_hide")
|
|
out["zone-top"]["new_key"] = "leaked?"
|
|
assert payload["zone-top"]["slot_order"] == ["a", "b"]
|
|
assert payload["zone-top"]["hidden_slots"] == ["x"]
|
|
assert "new_key" not in payload["zone-top"]
|
|
|
|
|
|
# -- apply_structure_override ----------------------------------------------
|
|
|
|
|
|
def test_apply_structure_override_hide_only_mutates_in_place():
|
|
zone: dict = {
|
|
"slot_title": ["title line"],
|
|
"slot_body": ["body line"],
|
|
"slot_caption": ["caption"],
|
|
}
|
|
assert apply_structure_override(zone, {"hidden_slots": ["slot_caption"]}) is True
|
|
assert list(zone.keys()) == ["slot_title", "slot_body"]
|
|
assert zone == {
|
|
"slot_title": ["title line"],
|
|
"slot_body": ["body line"],
|
|
}
|
|
|
|
|
|
def test_apply_structure_override_reorder_only_partial():
|
|
# Partial reorder — listed keys move to front in order; unmentioned
|
|
# keys keep their original relative order at the tail.
|
|
zone: dict = {
|
|
"slot_title": ["t"],
|
|
"slot_body": ["b"],
|
|
"slot_caption": ["c"],
|
|
"slot_aux": ["a"],
|
|
}
|
|
assert apply_structure_override(zone, {"slot_order": ["slot_aux", "slot_title"]}) is True
|
|
assert list(zone.keys()) == ["slot_aux", "slot_title", "slot_body", "slot_caption"]
|
|
|
|
|
|
def test_apply_structure_override_combines_hide_and_reorder():
|
|
zone: dict = {
|
|
"slot_title": ["t"],
|
|
"slot_body": ["b"],
|
|
"slot_caption": ["c"],
|
|
"slot_aux": ["a"],
|
|
}
|
|
override = {
|
|
"slot_order": ["slot_aux", "slot_body"],
|
|
"hidden_slots": ["slot_caption"],
|
|
}
|
|
assert apply_structure_override(zone, override) is True
|
|
# slot_caption hidden; slot_aux + slot_body moved to front; remaining
|
|
# (slot_title) appended at tail in original order.
|
|
assert list(zone.keys()) == ["slot_aux", "slot_body", "slot_title"]
|
|
|
|
|
|
def test_apply_structure_override_silently_skips_stale_slot_keys():
|
|
# Frame swap / layout regression — the prior render's override
|
|
# references slot_keys that the new render's frame no longer emits.
|
|
# The resolver must silently skip those without raising.
|
|
zone: dict = {"slot_title": ["t"], "slot_body": ["b"]}
|
|
override = {
|
|
"slot_order": ["slot_phantom_1", "slot_body", "slot_phantom_2"],
|
|
"hidden_slots": ["slot_does_not_exist"],
|
|
}
|
|
assert apply_structure_override(zone, override) is True
|
|
# slot_body moves to front; slot_title appended at tail; phantoms
|
|
# silently ignored; hidden_slots no-op.
|
|
assert list(zone.keys()) == ["slot_body", "slot_title"]
|
|
assert zone == {"slot_body": ["b"], "slot_title": ["t"]}
|
|
|
|
|
|
def test_apply_structure_override_no_op_returns_false():
|
|
# Empty override → no mutation.
|
|
zone: dict = {"slot_title": ["t"], "slot_body": ["b"]}
|
|
snapshot = dict(zone)
|
|
assert apply_structure_override(zone, {}) is False
|
|
assert zone == snapshot
|
|
assert list(zone.keys()) == ["slot_title", "slot_body"]
|
|
|
|
|
|
def test_apply_structure_override_all_stale_returns_false():
|
|
# All slot_keys in the override are absent from zone → no mutation.
|
|
zone: dict = {"slot_title": ["t"], "slot_body": ["b"]}
|
|
snapshot = dict(zone)
|
|
override = {
|
|
"slot_order": ["phantom_a", "phantom_b"],
|
|
"hidden_slots": ["phantom_c"],
|
|
}
|
|
assert apply_structure_override(zone, override) is False
|
|
assert zone == snapshot
|
|
assert list(zone.keys()) == ["slot_title", "slot_body"]
|
|
|
|
|
|
def test_apply_structure_override_already_in_desired_order_returns_false():
|
|
# slot_order matches the existing key order exactly → no mutation.
|
|
zone: dict = {"slot_title": ["t"], "slot_body": ["b"]}
|
|
override = {"slot_order": ["slot_title", "slot_body"]}
|
|
assert apply_structure_override(zone, override) is False
|
|
assert list(zone.keys()) == ["slot_title", "slot_body"]
|
|
|
|
|
|
def test_apply_structure_override_preserves_zone_reference_identity():
|
|
# In-place mutation via clear + update — caller's reference must
|
|
# remain valid after reorder.
|
|
zone: dict = {"slot_a": ["a"], "slot_b": ["b"], "slot_c": ["c"]}
|
|
zone_ref = zone # capture reference
|
|
apply_structure_override(zone, {"slot_order": ["slot_c", "slot_a"]})
|
|
assert zone_ref is zone
|
|
assert list(zone.keys()) == ["slot_c", "slot_a", "slot_b"]
|
|
|
|
|
|
def test_apply_structure_override_never_inspects_per_slot_values():
|
|
# The resolver MUST NOT inspect / mutate per-slot list[str] contents.
|
|
# Use weird non-list values to confirm passthrough.
|
|
zone: dict = {
|
|
"slot_a": ["a1", "a2", "a3"],
|
|
"slot_b": {"nested": "object"}, # non-list payload — passthrough
|
|
"slot_c": None, # None payload — passthrough
|
|
"slot_d": 42, # int payload — passthrough
|
|
}
|
|
snapshot = {k: zone[k] for k in zone}
|
|
apply_structure_override(zone, {"slot_order": ["slot_d", "slot_a"]})
|
|
assert list(zone.keys()) == ["slot_d", "slot_a", "slot_b", "slot_c"]
|
|
# values are identity-preserved
|
|
for key in zone:
|
|
assert zone[key] is snapshot[key]
|
|
|
|
|
|
def test_apply_structure_override_defensive_on_non_list_arrays():
|
|
# If a non-validated override leaks through, non-list slot_order /
|
|
# hidden_slots should be treated as empty rather than raising.
|
|
zone: dict = {"slot_title": ["t"], "slot_body": ["b"]}
|
|
snapshot = dict(zone)
|
|
override = {
|
|
"slot_order": "not a list",
|
|
"hidden_slots": {"also": "not a list"},
|
|
}
|
|
assert apply_structure_override(zone, override) is False
|
|
assert zone == snapshot
|
|
assert list(zone.keys()) == ["slot_title", "slot_body"]
|
|
|
|
|
|
def test_apply_structure_override_hide_wins_over_reorder():
|
|
# Edge case: a slot_key appears in BOTH slot_order and hidden_slots.
|
|
# hidden_slots is applied first, so the slot is gone by the time
|
|
# reorder runs — the reorder entry silently no-ops.
|
|
zone: dict = {"slot_a": ["a"], "slot_b": ["b"], "slot_c": ["c"]}
|
|
override = {
|
|
"slot_order": ["slot_b", "slot_a", "slot_c"],
|
|
"hidden_slots": ["slot_b"],
|
|
}
|
|
assert apply_structure_override(zone, override) is True
|
|
# slot_b removed first; slot_a + slot_c reordered to front (slot_b
|
|
# silently skipped because it no longer exists).
|
|
assert list(zone.keys()) == ["slot_a", "slot_c"]
|
|
|
|
|
|
def test_apply_structure_override_returns_true_on_pure_reorder_only():
|
|
# Pure reorder (no hide) — should still return True when key order
|
|
# actually changes.
|
|
zone: dict = {"slot_a": ["a"], "slot_b": ["b"]}
|
|
override = {"slot_order": ["slot_b", "slot_a"]}
|
|
assert apply_structure_override(zone, override) is True
|
|
assert list(zone.keys()) == ["slot_b", "slot_a"]
|
|
|
|
|
|
def test_apply_structure_override_returns_true_on_pure_hide_only():
|
|
# Pure hide (no reorder) — should still return True when a key was
|
|
# actually removed.
|
|
zone: dict = {"slot_a": ["a"], "slot_b": ["b"]}
|
|
override = {"hidden_slots": ["slot_a"]}
|
|
assert apply_structure_override(zone, override) is True
|
|
assert list(zone.keys()) == ["slot_b"]
|