Found by design-dev, the same class as the wipe dialog he had just fixed on the Desk, and reported across the fence rather than kept. A board row's description and URL are written by any of seventeen agent handles and were pasted RAW into the `confirm()` the operator reads before approving a delete. A bidi override (U+202E) or a newline in either re-orders or hides what he is consenting to, so the row shown is not the row removed. Escaping does nothing here and that is the trap: autoescape protects the PAGE, but `confirm` renders a plain string, so the markup defence everyone reaches for first is irrelevant to the surface that actually carries the decision. Control and bidi formatting characters now render as U+FFFD — visibly mangled, never silently re-ordered — through the same helper shape design-dev used, so the two dialogs cannot drift apart. Both arguments go through it, and the mutation row defeats exactly that: taking the raw description back for one of the two turns the test red. 763 green.
215 lines
7.3 KiB
TOML
215 lines
7.3 KiB
TOML
# U7 — every falsifier the navigation unit claims, and the change it forbids.
|
|
#
|
|
# Generated from the session harness that proved them, not retyped. Each `old`
|
|
# must match the source byte-for-byte; a drifted anchor is REPORTED, never
|
|
# silently skipped — a table that stops matching stops proving anything.
|
|
|
|
unit = "the rail, the filters, the grid keyboard, and the groups"
|
|
|
|
[[mutation]]
|
|
label = "INV-2 sort the grid by (group, rel) so groups render contiguously"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_grouping_never_reorders_the_grid"
|
|
old = '''
|
|
shown = buckets[active]'''
|
|
new = '''
|
|
shown = sorted(buckets[active], key=lambda i: (i["group"] or "", i["name"]))'''
|
|
|
|
[[mutation]]
|
|
label = "INV-3a drop the >=2 groups guard (a rail with one row)"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_no_group_rail_when_there_is_only_one_group"
|
|
old = '''
|
|
if len(sizes) < 2 or sizes[len(sizes) // 2] <= 1:'''
|
|
new = '''
|
|
if len(sizes) < 1 or sizes[len(sizes) // 2] <= 1:'''
|
|
|
|
[[mutation]]
|
|
label = "INV-3b drop the median guard (a rail that is a second copy of the grid)"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_no_group_rail_when_every_item_is_its_own_group"
|
|
old = '''
|
|
if len(sizes) < 2 or sizes[len(sizes) // 2] <= 1:'''
|
|
new = '''
|
|
if len(sizes) < 2:'''
|
|
|
|
[[mutation]]
|
|
label = "groups derived from the FULL gallery, not the rendered list"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_groups_describe_the_filtered_grid"
|
|
old = '''
|
|
"groups": _groups(shown),'''
|
|
new = '''
|
|
"groups": _groups(gallery),'''
|
|
|
|
[[mutation]]
|
|
label = "the anchor names the group key instead of the tile id"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_every_group_anchor_lands_on_a_rendered_tile"
|
|
old = '''
|
|
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
|
|
new = '''
|
|
{"key": k, "n": len(v), "anchor": f"group-{k}"}'''
|
|
|
|
[[mutation]]
|
|
label = "the rail orders groups alphabetically instead of by first member"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_group_order_is_the_position_of_the_first_member"
|
|
old = '''
|
|
for k, v in by_group.items()'''
|
|
new = '''
|
|
for k, v in sorted(by_group.items())'''
|
|
|
|
[[mutation]]
|
|
label = "the rail orders groups by count, which the docstring also claims differs"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_group_order_is_the_position_of_the_first_member"
|
|
old = '''
|
|
for k, v in by_group.items()'''
|
|
new = '''
|
|
for k, v in sorted(by_group.items(), key=lambda kv: -len(kv[1]))'''
|
|
|
|
[[mutation]]
|
|
label = "_group_of reverts to the contract's original strip-trailing-digits rule"
|
|
file = "booth/items.py"
|
|
test = "tests/test_items.py::test_group_of_takes_the_first_segment"
|
|
old = '''
|
|
segs = _SEG.split(stem)
|
|
if len(segs) == 1:
|
|
return re.sub(r"\d+$", "", stem) or None
|
|
return segs[0] or None'''
|
|
new = '''
|
|
m = re.match(r"^(.*?)[-_. ]?\d+$", stem)
|
|
return (m.group(1) or None) if m else (stem or None)'''
|
|
|
|
[[mutation]]
|
|
label = "INV-1 a route body derives the group inline"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_no_route_body_derives_a_group"
|
|
old = '''
|
|
by_group: dict[str, list[dict]] = {}'''
|
|
new = '''
|
|
_ = _group_of # noqa
|
|
by_group: dict[str, list[dict]] = {}'''
|
|
|
|
[[mutation]]
|
|
label = "the rail markup is emitted with |safe"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_navigation.py::test_a_hostile_filename_cannot_break_out_of_the_rail"
|
|
old = '''
|
|
href="#{{ g.anchor }}">{{ g.key }} <b>{{ g.n }}</b></a>'''
|
|
new = '''
|
|
href="#{{ g.anchor }}">{{ g.key|safe }} <b>{{ g.n }}</b></a>'''
|
|
|
|
[[mutation]]
|
|
label = "a flat all-digit stem yields the empty string instead of None"
|
|
file = "booth/items.py"
|
|
test = "tests/test_navigation.py::test_a_group_key_is_never_the_empty_string"
|
|
old = '''
|
|
return re.sub(r"\d+$", "", stem) or None'''
|
|
new = '''
|
|
return re.sub(r"\d+$", "", stem)'''
|
|
|
|
[[mutation]]
|
|
label = "the template renders the group row whenever there is any group at all"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_navigation.py::test_no_group_rail_when_every_item_is_its_own_group"
|
|
old = '''
|
|
{% if rail.groups %}'''
|
|
new = '''
|
|
{% if rail.groups is not none %}'''
|
|
|
|
[[mutation]]
|
|
label = "the anchor is built from the raw name instead of the encoded url"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_a_group_anchor_survives_a_filename_that_percent_decodes"
|
|
old = '''
|
|
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
|
|
new = '''
|
|
{"key": k, "n": len(v), "anchor": f"item-{v[0]['name']}"}'''
|
|
|
|
[[mutation]]
|
|
label = "the anchor names the LAST member instead of the first"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_a_group_anchor_names_the_FIRST_member"
|
|
old = '''
|
|
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
|
|
new = '''
|
|
{"key": k, "n": len(v), "anchor": f"item-{v[-1]['url']}"}'''
|
|
|
|
[[mutation]]
|
|
label = "a group row over-reports its own size"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_a_group_row_reports_its_own_size"
|
|
old = '''
|
|
{"key": k, "n": len(v), "anchor": f"item-{v[0]['url']}"}'''
|
|
new = '''
|
|
{"key": k, "n": len(v) + 1, "anchor": f"item-{v[0]['url']}"}'''
|
|
|
|
[[mutation]]
|
|
label = "the informativeness guard reads the LARGEST group, not the middle"
|
|
file = "booth/app.py"
|
|
test = "tests/test_navigation.py::test_the_informativeness_guard_reads_the_middle_not_the_largest"
|
|
old = '''
|
|
if len(sizes) < 2 or sizes[len(sizes) // 2] <= 1:'''
|
|
new = '''
|
|
if len(sizes) < 2 or sizes[-1] <= 1:'''
|
|
|
|
[[mutation]]
|
|
label = "the rail is gated on the FILTERED list, removing the way back"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_navigation.py::test_a_filter_that_matches_nothing_leaves_a_way_back"
|
|
old = '''
|
|
{% elif all_items %}'''
|
|
new = '''
|
|
{% elif items %}'''
|
|
|
|
[[mutation]]
|
|
label = "the keyboard flag selector names a class nothing emits"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_navigation.py::test_the_keyboard_flag_targets_a_real_button"
|
|
old = '''
|
|
case 'f': click('.flagtoggle button');'''
|
|
new = '''
|
|
case 'f': click('.flagbtn, [name="target"]');'''
|
|
|
|
[[mutation]]
|
|
label = "an unrepresentable filename is let through and 500s the booth"
|
|
file = "booth/items.py"
|
|
test = "tests/test_items.py::test_one_unrepresentable_filename_costs_its_own_tile_not_the_booth"
|
|
old = '''
|
|
try:
|
|
quote(rel, safe="/")
|
|
except UnicodeEncodeError:'''
|
|
new = '''
|
|
try:
|
|
pass
|
|
except UnicodeEncodeError:'''
|
|
|
|
[[mutation]]
|
|
label = "the grid cursor starts at tile 0, so an arrow undoes a group jump"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_embed_browser.py::test_an_arrow_after_a_group_jump_does_not_scroll_back"
|
|
old = '''
|
|
case 'ArrowRight': focus(at < 0 ? fromViewport() : at + 1);'''
|
|
new = '''
|
|
case 'ArrowRight': focus(at + 1);'''
|
|
|
|
[[mutation]]
|
|
label = "the link board drops its href scheme guard"
|
|
file = "booth/links.py"
|
|
test = "tests/test_booth.py::test_the_link_board_refuses_to_render_a_script_href"
|
|
old = '''
|
|
return parts.scheme.lower() in ("http", "https")'''
|
|
new = '''
|
|
return True'''
|
|
|
|
[[mutation]]
|
|
label = "the board delete dialog takes the raw agent-written description"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_booth.py::test_the_board_delete_dialog_cannot_be_rewritten_by_a_link_row"
|
|
old = '''
|
|
var d = shown(btn.getAttribute('data-desc') || '');'''
|
|
new = '''
|
|
var d = btn.getAttribute('data-desc') || '';'''
|