fix(as-S5a): fixup from booth-dev's gate — release asks, fail-closed words, ids that cannot collide

From booth-dev's hulda bug-hunt with heid's second voice (BRINGA, thread
01M3MVGQ7QSCCK8WT59TQ4J469):
- The booth page's "kept — release" asks by name, as the Desk's does.
- WORDS has no prototype: data-confirm="__proto__" or "constructor" is an
  unknown word, and asks, instead of throwing before preventDefault.
- The confirm helper moved into <head>: its capture listener exists
  before any form, so a click during load is asked too (the inline
  confirm() it replaced had that property).
- shown() also marks U+2028/U+2029 and the zero-width characters.
- Derived ids take ':' (bk-ask-<id>-<key>:prompt, bk-ask-<id>:title), which
  no id or key can contain; '-prompt' and '-title' collided with valid
  keys. booth-dev's chip test now looks its fragment up by [id=...].
- human_dur says "—" for a value that is not finite, instead of raising.
- The tile's copy of a note drops its id (booth-dev: mark-<id> is the
  panel's article).
- Four guards that asserted source patterns now also hold on computed
  effects: embed rings, rings inside clipping containers, the withdraw ×
  on both axes, and question-level notes fields.

Contract: as_antislop S5a (fixup). Falsifiers: antislop.toml 102/102 with
r2_flow.toml 24/24 proved; the full gate follows.
This commit is contained in:
vh
2026-09-28 13:58:52 -07:00
parent d4f64fd7ec
commit 7143fae6c7
9 changed files with 370 additions and 53 deletions
+56 -2
View File
@@ -329,6 +329,12 @@ def _s5_booth(data):
write_note(b, "a.png", "soft edges")
write_note(b, None, "about the booth")
declare_pick(b, "p1", {"title": "Round one", "prompt": "Which?", "options": ["North", "South"]})
# S5a fixup: a multi-question ask with per-question notes, and keys that end
# like the ids S5a derives from them (`-prompt`, `title`)
declare_pick(b, "p2", {"title": "Round two", "questions": [
{"key": "x-prompt", "prompt": "First?", "options": ["keep", "cut"], "notes": True},
{"key": "x", "prompt": "Second?", "options": ["keep", "cut"], "notes": True},
{"key": "title", "prompt": "Third?", "options": ["keep", "cut"]}]})
_s5_board(data)
return b
@@ -415,7 +421,7 @@ def test_fields_are_named(client):
def test_radio_groups_are_named_and_ids_are_unique(client):
c, data = client
_s5_booth(data)
(m,) = c.get("/b/b/embed.json").json()["marks"]
(m,) = [m for m in c.get("/b/b/embed.json").json()["marks"] if m["id"] == "p1"]
# The embed places an ask one of two ways: `whole` (title, questions and
# submit in one piece), or its questions one by one plus `submit`. Never both.
placements = {"whole": m["whole"], "parts": "".join(q["html"] for q in m["questions"]) + m["submit"]}
@@ -466,7 +472,7 @@ def test_wipe_now_asks_by_name(client):
page = c.get("/b/b/").text
form = re.search(r'<form class="wipe wipe-lg"[^>]*>', page, flags=re.S).group(0)
assert 'data-booth="b"' in form and 'data-confirm="wipe"' in form and "onsubmit" not in form, form
assert "var WORDS = {" in page, "the shared confirm helper is on the booth page"
assert "var WORDS = Object.create(null);" in page, "the shared confirm helper is on the booth page"
def test_human_dur_rolls_up_to_days():
@@ -497,3 +503,51 @@ def test_a_truncated_why_carries_its_full_text(client):
(b / ".booth.json").write_text('{"handle": "design-dev", "why": "a long reason that the Desk truncates with an ellipsis"}')
page = c.get("/").text
assert re.search(r'<span class="prov-why" title="a long reason that the Desk truncates with an ellipsis">', page)
# ---- S5a fixup: booth-dev's gate (hulda bug-hunt + heid BRINGA, thread 01M3MVGQ7QSCCK8WT59TQ4J469) ----
def test_no_id_repeats_on_any_page(client):
"""An id names one element. The tile's copy of a note used to repeat the
panel article's `mark-<id>` (booth-dev: the id is the article's); a question
key ending in `-prompt`, or named `title`, repeated the ids S5a derived."""
c, data = client
_s5_booth(data)
pages = _pages(c)
for m in c.get("/b/b/embed.json").json()["marks"]:
pages[f"embed {m['id']} whole"] = m["whole"]
pages[f"embed {m['id']} parts"] = "".join(q["html"] for q in m["questions"]) + m["submit"]
for url, page in pages.items():
ids = re.findall(r'\sid="([^"]+)"', _markup(page))
dupes = sorted({i for i in ids if ids.count(i) > 1})
assert not dupes, (url, dupes[:5])
for ref in re.findall(r'aria-labelledby="([^"]+)"', page):
assert f'id="{ref}"' in page, (url, ref)
def test_release_on_the_booth_page_asks_by_name(client):
c, data = client
b = data / "k"
b.mkdir()
(b / "x.txt").write_text("x")
(b / ".forever").write_text("")
page = c.get("/b/k/").text
form = re.search(r'<form class="keep-lg"[^>]*>', page).group(0)
assert 'data-booth="k"' in form and 'data-confirm="release"' in form, form
def test_the_confirm_helper_is_listening_before_the_body_exists(client):
"""A click while the page is still loading must be asked too: the capture
listener is registered in <head>, not after the footer."""
c, data = client
_s5_booth(data)
for url in ("/", "/b/b/"):
page = c.get(url).text
assert page.index("function shown(n)") < page.index("</head>"), url
def test_human_dur_never_raises():
from booth.app import human_dur
for bad in (float("nan"), float("inf"), float("-inf")):
assert isinstance(human_dur(bad), str), bad