fix(r3): fold heid's bug hunt — no link offers a pair that 404s, NUL booth names, a FIFO marker, encoded view-state names
Navigation was built from the review ring while the compare GET also demands containment, so an outside symlink (which stays in the ring) was offered by the strip, the steps, the review's Compare control and the flag landing, and 404ed on arrival. Every one is now built from the compare ring (the review ring filtered by the same conjunction, _in_booth). Two pre-existing gaps compare inherits, fixed at the source: resolve_booth caught only OSError, so a NUL in the booth segment was a 500; record_view opened its marker blocking, so a planted FIFO hung every look. Plus: the page treats %73ide=a as side=a, and the subgrid engine floor is stated. Two findings refuted (a chorded click mid-drag never fires pointerup, measured; booth_items never yields an unquotable rel). r3.toml: 57 rows.
This commit is contained in:
+53
-14
@@ -10,11 +10,9 @@ label = "C1 the conjunction loses containment (an outside symlink in the ring op
|
||||
file = "booth/app.py"
|
||||
test = "tests/test_compare.py::test_an_outside_symlink_in_the_ring_is_404"
|
||||
old = '''
|
||||
if not str(target).startswith(str(booth) + os.sep) or not target.is_file():
|
||||
raise HTTPException(status_code=404, detail="no such item")
|
||||
if rel not in ring:'''
|
||||
return str(target).startswith(str(booth) + os.sep) and target.is_file()'''
|
||||
new = '''
|
||||
if rel not in ring:'''
|
||||
return target.is_file()'''
|
||||
|
||||
[[mutation]]
|
||||
label = "C1 the conjunction loses the ring (a doc or a sidecar opens as a side)"
|
||||
@@ -50,14 +48,12 @@ test = "tests/test_compare.py::test_a_look_records_both_seen"
|
||||
old = '''
|
||||
booth = resolve_booth(name)
|
||||
items = booth_items(booth)
|
||||
ring = review_chain(items)
|
||||
a = _compare_side(booth, ring, a)'''
|
||||
a = _compare_side(booth, review_chain(items), a)'''
|
||||
new = '''
|
||||
booth = resolve_booth(name)
|
||||
record_view(booth)
|
||||
items = booth_items(booth)
|
||||
ring = review_chain(items)
|
||||
a = _compare_side(booth, ring, a)'''
|
||||
a = _compare_side(booth, review_chain(items), a)'''
|
||||
|
||||
[[mutation]]
|
||||
label = "C6 compare does not carry data-booth (Reveal all and its restore bail)"
|
||||
@@ -118,8 +114,8 @@ new = ''' side_a = side not in ("", "b")'''
|
||||
label = "C2 the review's Compare does not wrap (the last item compares with itself)"
|
||||
file = "booth/app.py"
|
||||
test = "tests/test_compare.py::test_the_review_offers_compare_with_the_next_item"
|
||||
old = ''' f"&b={quote(ring[(pos + 1) % len(ring)], safe='/')}"),'''
|
||||
new = ''' f"&b={quote(ring[min(pos + 1, len(ring) - 1)], safe='/')}"),'''
|
||||
old = ''' f"&b={quote(cring[(cring.index(f) + 1) % len(cring)], safe='/')}"),'''
|
||||
new = ''' f"&b={quote(cring[min(cring.index(f) + 1, len(cring) - 1)], safe='/')}"),'''
|
||||
|
||||
# ---- C5: the regions and the JS-off flag landing
|
||||
|
||||
@@ -480,11 +476,9 @@ label = "C1 containment is a bare prefix (a sibling booth sharing the name opens
|
||||
file = "booth/app.py"
|
||||
test = "tests/test_compare.py::test_an_outside_symlink_in_the_ring_is_404"
|
||||
old = '''
|
||||
if not str(target).startswith(str(booth) + os.sep) or not target.is_file():
|
||||
raise HTTPException(status_code=404, detail="no such item")'''
|
||||
return str(target).startswith(str(booth) + os.sep) and target.is_file()'''
|
||||
new = '''
|
||||
if not str(target).startswith(str(booth)) or not target.is_file():
|
||||
raise HTTPException(status_code=404, detail="no such item")'''
|
||||
return str(target).startswith(str(booth)) and target.is_file()'''
|
||||
|
||||
[[mutation]]
|
||||
label = "C1 the strip is not in ring order"
|
||||
@@ -511,6 +505,51 @@ test = "tests/test_compare_browser.py::test_a_flags_A_in_place_and_the_stages_su
|
||||
old = '''<a class="film-f{% if x.flagged %} is-flagged{% endif %}'''
|
||||
new = '''<a class="film-f'''
|
||||
|
||||
# ---- the heid bug-hunt fold (01M3ANEPHTDMPP4Q18Z075181W)
|
||||
|
||||
[[mutation]]
|
||||
label = "C1 navigation is built from the review ring (it offers an outside symlink that 404s)"
|
||||
file = "booth/app.py"
|
||||
test = "tests/test_compare.py::test_no_navigation_offers_a_pair_that_404s"
|
||||
old = '''
|
||||
return [r for r in review_chain(items) if _in_booth(booth, r)]'''
|
||||
new = '''
|
||||
return list(review_chain(items))'''
|
||||
|
||||
[[mutation]]
|
||||
label = "a NUL in the booth segment is a 500 (ValueError is not an OSError)"
|
||||
file = "booth/app.py"
|
||||
test = "tests/test_compare.py::test_hostile_booth_names_are_404_not_500"
|
||||
old = '''
|
||||
resolved = candidate.resolve()
|
||||
except (OSError, ValueError):'''
|
||||
new = '''
|
||||
resolved = candidate.resolve()
|
||||
except OSError:'''
|
||||
|
||||
[[mutation]]
|
||||
label = "a FIFO planted at .viewed hangs the look (a blocking open)"
|
||||
file = "booth/app.py"
|
||||
test = "tests/test_compare.py::test_a_planted_fifo_marker_cannot_hang_a_look"
|
||||
old = '''os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o644)'''
|
||||
new = '''os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o644)'''
|
||||
|
||||
[[mutation]]
|
||||
label = "C2 an encoded view-state name survives the rewrite (%73ide=a outlives X)"
|
||||
file = "booth/templates/compare.html"
|
||||
test = "tests/test_compare_browser.py::test_an_encoded_view_state_name_is_still_view_state"
|
||||
old = '''
|
||||
return p && n !== 'side' && n !== 'link';'''
|
||||
new = '''
|
||||
return p && !/^(side|link)(=|$)/.test(p);'''
|
||||
|
||||
# Refuted, not rowed (bug hunt): "a right-click mid-drag ends the pan" — a
|
||||
# second button pressed and released during a drag arrives as chorded
|
||||
# `pointermove` events, never a `pointerup` (measured 3/3 in the test browser,
|
||||
# the pan continuing each time). "A non-UTF-8 name 500s compare in `quote()`" —
|
||||
# booth_items never yields a rel that quote() cannot encode (test_flow's
|
||||
# test_ordinals_count_rendered_items_only).
|
||||
#
|
||||
# Accepted, not rowed: the mode's `onChange: settleAll` (re-deciding which stage
|
||||
# can pan) is redundant with compare's ResizeObserver — 1:1 drops the stage's
|
||||
# padding, so every mode change resizes the stage's content box and the
|
||||
|
||||
@@ -120,6 +120,51 @@ def test_an_outside_symlink_in_the_ring_is_404(tmp_path):
|
||||
assert c.get(f"/b/g/compare?a=p.png&b={rel}").status_code == 404, rel
|
||||
|
||||
|
||||
def test_no_navigation_offers_a_pair_that_404s(tmp_path):
|
||||
"""An outside symlink stays in the review ring, and compare 404s it. So no
|
||||
compare link may offer it: not the strip, not a step, not the review's
|
||||
Compare control, not the JS-off flag landing (heid bug hunt, 3 of 4)."""
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG, "c.png": PNG})
|
||||
outside = tmp_path / "elsewhere.png"
|
||||
outside.write_bytes(PNG)
|
||||
(b / "b-link.png").symlink_to(outside)
|
||||
c = _client(tmp_path)
|
||||
body = c.get("/b/g/compare?a=a.png&b=c.png").text
|
||||
assert list(_frames(body)) == ["a.png", "c.png"], _frames(body)
|
||||
for h in _compare_links(body):
|
||||
q = parse_qs(urlsplit(h).query)
|
||||
assert "b-link.png" not in (q["a"][0], q["b"][0]), h
|
||||
assert _step(body, "a-next") == ("c.png", "c.png") # steps over it
|
||||
assert _compare_href(c.get("/b/g/view?f=a.png").text) == ("a.png", "c.png")
|
||||
r = c.post("/b/g/flag", data={"target": "a.png", "on": "1", "back": "compare",
|
||||
"a": "a.png", "b": "b-link.png"})
|
||||
assert r.headers["location"] == "/b/g/#item-a.png", r.headers["location"]
|
||||
|
||||
|
||||
def test_hostile_booth_names_are_404_not_500(tmp_path):
|
||||
"""A NUL in the booth segment makes Path.resolve raise ValueError, which
|
||||
is not an OSError: it must still be a 404 (heid bug hunt, hulda)."""
|
||||
_four(tmp_path)
|
||||
c = _client(tmp_path)
|
||||
for path in ("/b/g%00/compare?a=p.png&b=q.png", "/b/g%00/view?f=p.png", "/b/g%00/"):
|
||||
assert c.get(path).status_code == 404, path
|
||||
|
||||
|
||||
def test_a_planted_fifo_marker_cannot_hang_a_look(tmp_path):
|
||||
"""Recording a look never costs the page: a FIFO planted at `.viewed` must
|
||||
not block the open that touches it (heid bug hunt, hulda)."""
|
||||
import os
|
||||
import threading
|
||||
b = _four(tmp_path)
|
||||
os.mkfifo(b / ".viewed")
|
||||
got = []
|
||||
t = threading.Thread(target=lambda: got.append(
|
||||
_client(tmp_path).get("/b/g/compare?a=p.png&b=q.png").status_code), daemon=True)
|
||||
t.start()
|
||||
t.join(10)
|
||||
assert got == [200], "a planted FIFO held the look open"
|
||||
|
||||
|
||||
def test_a_look_records_both_seen(tmp_path):
|
||||
"""A compare GET is a look at both sides; a 404 records nothing."""
|
||||
import json
|
||||
|
||||
@@ -419,6 +419,22 @@ def test_a_save_keeps_the_active_side(browser, live):
|
||||
assert picked == ("m-2-forge-s23.png", "r-1-dock-s11.png") and side == "a", (picked, side)
|
||||
|
||||
|
||||
def test_an_encoded_view_state_name_is_still_view_state(browser, live):
|
||||
"""`%73ide=a` IS `side=a` to the server, so the page must treat it as view
|
||||
state too: after X, a reload shows the side X chose, not the stale one
|
||||
the encoded parameter still named (heid bug hunt, hulda)."""
|
||||
base, root = live
|
||||
_bakeoff(root)
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
_open(page, f"{base}/b/g/compare?a=m-1-dock-s11.png&b=r-1-dock-s11.png&%73ide=a")
|
||||
first = page.evaluate("document.querySelector('.cmp-side.is-active').dataset.side")
|
||||
page.keyboard.press("x")
|
||||
page.reload(wait_until="networkidle")
|
||||
after = page.evaluate("document.querySelector('.cmp-side.is-active').dataset.side")
|
||||
page.close()
|
||||
assert (first, after) == ("a", "b"), (first, after)
|
||||
|
||||
|
||||
def test_without_js_every_judgment_and_step_still_works(browser, live):
|
||||
"""INV-4. Scripts off: the pair renders in two Fit stages, the step and
|
||||
strip links navigate, both flag forms are there and a flag lands back on
|
||||
|
||||
Reference in New Issue
Block a user