fix(u3): seven defects two cold panels found in the declared seam
The /heid-code-review and /heid-bug-hunt panels, artifact-only over the U3
diff, between them found four real defects and three vacuous falsifiers. Both
snapshots predate the contract-review fixes, so two of their findings were
already closed; the rest are here.
Prototype pollution in the placement maps. A mark id and a question key are
both [A-Za-z0-9][A-Za-z0-9._-]*, so `toString` and `constructor` are legal in
each. Against a plain `{}` an anchor naming NO mark returned an inherited
function, passed the guard meant to reject it, and threw on .questions.length
-- aborting placement before the tail, so one typo in author markup cost the
page every ask. The `placed` set had the mirror bug: inherited
`got.constructor` read as already-placed and silently dropped a question.
Object.create(null), three times. Found independently by both panels.
A declaring page was not served as written. read_text() opens in
universal-newline mode, so a CRLF report came back LF, and errors="replace"
replaced every byte that was not valid UTF-8. That is this unit's headline
promise, broken by the read itself, and the test could not see it because its
fixture was LF-only ASCII. The verbatim branch reads and serves bytes now; the
decoded copy answers only "does it declare the seam?".
A submit anchor inside the author's own <form> lost ours -- the parser drops a
nested form element outright -- while the code still recorded the pick as
submitted, so no fallback was appended. Every control's form= pointed at
nothing and the button did nothing. It counts as submitted only if the form
survived.
A broken pick's diagnostic never rendered from a submit-only anchor: an errored
pick's submit block is empty, and mounting that then marking it placed made the
tail skip the "broken ask" box entirely. The anchor is left alone instead.
An author's own element could hijack the open-ask chip -- id="bk-ask-winner-
background" satisfies any prefix rule, hyphen boundary included. The chip now
searches only elements this script mounted, which is the identity the deleted
bk-ask-<id>-top anchor used to guarantee, and takes the earliest by
compareDocumentPosition.
No error boundary around fragment rendering. A .marks.json that is well-formed
JSON with a wrong-shaped answer hydrates with no error and then raises in the
macro; this endpoint renders every pick on every load of the report, so that
was the whole seam gone while hold_read called the file readable. Reproduced
before building for it. _safe_fragments gives it the per-mark leniency
_hydrate_safe already applies one layer down.
The gallery and marks pages still 500 on that same entry. Measured at 42ea67f
-- it predates this unit, they render the same macro with no guard, and the
gallery is named out of scope in the contract. Recorded, not quietly widened:
persistent-memory.d/2026-09-22-a-wrong-shaped-answer-500s-the-gallery.md
Also corrected: several comments claimed a multi-question pick POSTs a 400
unless every question is answered. It does not -- an empty submission is
refused, a partial one is recorded on purpose. The real reason an unplaced
question must still be appended is that a question which never reaches the page
cannot be answered at all.
Vacuity pass rebuilt around the rule this session learned: the mutation comes
from the invariant's claim, never from the falsifier's example. 21 mutations,
21 caught, unmutated control green. Getting there took three rounds -- it
passed INV-3 with the contract's own mutation, then found its own fix's hole,
then flagged seven stale mutations and one genuinely vacuous fixture whose
sibling-mark arrangement made the right answer also the first answer.
444 tests. Deployed and verified: 23/23 booths 200, and all four live verbatim
reports served at exactly +46 bytes -- len(EMBED_SCRIPT_TAG) -- with the
authors' own wrappers and headings intact and no console errors.
This commit is contained in:
+104
-6
@@ -58,7 +58,8 @@ def test_embed_payload_carries_a_fragment_for_every_shape(client):
|
||||
c, data = client
|
||||
_multi(data / "b")
|
||||
body = c.get("/b/b/embed.json").json()
|
||||
assert body["booth"] == "b" and body["home"] == "/"
|
||||
assert body["booth"] == "b"
|
||||
assert "home" not in body, "a value nothing reads is a second copy waiting to drift"
|
||||
assert body["favicon"].startswith("data:image/svg+xml,")
|
||||
(m,) = body["marks"]
|
||||
assert m["id"] == "batch" and m["error"] is None
|
||||
@@ -80,11 +81,29 @@ def test_a_single_question_pick_has_one_question_with_a_null_key(client):
|
||||
assert "Which render wins?" in m["questions"][0]["html"]
|
||||
|
||||
|
||||
def test_marks_are_ordered_created_then_id(client):
|
||||
def test_marks_are_ordered_by_creation_before_id(client):
|
||||
"""`created` LEADS. The fixture makes creation order and alphabetical order
|
||||
disagree, because a fixture where they agree cannot tell the stated rule
|
||||
from a plain id sort — which is what the first version of this test did, and
|
||||
what a panel caught by reading the fixture rather than the assertion."""
|
||||
c, data = client
|
||||
b = _pick(data / "b", "zebra")
|
||||
_pick(b, "alpha")
|
||||
raw = json.loads((b / ".marks.json").read_text())
|
||||
stamps = {"zebra": "2026-09-22T10:00:00.000000-07:00", # first
|
||||
"alpha": "2026-09-22T11:00:00.000000-07:00"} # second
|
||||
for e in raw["marks"]:
|
||||
e["created"] = stamps[e["id"]]
|
||||
(b / ".marks.json").write_text(json.dumps(raw))
|
||||
assert [m["id"] for m in c.get("/b/b/embed.json").json()["marks"]] == ["zebra", "alpha"]
|
||||
|
||||
|
||||
def test_the_id_is_only_the_tie_break(client):
|
||||
"""And with `created` equal, the id decides — so two marks written in the
|
||||
same second cannot swap between renders."""
|
||||
c, data = client
|
||||
b = _pick(data / "b", "zebra")
|
||||
_pick(b, "alpha")
|
||||
# force identical creation stamps: the id is the tie-break, not json order
|
||||
raw = json.loads((b / ".marks.json").read_text())
|
||||
for e in raw["marks"]:
|
||||
e["created"] = "2026-09-22T10:00:00.000000-07:00"
|
||||
@@ -179,10 +198,16 @@ def test_embed_js_does_not_hot_reload_from_disk(tmp_path):
|
||||
original = src.read_text()
|
||||
app = create_app(tmp_path, ttl_hours=24, start_sweeper=False)
|
||||
c = TestClient(app)
|
||||
served = c.get(EMBED_SRC).text
|
||||
try:
|
||||
# Poisoned BEFORE the first request, not between two of them. The
|
||||
# earlier shape passed for a route that read the file lazily and cached
|
||||
# on first use — which is not "read once at startup", and is exactly the
|
||||
# staleness this invariant exists to forbid.
|
||||
src.write_text("/* POISONED */\n")
|
||||
assert c.get(EMBED_SRC).text == served, "embed.js is being re-read per request"
|
||||
first = c.get(EMBED_SRC).text
|
||||
assert "POISONED" not in first, "embed.js is read at request time, not at startup"
|
||||
assert first == original
|
||||
assert c.get(EMBED_SRC).text == first
|
||||
finally:
|
||||
src.write_text(original)
|
||||
|
||||
@@ -208,8 +233,13 @@ def test_undeclared_page_gains_only_the_tag(client):
|
||||
src = "<!doctype html><meta charset=utf-8><title>r</title><h1>REPORT</h1>"
|
||||
(b / "index.html").write_text(src)
|
||||
out = c.get("/b/b/").text
|
||||
# The literal, not just the constant: `out == src + EMBED_SCRIPT_TAG` also
|
||||
# holds when EMBED_SCRIPT_TAG is the empty string, which is a mutation this
|
||||
# test exists to catch. A panel found it by reading the assertion, not the
|
||||
# code.
|
||||
assert out == src + '<script src="/_booth/embed.js" defer></script>'
|
||||
assert out == src + EMBED_SCRIPT_TAG
|
||||
assert out.startswith(src)
|
||||
assert len(out) > len(src)
|
||||
assert out.lower().lstrip().startswith("<!doctype")
|
||||
assert out.index("charset") < 1024
|
||||
|
||||
@@ -223,6 +253,74 @@ def test_a_booth_with_no_marks_still_gets_the_seam(client):
|
||||
assert c.get("/b/b/").text == "<h1>bare fragment</h1>" + EMBED_SCRIPT_TAG
|
||||
|
||||
|
||||
def test_a_declaring_page_is_served_BYTE_for_byte(client):
|
||||
"""INV-1, at the level the promise is actually made.
|
||||
|
||||
The first version read the file with `read_text()`, which opens in
|
||||
universal-newline mode: a report written with CRLF came back with LF, and
|
||||
`errors="replace"` turned any non-UTF-8 byte into U+FFFD. A declaring page
|
||||
was NOT served as its author wrote it — the headline promise — and the
|
||||
original test could not see it, because its fixture was LF-only ASCII.
|
||||
Found by a cross-frontier bug-hunt panel.
|
||||
"""
|
||||
c, data = client
|
||||
b = _pick(data / "b")
|
||||
src = (b'<!doctype html>\r\n<title>r</title>\r\n<body>caf\xe9 \xff\r\n'
|
||||
b'<script src="/_booth/embed.js" defer></script>\r\n</body>')
|
||||
(b / "index.html").write_bytes(src)
|
||||
r = c.get("/b/b/")
|
||||
assert r.content == src, "the operator's document was edited on the way out"
|
||||
assert b"\r\n" in r.content and b"\xff" in r.content
|
||||
|
||||
|
||||
def test_an_undeclared_page_keeps_every_byte_and_gains_the_tag(client):
|
||||
"""INV-2, same level: the source is a BYTE-exact prefix of the response."""
|
||||
c, data = client
|
||||
b = _pick(data / "b")
|
||||
src = b'<!doctype html>\r\n<title>r</title>\r\n<body>caf\xe9 \xff\r\n</body>'
|
||||
(b / "index.html").write_bytes(src)
|
||||
r = c.get("/b/b/")
|
||||
assert r.content == src + EMBED_SCRIPT_TAG.encode("utf-8")
|
||||
assert r.content.startswith(src)
|
||||
|
||||
|
||||
def test_a_wrongly_shaped_answer_costs_its_pick_not_the_report(client):
|
||||
"""`marks_for` hydrates `{"answer": {"answers": []}}` with no error — the
|
||||
JSON is well formed, the SHAPE is not — and the template then asks a list
|
||||
for `.get`. This endpoint renders every pick on every load of the operator's
|
||||
report, so an unguarded raise here is the whole seam gone while `hold_read`
|
||||
calls the file perfectly readable. Verified reachable, not assumed."""
|
||||
c, data = client
|
||||
b = data / "b"
|
||||
b.mkdir(parents=True, exist_ok=True)
|
||||
declare_pick(b, "batch", {"title": "T", "questions": [
|
||||
{"key": "r1", "prompt": "A?", "options": ["x", "y"]},
|
||||
{"key": "r2", "prompt": "B?", "options": ["x", "y"]}]})
|
||||
_pick(b, "healthy")
|
||||
raw = json.loads((b / ".marks.json").read_text())
|
||||
for e in raw["marks"]:
|
||||
if e["id"] == "batch":
|
||||
e["answer"] = {"answers": [], "notes": ""}
|
||||
(b / ".marks.json").write_text(json.dumps(raw))
|
||||
|
||||
(b / "index.html").write_text(DECLARED)
|
||||
r = c.get("/b/b/embed.json")
|
||||
assert r.status_code == 200
|
||||
by = {m["id"]: m for m in r.json()["marks"]}
|
||||
assert by["batch"]["error"] and "could not be rendered" in by["batch"]["error"]
|
||||
assert "broken ask" in by["batch"]["whole"]
|
||||
# and the booth's other pick is untouched — one bad entry costs one entry
|
||||
assert by["healthy"]["error"] is None
|
||||
assert "Which render wins?" in by["healthy"]["whole"]
|
||||
assert c.get("/b/b/").status_code == 200
|
||||
|
||||
# ⚠ THE GALLERY AND MARKS PAGES STILL 500 ON THIS ENTRY, and that is NOT
|
||||
# U3's doing — measured at 42ea67f, the commit before this unit. They render
|
||||
# the same macro without this guard. Out of scope here (the gallery is named
|
||||
# out of scope in the contract) and recorded rather than quietly widened:
|
||||
# see persistent-memory.d/2026-09-22-a-wrong-shaped-answer-500s-the-gallery.md
|
||||
|
||||
|
||||
def test_no_regex_touches_author_html():
|
||||
"""INV-3, and the version that actually falsifies it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user