fix(r2): the heid bug-hunt panel (round "Nyx", 4/4) — triaged and folded
In-place client (base.html): - Saves are serialized: POST, re-fetch and swap complete before the next save starts, so an older snapshot can no longer land after a newer one. - A form already queued or in flight ignores another submit; a double-click writes one note. - Dirty controls (drafts, unsent radio choices) and disclosures carry by identity (form action + hidden ask/target/mark/f + name), not position. - Any non-tile structural difference, or a page with no region to swap, reloads instead of patching. Server and templates: - .seen is a JSON array read without following links or blocking, regular files of at most 1 MiB only; malformed, nested-too-deep or planted markers read as nothing seen. - landed_at reads symlinks by lstat and skips one unreadable entry instead of pinning the booth in "new". - The Desk counts flags on current items only; orphan flags are listed under the tray with an unmark form. - Agent-written bench and bookmark URLs link only when http(s). - Audio and video tiles carry a review link. - A rel the filesystem cannot represent is a 404, not a 500. - A non-finite Accept q-value fails to parse. - The standalone marks page has regions and updates in place. - The review's next arrow sits at the edge at phone width. Contract amended for each, plus an accepted-risks section (unlocked .seen read-modify-write, a planted .viewed symlink, Item.ordinal with no default). 741 passed. Each new browser test was mutation-checked against its fix; the serialization test forces the race with a held first refresh, since localhost alone never lost it.
This commit is contained in:
+125
-1
@@ -113,7 +113,7 @@ def test_a_full_size_look_is_recorded_as_seen_and_a_non_item_is_not(tmp_path):
|
||||
for f in ("a.png", "c.png", "a.png", ".marks.lock"):
|
||||
c.get(f"/b/g/view?f={f}")
|
||||
assert read_seen(b) == {"a.png", "c.png"}
|
||||
assert (b / ".seen").read_text() == "a.png\nc.png\n" # sorted, deduplicated
|
||||
assert (b / ".seen").read_text() == '["a.png", "c.png"]' # sorted, deduplicated, JSON
|
||||
|
||||
|
||||
def test_seen_is_pruned_to_live_items_at_the_next_write(tmp_path):
|
||||
@@ -673,3 +673,127 @@ def test_a_full_size_look_also_counts_as_looking_at_the_booth(tmp_path):
|
||||
assert not (b / ".viewed").exists()
|
||||
_client(tmp_path).get("/b/g/view?f=a.png")
|
||||
assert (b / ".viewed").exists() and (b / ".seen").exists()
|
||||
|
||||
|
||||
def test_a_flag_on_a_file_that_is_gone_stays_visible_and_withdrawable(tmp_path):
|
||||
"""Nyx N3 (2/4): the tray only shows live items, and `tray` being always
|
||||
defined killed the old list fallback — so a flag whose file was deleted
|
||||
rendered NOWHERE on the booth page while the Desk still counted it. It is
|
||||
now listed apart, with its withdraw control; the Desk counts live items."""
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG, "b.png": PNG})
|
||||
set_flag(b, "a.png", True)
|
||||
set_flag(b, "b.png", True)
|
||||
(b / "b.png").unlink()
|
||||
c = _client(tmp_path)
|
||||
aside = _region(c.get("/b/g/").text, "verdict")
|
||||
orphans = re.search(r'class="orphan-flags".*?</ul>', aside, re.S)
|
||||
assert orphans and "b.png" in orphans.group(0)
|
||||
assert 'action="/b/g/unmark"' in orphans.group(0)
|
||||
row = re.search(r'data-booth="g".*?</article>', c.get("/").text, re.S).group(0)
|
||||
assert "1 flagged" in row
|
||||
|
||||
|
||||
def test_a_planted_fifo_or_device_seen_marker_cannot_hang_the_review(tmp_path):
|
||||
"""Nyx N4 (3/4): `.seen` was read with an unbounded, symlink-following
|
||||
read_text(). A FIFO with no writer blocked the worker forever; a symlink to
|
||||
/dev/zero read until memory ran out. The read now refuses anything that is
|
||||
not a small regular file, without following a link."""
|
||||
import os
|
||||
import threading
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG})
|
||||
os.mkfifo(b / ".seen")
|
||||
out = {}
|
||||
t = threading.Thread(target=lambda: out.setdefault(
|
||||
"r", _client(tmp_path).get("/b/g/view?f=a.png")), daemon=True)
|
||||
t.start()
|
||||
t.join(timeout=5)
|
||||
assert "r" in out, "the review hung on a FIFO .seen"
|
||||
assert out["r"].status_code == 200
|
||||
h = _booth(tmp_path, "h", {"a.png": PNG})
|
||||
(h / ".seen").symlink_to("/dev/zero")
|
||||
assert _client(tmp_path).get("/b/h/view?f=a.png").status_code == 200
|
||||
|
||||
|
||||
def test_seen_round_trips_names_with_spaces_and_newlines(tmp_path):
|
||||
"""Nyx (groa, hulda): one stripped line per rel lost ` a.png` and split a
|
||||
name holding a newline into two identities. The marker is a JSON array."""
|
||||
from booth.items import read_seen
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG, " a.png": PNG, "x\ny.png": PNG})
|
||||
c = _client(tmp_path)
|
||||
c.get("/b/g/view", params={"f": " a.png"})
|
||||
c.get("/b/g/view", params={"f": "x\ny.png"})
|
||||
assert read_seen(b) == {" a.png", "x\ny.png"}
|
||||
|
||||
|
||||
|
||||
def test_a_deeply_nested_seen_marker_reads_as_nothing_seen(tmp_path):
|
||||
"""A JSON array nested past the parser's recursion limit raises
|
||||
RecursionError, which is not a ValueError: a 100 KB file of `[` planted as
|
||||
`.seen` escaped the never-raises read and 500'd every review of the booth.
|
||||
It reads as nothing seen, and the next look rewrites it."""
|
||||
from booth.items import read_seen
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG})
|
||||
(b / ".seen").write_text("[" * 100_000)
|
||||
assert read_seen(b) == set()
|
||||
assert _client(tmp_path).get("/b/g/view?f=a.png").status_code == 200
|
||||
assert read_seen(b) == {"a.png"}
|
||||
|
||||
def test_the_content_clock_reads_the_booth_not_what_its_links_point_at(tmp_path):
|
||||
"""Nyx (groa, regin): stat() followed a symlink, so a link to a busy file
|
||||
outside the booth made the booth read as newly delivered on every load; and
|
||||
one unreadable entry (a symlink loop) made the whole booth read as landed
|
||||
NOW, forever. The link's own mtime counts; an unreadable entry is skipped."""
|
||||
import os
|
||||
t0 = time.time() - 10_000 # in the PAST: a future stamp outranks every real write and hides the bug
|
||||
outside = tmp_path / "busy.log"
|
||||
outside.write_text("x")
|
||||
b = _booth(tmp_path, "g", {"a.png": PNG})
|
||||
(b / "linked.png").symlink_to(outside)
|
||||
(b / "loop.png").symlink_to(b / "loop.png")
|
||||
for p in (b / "a.png", b / "linked.png", b / "loop.png"):
|
||||
os.utime(p, (t0, t0), follow_symlinks=False)
|
||||
_at(b, t0)
|
||||
c = _client(tmp_path)
|
||||
c.get("/b/g/") # look at it
|
||||
os.utime(outside, None) # the outside file keeps moving
|
||||
assert _desk(c.get("/").text).get("rest") == ["g"]
|
||||
|
||||
|
||||
def test_a_nul_in_the_review_path_is_a_404_not_a_500(tmp_path):
|
||||
"""Nyx (groa, seat-probed): Path raises ValueError on an embedded NUL, and
|
||||
the route caught only OSError. Every other hostile `f` is a 404."""
|
||||
_booth(tmp_path, "g", {"a.png": PNG})
|
||||
assert _client(tmp_path).get("/b/g/view?f=a%00.png").status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.parametrize("q", ["inf", "1e999", "nan", "-inf"])
|
||||
def test_a_non_finite_q_is_malformed(q):
|
||||
"""Nyx (regin): float() parses inf and 1e999, and inf > 0 — a malformed
|
||||
header slipped through to the 204. Non-finite q is malformed: False."""
|
||||
from booth.app import wants_json
|
||||
assert wants_json(f"application/json;q={q}") is False
|
||||
|
||||
|
||||
def test_a_sound_only_booth_can_open_the_review(tmp_path):
|
||||
"""Nyx (groa): only the image tile linked to view?f=, so a booth of tracks
|
||||
had no way into the review, the tape or `.seen`. Every media tile links in
|
||||
(and Enter on the grid cursor follows that link)."""
|
||||
_booth(tmp_path, "g", {"a.mp3": b"ID3", "b.webm": b"\x1aE"})
|
||||
body = _client(tmp_path).get("/b/g/").text
|
||||
for rel in ("a.mp3", "b.webm"):
|
||||
fig = re.search(r'<figure[^>]*data-item="%s".*?</figure>' % re.escape(rel), body, re.S).group(0)
|
||||
assert f'href="view?f={rel}"' in fig, rel
|
||||
|
||||
|
||||
def test_the_desk_never_makes_a_non_web_url_clickable(tmp_path):
|
||||
"""Nyx (kimi): bookmark and bench URLs are agent-written and land in href.
|
||||
Autoescape does nothing about a `javascript:` scheme. The Desk links only
|
||||
http(s) and shows anything else as plain text."""
|
||||
rows = (_link("evil", "javascript:alert`1`")
|
||||
+ _link("fine", "https://example.test/"))
|
||||
board = _booth(tmp_path, "links", {"links.md": rows.encode()})
|
||||
(board / ".forever").write_bytes(b"")
|
||||
body = _client(tmp_path).get("/").text
|
||||
panel = re.search(r'data-panel="bookmarks".*?</section>', body, re.S).group(0)
|
||||
assert 'href="javascript:' not in panel
|
||||
assert 'href="https://example.test/"' in panel and "evil" in panel
|
||||
|
||||
Reference in New Issue
Block a user