Files
booth/tests/mutations/blur_storage.toml
T
vh 4cfbce5109 fix(blur): .blurred round-trips any rel, and one writer serves both surfaces
The heid bug-hunt on r2b merge 1 found the /blur route stripping `f` before
writing, so the form for " a.png" blurred its neighbour "a.png". The route was
only half of it: `.blurred` was one stripped rel per line, so no writer could
store a rel with a leading space or a newline, whatever the route did.
Operator-ruled 2026-09-23 ("fix the blur").

- booth/blur.py (new, stdlib-only): read_blurred / set_blurred / BLUR_FILE.
  `.blurred` is now a JSON array in sorted order, the `.seen` shape: opened
  O_NOFOLLOW | O_NONBLOCK with an S_ISREG check and a 1 MiB cap, so a planted
  symlink is refused and a FIFO can no longer hang every Desk render (the old
  read_text() blocked on one). Writes go through mkstemp + os.replace. The
  legacy line format is still READ, so the 6 live line-format files keep their
  blur until their next write upgrades them. Measured before the change: 42
  live rels, none with edge whitespace, so the defect had no live victims.
- The route no longer strips `f`.
- scripts/booth `blur`/`unblur` go through booth.blur.set_blurred instead of
  their own grep/printf line writer. Two writers of one format is how the
  formats drift, and after this change the shell writer would have appended a
  line to a JSON array. Every path is checked before anything is written.
- Item.blurred_self (appended to the record): the item's own blur, resolved in
  booth_items from the same read as `blurred`. It replaces build_gallery's
  second read_blurred, which a write between the two reads could split
  (invariant 3). app.py no longer reads blur state at all, and a test asserts
  it.

Names stay importable from booth.app and booth.items (invariant 4). blur joins
test_stdlib_only. test_cli's per-item-survives test now reads through the reader
rather than asserting the old byte format. The r2b contract and its mutation
row follow blurred_self onto the record. tests/mutations/blur_storage.toml
proves 12 falsifiers by running the change each forbids.

Not in this change, and still ours: the "off"-means-ON idiom drift between
/blur, /blurbooth and /flag (forms only ever send 0/1), and the CLI's
`.blurbooth` touch following a symlink where the service no longer does.
2026-09-23 22:05:18 -07:00

123 lines
4.2 KiB
TOML

# Per-item blur storage: `.blurred` round-trips any rel, whoever writes it.
# The fix for the wrong-item write the heid bug-hunt found through r2b merge 1
# (a stripped rel blurred its neighbour), operator-ruled 2026-09-23. Every row
# is a change tests/test_blur.py claims to forbid.
#
# NOT here, on purpose: the S_ISREG guard in read_blurred. With O_NONBLOCK a
# FIFO opens and reads as EOF, a symlink is already refused by O_NOFOLLOW, and a
# device node needs root to plant, so no test here can see that guard go. It
# stays as the `.seen` shape, and it is not claimed as a proven falsifier.
unit = "blur storage round-trip"
[[mutation]]
label = "the writer strips the rel (the old line format's loss)"
file = "booth/blur.py"
test = "tests/test_blur.py::test_a_leading_space_rel_round_trips"
old = '''
current.add(rel)'''
new = '''
current.add(rel.strip())'''
[[mutation]]
label = "the route strips `f` before writing (the reported wrong-item write)"
file = "booth/app.py"
test = "tests/test_blur.py::test_the_blur_route_blurs_exactly_the_item_it_names"
old = '''
rel = f.lstrip("/")'''
new = '''
rel = f.strip().lstrip("/")'''
[[mutation]]
label = "a JSON-only reader: every live line-format file un-blurs on deploy"
file = "booth/blur.py"
test = "tests/test_blur.py::test_the_legacy_line_format_still_reads"
old = '''
return {ln.strip() for ln in text.splitlines() if ln.strip()}'''
new = '''
return set()'''
[[mutation]]
label = "a legacy file that is not JSON reads as nothing instead of falling back"
file = "booth/blur.py"
test = "tests/test_blur.py::test_a_legacy_rel_that_starts_with_a_bracket_still_reads"
old = '''
data = None'''
new = '''
return set()'''
[[mutation]]
label = "a FIFO blocks the read (no O_NONBLOCK)"
file = "booth/blur.py"
test = "tests/test_blur.py::test_a_fifo_blur_file_does_not_block_the_read"
old = '''
fd = os.open(booth / BLUR_FILE, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)'''
new = '''
fd = os.open(booth / BLUR_FILE, os.O_RDONLY | os.O_NOFOLLOW)'''
[[mutation]]
label = "the read follows a planted symlink (no O_NOFOLLOW)"
file = "booth/blur.py"
test = "tests/test_blur.py::test_a_symlinked_blur_file_is_not_followed_on_read"
old = '''
fd = os.open(booth / BLUR_FILE, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)'''
new = '''
fd = os.open(booth / BLUR_FILE, os.O_RDONLY | os.O_NONBLOCK)'''
[[mutation]]
label = "the write goes through a planted symlink instead of replacing it"
file = "booth/blur.py"
test = "tests/test_blur.py::test_a_write_replaces_a_planted_symlink_rather_than_writing_through_it"
old = '''
os.replace(tmp, path)'''
new = '''
path.write_bytes(Path(tmp).read_bytes()); os.unlink(tmp)'''
[[mutation]]
label = "the stored order is not the stated one (invariant 6)"
file = "booth/blur.py"
test = "tests/test_blur.py::test_the_file_is_a_json_array_in_sorted_order"
old = '''
body = json.dumps(sorted(current), ensure_ascii=False)'''
new = '''
body = json.dumps(sorted(current, reverse=True), ensure_ascii=False)'''
[[mutation]]
label = "the CLI ignores the verb: `unblur` blurs"
file = "scripts/booth"
test = "tests/test_blur.py::test_the_cli_writes_the_format_the_service_reads"
old = '''
on = sys.argv[1] == "blur"'''
new = '''
on = True'''
[[mutation]]
label = "the CLI writes past a refused '..' path"
file = "scripts/booth"
test = "tests/test_blur.py::test_the_cli_still_refuses_a_dotdot_path"
old = '''
*..*) echo "refusing path with '..': $item" >&2; exit 2 ;;'''
new = '''
*..*) echo "refusing path with '..': $item" >&2 ;;'''
[[mutation]]
label = "the item's own blur is the composed one (booth fog leaks into it)"
file = "booth/items.py"
test = "tests/test_blur.py::test_the_item_record_carries_its_own_blur_apart_from_the_booths"
old = '''
blurred_self=rel in blurred,'''
new = '''
blurred_self=rel in blurred or booth_blur,'''
[[mutation]]
label = "app.py reads the blur file a second time (invariant 3)"
file = "booth/app.py"
test = "tests/test_blur.py::test_app_py_never_reads_the_blur_file_itself"
old = '''
out = []
for it in booth_items(child):'''
new = '''
out = []
read_blurred(child)
for it in booth_items(child):'''