fix(u6): the booth check fails closed with a reason, and a dead write leaves no scratch

Two more from the in-session adversarial pass.

`booth link`'s new booth-URL check shells out to booth/links.py. When that
import cannot run, the command substitution under `set -e` aborted the script
with a bare ModuleNotFoundError traceback: the right DIRECTION (no row was
appended — a guard that fails open is not a guard) reached by accident, and
unactionable when it fires. Handled explicitly now: exit 3, and a message
naming what the check needs. The fail-closed direction is stated rather than
inherited from shell semantics, and a test pins it — the defeating change in
either direction goes red.

_write_all's scratch file was stranded beside the registry if the write died
between create and replace. Cleaned up on every exit path. The prior registry
was never at risk either way: os.replace is the only thing that publishes.

Also pins normalization idempotence, which `bench state <id|url>` and
`bench rm <id|url>` both rely on: they normalize whatever they are handed, so
an id that did not normalize to itself would miss the row it names.
This commit is contained in:
vh
2026-09-22 13:33:59 -07:00
parent 8c7f2127eb
commit 0a2bb1d26c
4 changed files with 84 additions and 3 deletions
+33
View File
@@ -579,3 +579,36 @@ def test_a_control_character_is_not_an_addressable_booth(encoded):
control-character clause — the `%2e%2e` and `%2f` rows above stay green
under it, so this needs its own."""
assert booth_target(f"http://h:8090/b/{encoded}/") is None
def test_normalization_is_idempotent(tmp_path):
"""LOAD-BEARING for `bench state <id|url>` and `bench rm <id|url>`: both
normalize whatever they are handed, so an id must normalize to itself or
addressing a bench by the id the registry stores would miss it. Defeating
change: any rule that rewrites an already-normalized form."""
for u in (GITEA_EIGHT + TALK_FIVE + [
"http://x.test/", "http://x.test:8080/p/", "https://x.test/?a=1",
"HTTP://X.Test:80/Some%20Path/?b=2&a=1#frag",
]):
once = normalize_bench_url(u)
assert normalize_bench_url(once) == once, u
def test_a_failed_write_leaves_no_scratch_file(tmp_path, monkeypatch):
"""The temp file is named per-pid so two writers cannot share it, but a
write that dies between create and replace would strand it beside the
registry forever. Defeating change: dropping the cleanup."""
import booth.benches as B
upsert_bench(tmp_path, "http://a.test/", "one", "o")
real = B.os.replace
def boom(src, dst):
raise OSError("disk full")
monkeypatch.setattr(B.os, "replace", boom)
with pytest.raises(OSError):
upsert_bench(tmp_path, "http://b.test/", "two", "o")
monkeypatch.setattr(B.os, "replace", real)
strays = [p.name for p in tmp_path.iterdir() if ".tmp" in p.name]
assert not strays, strays
# and the prior registry is intact — a failed write destroys nothing
assert [b.name for b in read_benches(tmp_path)[0]] == ["one"]