fix(u6): fold the cold code-review panel — four-arm convergence on three surface clauses

/heid-code-review panel 01M35CK8YKEKMV7T15JXEF6A8N, verdict NOT drift-zero.
Three findings arrived from all four arms independently, and they share a
shape: a contract clause written as prose and never converted into an
assertion. That is the lens working.

- The panel dropped the added date the contract promised to show.
- `bench ls` printed no ids, and the URL it printed was truncated to 52 columns
  so the line was not pasteable into `bench state|rm`. The test's docstring
  claimed it printed ids and asserted nothing of the kind.
- `bench import` printed the description instead of the raw URL beside each
  normalized id, hiding the collapse the clause exists to expose.
- An IPv6 literal lost its brackets: http://[::1]:8080/a normalized to
  http://::1:8080/a, a broken identity that no re-post can match. Bracketed
  literals are re-wrapped; an unbracketed one is refused rather than guessed.
- A deeply-nested JSON RecursionError escaped read_benches' except pair. The
  byte cap does not help -- 200k open brackets is 200 KB.
- An empty board hid the whole benches panel, registration form included.
- The link refusal classified by captured-text emptiness, which bash can erase;
  it now answers with a B:/N sentinel so no name reads as "not a booth".

INV-4's tie-break falsifier could not fail: _write_all serializes with
sort_keys=True, so both insertion orders came back already id-sorted and
removing the tie-break left the test green. It now calls order_benches
directly. Same class as the five vacuous U4 falsifiers, found by a cold reader
rather than by us.

Also from the arms' per-invariant vacuity pass: INV-6 had no vector pinning a
non-default port as part of the identity; INV-3 asserted only that links/ was
absent; INV-8's hashed sequence omitted a read verb; INV-9's AST walk is
defeated by a string import. All closed.

Contract amended where the code was right: `updated` means last mutation, the
id cap is write-only because the id is the locator controls post back, INV-8's
file list includes the lock sidecar it always mandated. Every line number is
out of the prose -- the panel found two already stale.

565 -> 593 tests. Nothing declined.
This commit is contained in:
vh
2026-09-22 13:50:40 -07:00
parent 0a2bb1d26c
commit 8a7af3eb08
8 changed files with 297 additions and 37 deletions
+8
View File
@@ -932,6 +932,14 @@ def create_app(
# every other booth, so this pair is read only when it renders.
# `read_benches` never raises; a damaged registry costs its own
# panel and says so, which is the v0.2.2 lesson.
# `is_board` is PAGE IDENTITY, not page content. Gating the
# panel on `board or benches` hid it — and its registration
# form — exactly when the board was empty and the registry
# absent, which is the state a new deployment starts in and the
# one where "no benches registered yet" is most worth saying.
# A panel that disappears when it has nothing to show is the
# same defect as a damaged panel rendering as an absent one.
"is_board": (booth / LINKS_FILE).is_file(),
**dict(zip(("benches", "benches_error"),
read_benches(data_dir) if (booth / LINKS_FILE).is_file()
else ([], None))),
+14
View File
@@ -136,6 +136,14 @@ def normalize_bench_url(url: str) -> str:
if not host:
raise ValueError("that URL has no host")
# RE-WRAP A BRACKETED IPv6 LITERAL. `urlsplit().hostname` strips the
# brackets, and rebuilding the netloc from it produces `http://::1:8080/a`
# — not a different spelling of the same URL but a BROKEN one, so a re-post
# never matches the row the operator thinks they are updating. The bracket
# is part of the authority's syntax, not decoration. Detected by the colon,
# which cannot appear in a hostname or an IPv4 literal.
if ":" in host:
host = f"[{host}]"
default = {"http": 80, "https": 443}[scheme]
netloc = host if port in (None, default) else f"{host}:{port}"
# A bare "/" is the same resource as no path at all; a trailing slash on a
@@ -240,6 +248,12 @@ def read_benches(root: Path) -> tuple[list[Bench], str | None]:
return [], str(exc)
except OSError as exc:
return [], f"registry could not be read: {exc}"
except RecursionError:
# Deeply nested JSON (`[[[[...`) blows the stack inside json.loads, and
# RecursionError is neither ValueError nor OSError — so it escaped the
# pair above and 500'd the page this function exists to protect. The
# byte cap does not help: 200k open brackets is 200 KB.
return [], "registry is nested too deeply to parse"
def _write_all(root: Path, benches: dict[str, Bench]) -> None:
+1
View File
@@ -511,6 +511,7 @@
.bench-row.is-promoted .bench-state{background:rgba(130,170,240,.18)}
.bench-main{flex:1;min-width:0}
.bench-url{font-size:.78em;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bench-meta{display:flex;flex-direction:column;align-items:flex-end;font-size:.75em;opacity:.6}
.bench-acts{display:flex;gap:.3rem}
.bench-to,.bench-rm{font-size:.75em;padding:.15rem .4rem;cursor:pointer}
.bench-add{display:flex;gap:.4rem;padding:.5rem .7rem;border-top:1px solid var(--line,#2a2a2a)}
+5 -1
View File
@@ -66,7 +66,7 @@
{% else %}
<h1>{{ name }}</h1>
{% endif %}
<span class="sub">{% if uploaded %}<span class="badge">⬆ pickup</span> {% endif %}{% if board or benches or benches_error %}
<span class="sub">{% if uploaded %}<span class="badge">⬆ pickup</span> {% endif %}{% if is_board %}
{# THE BENCH REGISTRY. A bench is a running thing — jackdaw's current bench,
talk's current bench, the things that get promoted to Homepage when they
are fully deployed. NOT a booth (a booth announces itself and is swept) and
@@ -102,6 +102,10 @@
</div>
<div class="bench-meta">
{% if b.owner %}<span class="bench-who">{{ b.owner }}</span>{% endif %}
{# The date it was REGISTERED, not the date it was last touched: `added`
survives re-registration and `updated` does not, so `added` is the
one that answers "how long has this been around". #}
{% if b.added %}<span class="bench-when">{{ b.added[:10] }}</span>{% endif %}
</div>
<form class="bench-acts" method="post" action="/b/{{ name_url }}/bench-state">
<input type="hidden" name="bench" value="{{ b.id }}">
+76 -14
View File
@@ -4,7 +4,7 @@ module: "booth.benches"
purpose: "A bench is a running thing, registered -- not a booth, and not a bookmark. The standing link board absorbed all three jobs because only one of them had a surface, and it now carries 221 rows of which 178 (80%) are booth announcements and 156 (71% of the whole board) point at booths that were swept. U5 gave the booth announcement a home; this unit gives the RUNNING SERVICE one, and closes the loop by refusing the one shape that now has somewhere better to go. Identity is the normalized URL, so re-announcing a bench UPDATES its row instead of appending a fifth -- `talk` is on the board five times and Peedlar's root three. Nothing on the board is deleted by this unit: the dead rows are MARKED so the operator can see and remove them with the bulk control that already exists."
depends_on:
- "booth.links (`booth_target` is DEFINED here and consumed there -- see INV-2. The board's existing parse/remove/pin machinery is untouched: rows keep their content-hash identity, `links.md` stays an O_APPEND multi-writer log, and no row is rewritten by anything this unit adds.)"
- "booth.app (the dead-row marker needs a booth-exists predicate. IT CANNOT USE `resolve_booth`: that is a CLOSURE inside `create_app` (app.py:798), not importable, and it RAISES HTTPException(404) -- calling it per row would turn one swept booth into a 404 for the whole board page, which is the opposite of the marker's purpose. The marker gets its own non-raising predicate carrying the SAME name-safety rules (no leading dot, no separator, no `..`) and returning False where `resolve_booth` raises. A row is dead when its target directory is absent, not when its target is nearly expired -- no new lifetime arithmetic. Verified against the real function, not assumed: seam review SR-2.)"
- "booth.app (the dead-row marker needs a booth-exists predicate. IT CANNOT USE `resolve_booth`: that is a CLOSURE inside `create_app`, not importable, and it RAISES HTTPException(404) -- calling it per row would turn one swept booth into a 404 for the whole board page, which is the opposite of the marker's purpose. The marker gets its own non-raising predicate carrying the SAME name-safety rules (no leading dot, no separator, no `..`) and returning False where `resolve_booth` raises. A row is dead when its target directory is absent, not when its target is nearly expired -- no new lifetime arithmetic. Verified against the real function, not assumed: seam review SR-2.)"
language: "python"
complexity: "medium"
estimated_loc: 320
@@ -117,9 +117,23 @@ and got a 404" and is never traced back here.
U5's `created`, and for the same reason: an upsert is the same bench saying
something new about itself, not a new bench.
Caps, applied at the write and again at the read: `name` 120, `owner` 64, `id`
and `url` 2048, `state` one of three. Each is a display budget, not a storage
limit.
**`updated` means the last MUTATION of the record, not the last upsert** —
`set_bench_state` bumps it too. Amended after the cold panel read "most recent
upsert" literally and found the code bumping on a state change: the code is
right (a promotion is a change to the record and "last touched" should say so)
and the earlier wording was narrower than what anyone wants the field to mean.
Caps: `name` 120, `owner` 64, `url` 2048, `state` one of three — applied at the
write and again at the read. Each is a display budget, not a storage limit.
**`id` is capped at the WRITE ONLY, and that asymmetry is deliberate.**
`normalize_bench_url` refuses an input over `URL_MAX`, so nothing this code
writes can exceed it. On the read the id is the dict KEY and it is the locator
every control posts back — `bench state`, `bench rm`, and the panel's remove
button all address by it. Truncating a hand-edited over-long key on read would
produce a row the operator can see and cannot act on, which is strictly worse
than a long one. Amended after the cold panel found the code and the contract
disagreeing here; the code was right.
## Signatures
@@ -234,10 +248,18 @@ A registry that cannot be read renders as a panel carrying its error, never as
an absent panel and never as a 500 — the v0.2.2 lesson, which this repo learned
by returning 500 for `/` and `/healthz` across all 25 booths.
**The panel is gated on PAGE IDENTITY — the booth carries a `links.md` — and
never on content.** A content gate (`board or benches`) hides the panel AND its
registration form exactly when the board is empty and the registry absent,
which is the state a fresh deployment starts in and the one where "no benches
registered yet" is most worth saying. That is the same defect as a damaged
panel rendering as an absent one, one level up. Amended after the cold panel
found the content gate shipped.
## The CLI surface
```
booth bench add <url> <name> register or update; prints created/updated
booth bench add <url> <name> register or update; prints registered/updated
booth bench ls list, in the rendered order, with ids
booth bench state <id|url> <s> live | promoted | retired
booth bench rm <id|url> remove one
@@ -362,7 +384,14 @@ byte.
**INV-8 — nothing this unit ships removes a board row.** The dead marker is a
render-time stamp; `import` without `--apply` writes nothing anywhere; `import`
with `--apply` writes only `.benches.json`.
with `--apply` writes only the registry and its lock sidecar (`.benches.json`,
`.benches.lock`) and never touches `links.md`.
*(Amended: this said "writes only `.benches.json`", which contradicted the
unit's own assumption that every read-modify-write is held under an flock on a
sidecar. The cold panel caught the contract arguing with itself. The
load-bearing half — `links.md` is not touched — is unchanged and is what the
test hashes.)*
*Falsifiable:* the defeating change is `import --apply` "tidying up" the rows it
consumed. The test snapshots `links.md` byte for byte, runs the full unit's CLI
surface against it — refusal, import, import --apply, bench add, bench rm — and
@@ -374,12 +403,12 @@ outside the standard library and nothing from `booth.*`.
which is the natural thing to write, since `booth_target` is the predicate this
unit's CLI branch also needs.
**The existing parametrized `test_stdlib_only` (tests/test_marks.py:280) DOES
**The existing parametrized `test_stdlib_only` in tests/test_marks.py DOES
NOT CATCH THAT, and an earlier draft of this contract claimed it did.** Its
failure set is `{r for r in roots if r != "booth" and r not in
sys.stdlib_module_names}` — it exempts `booth` explicitly, so a sibling import
passes it clean. The sibling-free clause exists only in the stricter copy at
tests/test_manifest.py:209. Adding `benches` to the parametrized list therefore
passes it clean. The sibling-free clause exists only in the stricter copy in
tests/test_manifest.py. Adding `benches` to the parametrized list therefore
buys stdlib-only and NOT sibling-free. So: `benches` joins that list AND
`tests/test_benches.py` carries its own stricter copy, mirroring `manifest`'s,
which fails on a `booth` root. Verified by reading the real test — seam review
@@ -395,13 +424,13 @@ this file and are forbidden the siblings it borrows from.
| # | seam | what the real surface said | disposition |
|---|---|---|---|
| **SR-1** | `test_stdlib_only` (tests/test_marks.py:280) | **The contract was wrong.** It claimed the parametrized test "already carries" the sibling-free clause. It does not — its failure set is `{r for r in roots if r != "booth" and ...}`, which exempts `booth` on purpose. Only tests/test_manifest.py:209 has the strict copy. | **Fixed.** INV-9 now requires both: the parametrize entry AND a stricter copy in `tests/test_benches.py`. Without this the unit would have shipped with its own INV-9 untested. |
| **SR-2** | `resolve_booth` (app.py:798) | **The contract invited an outage.** It named `resolve_booth` as the existence check for the dead marker. That function is a closure inside `create_app` (not importable) and **raises HTTPException(404)** — called per row, one swept booth would 404 the entire board page. It also calls `.resolve()`, a syscall per row, 178 of them on this board. | **Fixed.** `depends_on` now forbids it explicitly and specifies an own non-raising predicate with the same name-safety rules. Cost stated below. |
| **SR-1** | `test_stdlib_only` (tests/test_marks.py) | **The contract was wrong.** It claimed the parametrized test "already carries" the sibling-free clause. It does not — its failure set is `{r for r in roots if r != "booth" and ...}`, which exempts `booth` on purpose. Only tests/test_manifest.py:209 has the strict copy. | **Fixed.** INV-9 now requires both: the parametrize entry AND a stricter copy in `tests/test_benches.py`. Without this the unit would have shipped with its own INV-9 untested. |
| **SR-2** | `resolve_booth` (booth/app.py) | **The contract invited an outage.** It named `resolve_booth` as the existence check for the dead marker. That function is a closure inside `create_app` (not importable) and **raises HTTPException(404)** — called per row, one swept booth would 404 the entire board page. It also calls `.resolve()`, a syscall per row, 178 of them on this board. | **Fixed.** `depends_on` now forbids it explicitly and specifies an own non-raising predicate with the same name-safety rules. Cost stated below. |
| **SR-7** | `quote(name, safe="")` (app.py, booth link emission) | **The contract was silent on encoding.** Booth links are emitted percent-encoded. A `booth_target` comparing the raw path segment to a directory name marks every encoded-name booth permanently dead and echoes the encoded form back in the refusal. | **Fixed.** `booth_target` decodes, and applies `resolve_booth`'s own addressability rules so the two cannot disagree. |
| **SR-6** | `scripts/booth` dispatch (flat `case "$cmd"`, 13 single-word verbs) | Not a defect — a gap. **`bench add` would be the first two-word verb in this script.** Nothing about the existing dispatch anticipates one, and `booth bench` with no sub-verb must not fall through into the generic usage in a way that hides which word was wrong. | **Recorded.** A nested `case` under `bench)`, and a bare `bench` prints the bench verbs specifically. Named so the implementer does not invent a third pattern. |
| **SR-3** | `data_dir` (app.py:704) vs `DATA` (scripts/booth:120) | The service resolves and expands its root in `create_app`; the CLI derives it from `$BOOTH_DATA_DIR`. Two independent derivations of one path. | **No change.** This is already true of `links.md`, `.marks.json` and `.booth.json` — pre-existing and out of this unit's scope. Recorded so it is a known property rather than a discovery. |
| **SR-4** | `list_booths` (app.py:418) | **Confirmed, not assumed.** `if not child.is_dir() or child.name.startswith("."): continue` — `.benches.json` fails both guards. The index cannot see the registry. | **Verified.** The assumption stands on read code. |
| **SR-5** | `sweep_once` (app.py:386) | **Confirmed, not assumed — and this was the dangerous one.** The sweeper iterates the data root and could in principle delete the registry. It cannot: the same `is_dir()` + leading-dot pair guards it, and `shutil.rmtree` is reached only past both. | **Verified.** Had either guard been absent this unit would have shipped a design that eats its own registry on the first tick. |
| **SR-3** | `data_dir` (booth/app.py) vs `DATA` (scripts/booth) | The service resolves and expands its root in `create_app`; the CLI derives it from `$BOOTH_DATA_DIR`. Two independent derivations of one path. | **No change.** This is already true of `links.md`, `.marks.json` and `.booth.json` — pre-existing and out of this unit's scope. Recorded so it is a known property rather than a discovery. |
| **SR-4** | `list_booths` (booth/app.py) | **Confirmed, not assumed.** `if not child.is_dir() or child.name.startswith("."): continue` — `.benches.json` fails both guards. The index cannot see the registry. | **Verified.** The assumption stands on read code. |
| **SR-5** | `sweep_once` (booth/app.py) | **Confirmed, not assumed — and this was the dangerous one.** The sweeper iterates the data root and could in principle delete the registry. It cannot: the same `is_dir()` + leading-dot pair guards it, and `shutil.rmtree` is reached only past both. | **Verified.** Had either guard been absent this unit would have shipped a design that eats its own registry on the first tick. |
**The per-render cost, stated because SR-2 surfaced it.** The dead marker runs
once per board row: 221 rows today, 178 of which parse as booth links and cost
@@ -411,3 +440,36 @@ per booth on the index, so it is not a new order of magnitude. It is bounded by
the row count, it touches no network, and it is confined to the ONE booth that
carries a `links.md`. If the board ever grows past a few thousand rows this
becomes worth caching; at 221 it would be premature.
## Code review — what the cold panel found
`/heid-code-review` panel `01M35CK8YKEKMV7T15JXEF6A8N`, four arms, verdict
**NOT drift-zero**. Folded in full. Three findings were independently reported
by **all four arms**, which is the signature of a contract clause that was
written as prose and never converted into an assertion.
| # | finding | arms | disposition |
|---|---|---|---|
| **A** | **The panel dropped the added date.** *What renders* says "the date it was added"; `b.added` appeared nowhere in the template and no test asked for it. | 4/4 | **Fixed** — rendered, and pinned by a test. |
| **B** | **`bench ls` printed no ids**, and the truncated URL it printed was not pasteable into `bench state\|rm`. Worse: the test's own docstring *claimed* it printed ids while asserting nothing — a claim standing in for evidence, which is how the drift would have survived CI. | 4/4 | **Fixed** — the id prints whole and last; the test now round-trips what `ls` prints back through `bench state`. |
| **C** | **`bench import` printed the description, not the raw URL**, beside each id — hiding the five-rows-of-talk collapse the clause exists to expose. | 4/4 | **Fixed** — raw URL beside the id, description demoted to a continuation line. |
| **D** | **An IPv6 literal lost its brackets.** `http://[::1]:8080/a` normalized to `http://::1:8080/a` — not another spelling but a BROKEN identity, so a re-post never matches the row. | 3/4 | **Fixed** — bracketed literals are re-wrapped; an *unbracketed* one is refused with a reason rather than guessed at. |
| **H** | **INV-4's tie-break falsifier could not fail.** `_write_all` serializes with `sort_keys=True`, so both insertion orders came back off disk already id-sorted and removing the tie-break left the test green. | 1/4 | **Fixed** — the test now calls `order_benches` directly with records that tie on both prior keys. A vacuous falsifier of exactly the class `persistent-memory.d/2026-09-22-vacuous-falsifiers.md` names, found by a cold reader and not by us. |
| **I** | **An empty board hid the whole panel**, registration form included — the state a fresh deployment starts in. | 1/4 | **Fixed** — gated on page identity. |
| **J** | **The `booth link` refusal could fail OPEN** on a name bash's `$()` erases, because it classified by captured-text emptiness. | 1/4 | **Fixed** — the predicate answers with a `B:`/`N` sentinel, so no name can be mistaken for "not a booth". |
| **K** | A FIFO at the registry path blocked in `open()`; a deeply-nested JSON `RecursionError` escaped the `except (ValueError, OSError)` pair. | 1/4 | **The FIFO half was already fixed** by our own pass before the reply landed. **The RecursionError half was not** — 200k open brackets is 200 KB, well inside the byte cap, and it 500'd the page the function exists to protect. Fixed. |
| **E** | The read does not apply the `id` cap the contract promised. | 3/4 | **Contract amended, code kept.** The id is the locator every control posts back; truncating a hand-edited over-long key would make a row visible and unactionable. |
| **F, G** | INV-5's render test covered 5 of 6 cases and asserted only status 200; INV-2's URL table never ran through the dead-marker render. | 4/4, 3/4 | **Both fixed** — the render test now covers oversized, unreadable and FIFO and asserts the error is *visible*; the full table runs through the marker. |
**Also folded from the per-invariant vacuity pass** (the arms' "what would still
pass" section, which is the single most useful thing the panel produced):
INV-6 had no vector asserting a non-default port is part of the identity, so
"always omit the port" passed every row; INV-3 asserted only that `links/` was
absent, so a refusal touching any other sidecar passed; INV-8's hashed sequence
omitted `bench ls`; INV-9's AST walk is defeated by `__import__("booth.links")`.
All four closed.
**Declined:** nothing. **Amended rather than fixed:** E, `updated`'s meaning,
INV-8's file list, the `registered`/`created` wording, and every line number in
this document's prose — the panel found two already stale, which is the whole
argument against putting them in prose at all.
+32 -3
View File
@@ -218,12 +218,20 @@ booth_src() {
# the board's dead-row marker and `bench import` use that same function and a
# second implementation in the shell would classify the host-agnostic and
# percent-encoded cases differently (INV-2).
# Prints `B:<name>` for a booth URL and `N` for anything else.
#
# A SENTINEL, NOT AN EMPTY STRING. Command substitution strips trailing
# newlines, so a predicate that answers with the bare name cannot distinguish
# "not a booth" from "a booth whose name bash just erased" — and the guard
# then fails OPEN on that edge, which is the one direction a guard must never
# fail. The prefix makes the answer unambiguous whatever the name contains.
booth_target_of() {
BOOTH_SRC="$(booth_src)" BOOTH_Q="$1" python3 -c '
import os, sys
sys.path.insert(0, os.environ["BOOTH_SRC"])
from booth.links import booth_target # stdlib only — no venv needed
sys.stdout.write(booth_target(os.environ["BOOTH_Q"]) or "")
name = booth_target(os.environ["BOOTH_Q"])
sys.stdout.write("N" if name is None else "B:" + name)
'
}
@@ -344,6 +352,13 @@ case "$cmd" in
} >&2
exit 3
fi
case "$refused_name" in
N) refused_name="" ;;
B:*) refused_name="${refused_name#B:}" ;;
*)
echo "booth link: the booth check answered something unrecognised; nothing was posted." >&2
exit 3 ;;
esac
if [ -n "$refused_name" ]; then
{
echo "booth link: that is a booth, and a booth announces itself now."
@@ -463,7 +478,12 @@ def die(msg, code=2):
def row(b):
# ONE LINE PER BENCH, in the rendered order — state first, so a retired
# bench sinks, then name, then id as a total tie-break (INV-4).
return "%-9s %-28s %-52s %s" % (b.state, b.name[:28], b.url[:52], b.owner)
# THE ID IS PRINTED WHOLE AND UNTRUNCATED, because it is the locator
# `bench state` and `bench rm` take: a truncated one is not an id, it is a
# string that looks like one and silently addresses nothing. The name and
# the added date are the truncatable columns.
return "%-9s %-10s %-16s %-24s %s" % (
b.state, b.added[:10], b.owner[:16], b.name[:24], b.id)
if sub == "add":
if len(argv) < 2: die("bench add <url> <name>")
@@ -478,6 +498,9 @@ elif sub == "ls":
die("the registry could not be read: %s" % err, 3)
if not benches:
print("no benches registered yet")
else:
print("%-9s %-10s %-16s %-24s %s"
% ("STATE", "ADDED", "OWNER", "NAME", "ID (pass to state|rm)"))
for b in benches:
print(row(b))
elif sub in ("state", "rm"):
@@ -518,7 +541,13 @@ elif sub == "import":
print("CANDIDATES — would be registered (%d rows, %d distinct):"
% (len(candidates), len({i for i, _ in candidates})))
for i, e in candidates:
print(" %-52s %s" % (i, e["desc"][:60]))
# THE NORMALIZED ID BESIDE THE RAW URL, which is the whole point of the
# proposal: five rows of `talk` collapsing to one is only visible if you
# can see which five raw URLs produced the one id. The description is
# the thing to drop here, not the URL.
print(" %-52s %s" % (i, e["url"]))
if e["desc"]:
print(" %-52s %s" % ("", e["desc"][:70]))
print()
print("REFUSED — normalization said no (%d):" % len(refused))
for e, why in refused:
+135 -18
View File
@@ -97,6 +97,8 @@ def test_query_is_part_of_the_identity(tmp_path):
("https://x.test:443/p", "https://x.test/p"),
("http://x.test/p#frag", "http://x.test/p"), # fragment dropped
(" http://x.test/p ", "http://x.test/p"), # whitespace
("http://[::1]:80/a", "http://[::1]/a"), # default port, bracketed
("http://[::1]/A", "http://[::1]/A"), # bracket round-trips
])
def test_these_pairs_are_one_bench(a, b):
"""INV-6. Each pair is the SAME resource reached two ways."""
@@ -108,6 +110,11 @@ def test_these_pairs_are_one_bench(a, b):
("http://x.test/p", "http://x.test/P"), # path case
("http://x.test/?a=1&b=2", "http://x.test/?b=2&a=1"), # query order is opaque
("http://x.test:8092/", "https://x.test:8092/"), # scheme
# A NON-DEFAULT PORT IS PART OF THE IDENTITY. Without this vector, "always
# omit the port" passes every other row in this file — caught by the cold
# panel's per-invariant "what would still pass" pass, not by us.
("http://x.test:8092/p", "http://x.test/p"),
("https://x.test:8443/p", "https://x.test/p"),
])
def test_these_pairs_are_two_benches(a, b):
"""INV-6, the other direction. Each pair MAY be two different resources, and
@@ -150,18 +157,32 @@ def test_the_stored_url_is_the_raw_string(tmp_path):
# ---- INV-4: the rendered order is TOTAL and stated --------------------------
def test_same_name_benches_do_not_swap(tmp_path):
"""INV-4. Defeating change: dropping the `id` tie-break. Two benches with
the SAME name, registered in both orders, must render identically — a test
over distinct names passes with no tie-break at all."""
def build(order):
root = tmp_path / f"r{order}"
root.mkdir()
for u in (["http://a.test/", "http://b.test/"] if order else
["http://b.test/", "http://a.test/"]):
upsert_bench(root, u, "same name", "o")
return [b.id for b in read_benches(root)[0]]
assert build(0) == build(1)
def test_same_name_benches_do_not_swap():
"""INV-4. Defeating change: dropping the `id` tie-break.
THIS TEST USED TO GO THROUGH THE REGISTRY AND COULD NOT FAIL. `_write_all`
serializes with `sort_keys=True`, so whatever order two benches were
inserted in, they came back off disk already id-sorted — and removing the
tie-break from `order_benches` left it green. A vacuous falsifier of
exactly the shape persistent-memory.d/2026-09-22-vacuous-falsifiers.md
describes: it asserted the outcome the author had in mind rather than the
discriminator the invariant names. Caught by the cold panel (hulda, solo),
not by us.
So it calls `order_benches` DIRECTLY, with records that tie on both prior
keys, presented in both orders. Nothing upstream can pre-sort them.
"""
def recs(order):
pair = [
Bench(id="http://a.test/", url="http://a.test/", name="same name",
owner="o", state="live", added="", updated=""),
Bench(id="http://b.test/", url="http://b.test/", name="same name",
owner="o", state="live", added="", updated=""),
]
return pair if order else list(reversed(pair))
assert [b.id for b in order_benches(recs(0))] == \
[b.id for b in order_benches(recs(1))]
assert [b.id for b in order_benches(recs(1))] == ["http://a.test/", "http://b.test/"]
def test_state_ranks_before_name(tmp_path):
@@ -276,6 +297,14 @@ def test_benches_is_stdlib_only_and_imports_no_sibling():
roots.add("booth" if node.level else (node.module or "").split(".")[0])
outside = {r for r in roots if r and r not in sys.stdlib_module_names}
assert not outside, f"booth/benches.py imports outside the stdlib (booth.* included): {sorted(outside)}"
# A STRING IMPORT IS INVISIBLE TO THE WALK ABOVE. `__import__("booth.links")`
# or `importlib.import_module(...)` inside a function defeats it entirely,
# and that is the exact shape someone reaches for when a sibling import is
# refused by review. Caught by the cold panel's per-invariant vacuity pass.
called = {n.func.id for n in ast.walk(tree)
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)}
assert "__import__" not in called, "benches.py imports by string, defeating the AST walk"
assert "importlib" not in roots, "benches.py can import anything at runtime via importlib"
# ---- INV-2: ONE predicate decides what a booth URL is -----------------------
@@ -485,6 +514,9 @@ def test_the_anchor_href_is_the_raw_url_not_the_id(tmp_path):
("[]", "wrong top-level shape"),
('{"http://a/": {"name": [], "owner": "o", "state": "live"}}', "wrong-typed field"),
('{"http://a/": {"name": "n", "owner": "o", "state": "invented"}}', "unknown state"),
("OVERSIZED", "over the size cap"),
("UNREADABLE", "chmod 000"),
("FIFO", "a named pipe"),
])
def test_a_damaged_registry_costs_its_panel_and_never_the_page(tmp_path, payload, label):
"""INV-5, at the render. The v0.2.2 lesson: a poisoned sidecar returned 500
@@ -492,12 +524,34 @@ def test_a_damaged_registry_costs_its_panel_and_never_the_page(tmp_path, payload
the shape currently 500ing the gallery elsewhere in this service, so it is
the one that matters most."""
_board(tmp_path, ROW_REF)
if payload is not None:
(tmp_path / BENCHES_FILE).write_text(payload)
c = _client(tmp_path)
assert c.get("/b/links/").status_code == 200, label
assert c.get("/").status_code == 200, label
assert c.get("/healthz").status_code == 200, label
reg = tmp_path / BENCHES_FILE
if payload == "OVERSIZED":
from booth.benches import BENCHES_MAX_BYTES
reg.write_text('{"http://a/": {"name": "' + "x" * BENCHES_MAX_BYTES + '"}}')
elif payload == "UNREADABLE":
if os.geteuid() == 0:
pytest.skip("root ignores the mode bit")
reg.write_text("{}")
reg.chmod(0o000)
elif payload == "FIFO":
os.mkfifo(reg)
elif payload is not None:
reg.write_text(payload)
try:
c = _client(tmp_path)
body = c.get("/b/links/")
assert body.status_code == 200, label
assert c.get("/").status_code == 200, label
assert c.get("/healthz").status_code == 200, label
# AND THE ERROR IS VISIBLE. Asserting only 200 was the gap: a render
# that swallowed the failure and drew an empty panel passed every case
# here while telling the operator nothing needed fixing. Absent is the
# one case that must NOT show an error.
shown = "the bench registry could not be read" in body.text
assert shown is (payload is not None), label
finally:
if payload == "UNREADABLE":
reg.chmod(0o644)
def test_damaged_and_absent_render_different_text(tmp_path):
@@ -612,3 +666,66 @@ def test_a_failed_write_leaves_no_scratch_file(tmp_path, monkeypatch):
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"]
def test_an_ipv6_literal_keeps_its_brackets():
"""`urlsplit().hostname` strips them, and a netloc rebuilt from it is not
another spelling of the URL — it is a broken one, so a re-post never
matches the row the operator means to update. Defeating change: rebuilding
netloc from `hostname` with no re-wrap, which is what this shipped as."""
assert normalize_bench_url("http://[::1]:8080/a") == "http://[::1]:8080/a"
assert normalize_bench_url("http://[2001:DB8::1]/p") == "http://[2001:db8::1]/p"
assert normalize_bench_url("HTTP://[::1]:80/p") == "http://[::1]/p"
# An UNBRACKETED IPv6 netloc is refused with a reason, not repaired:
# `urlsplit(...).port` raises on `::1:8080` because it cannot tell the
# address from the port — which is precisely why the brackets exist. The
# refusal is the honest answer; guessing where the address ends would be
# inventing an identity out of an ambiguous string.
with pytest.raises(ValueError):
normalize_bench_url("http://::1:8080/a")
def test_deeply_nested_json_does_not_escape_the_read(tmp_path):
"""RecursionError is neither ValueError nor OSError, so it went straight
past `read_benches`'s except pair and 500'd the page the function exists to
protect. The byte cap does not help: 200k open brackets is 200 KB, well
inside it. Defeating change: dropping the RecursionError arm."""
(tmp_path / BENCHES_FILE).write_text("[" * 200_000)
benches, err = read_benches(tmp_path)
assert benches == [] and err
def test_the_panel_renders_on_an_EMPTY_board(tmp_path):
"""The panel is gated on PAGE IDENTITY, not page content. Gating on
`board or benches` hid the panel and its registration form exactly when the
board was empty and the registry absent — the state a new deployment starts
in, and the one where "no benches registered yet" is most worth saying.
Defeating change: any content-derived gate."""
_board(tmp_path, "")
body = _client(tmp_path).get("/b/links/").text
assert "no benches registered yet" in body
assert "bench-add" in body, "the registration form vanished with the panel"
def test_the_panel_shows_when_a_bench_was_added(tmp_path):
"""INV-N/What renders: the contract says the panel shows the date it was
added; `b.added` appeared nowhere in the template and no test asked. All
four cold arms found this independently."""
_board(tmp_path, ROW_REF)
upsert_bench(tmp_path, "https://talk.test/", "talk", "o")
added = read_benches(tmp_path)[0][0].added[:10]
assert added in _client(tmp_path).get("/b/links/").text
@pytest.mark.parametrize("url,expected", BOOTH_URL_TABLE)
def test_the_dead_marker_classifies_the_SAME_table(tmp_path, url, expected):
"""INV-2 names RENDER-LEVEL agreement, and the marker tests never ran the
table — three hand-written rows with no query between them, so a marker
that stopped calling `booth_target` and treated `?q=1` as "not a booth"
stayed green while disagreeing with the CLI. Caught by the cold panel.
Every row whose target does not exist on disk must be marked dead; every
non-booth row must not be."""
_board(tmp_path, f"- [r]({url}) <sub>· x · 2026-09-01 00:00</sub>\n")
marked = bool(_dead_rows(_client(tmp_path).get("/b/links/").text))
assert marked is (expected is not None), (url, expected)
+26 -1
View File
@@ -358,6 +358,7 @@ REFUSED = 2
# decides what a booth URL is, and these are the rows the CLI must agree on.
# A second `/b/` check inlined in the shell for speed goes red HERE.
from test_benches import BOOTH_URL_TABLE # noqa: E402
from booth.benches import normalize_bench_url as normalize_bench_url_cli # noqa: E402
@pytest.mark.parametrize("url,is_booth", [(u, e is not None) for u, e in BOOTH_URL_TABLE])
@@ -378,9 +379,14 @@ def test_a_refused_link_writes_nothing_at_all(booth):
data, _ = booth
board = data / "links"
assert not board.exists()
before = sorted(p.name for p in data.iterdir())
r = run(data, "link", "http://10.100.10.50:8090/b/some-booth/", "nope")
assert r.returncode == REFUSED
assert not board.exists(), f"a refused link left {sorted(p.name for p in board.iterdir())}"
assert not board.exists(), "a refused link created the board directory"
# NOTHING AT ALL, not just no board. Asserting only `links/`'s absence let
# a refusal that touched `.benches.lock` (or any other sidecar) on its way
# out stay green — the cold panel's vacuity pass named exactly that.
assert sorted(p.name for p in data.iterdir()) == before, "a refused link wrote something"
def test_the_refusal_names_the_alternative(booth):
@@ -410,6 +416,18 @@ def test_bench_add_is_an_upsert(booth):
r = run(data, "bench", "ls")
assert r.returncode == OK, r.stderr
assert r.stdout.count("talk v") == 1 and "talk v2" in r.stdout
# THE ID, WHOLE AND UNTRUNCATED, because it is the locator `bench state`
# and `bench rm` take. An earlier version of this test had a docstring
# claiming `bench ls` prints ids and asserted nothing of the kind, while
# the code printed a url truncated to 52 columns — a claim standing in for
# evidence, which is how the drift would have survived CI. Found by all
# four cold arms independently.
bid = normalize_bench_url_cli("https://talk.nh3.phasefinal.com:8092/")
assert bid in r.stdout, r.stdout
# and what ls prints is addressable, end to end
line = [l for l in r.stdout.splitlines() if "talk v2" in l][0]
printed_id = line.split()[-1]
assert run(data, "bench", "state", printed_id, "promoted").returncode == OK
def test_bench_verbs_round_trip(booth):
@@ -476,6 +494,12 @@ def test_import_classifies_into_three_groups(booth):
assert "gone" in out # the booth row, skipped
assert "talk" in out # a candidate
assert "ftp://x.test/f" in out # refused, with its reason
# THE RAW URL BESIDE THE NORMALIZED ID, which is the entire point of the
# proposal: five rows of `talk` collapsing to one is only checkable if you
# can see which raw URLs produced the one id. This printed the description
# instead, so the collapse was invisible in the one place it had to be
# visible. All four cold arms found it.
assert out.count("https://talk.nh3.phasefinal.com:8092/") >= 2, out
def test_import_apply_collapses_the_repost(booth):
@@ -503,6 +527,7 @@ def test_nothing_in_the_unit_touches_links_md(booth):
run(data, "bench", "import", "--apply")
run(data, "bench", "add", "http://new.test/", "new")
run(data, "bench", "state", "http://new.test/", "retired")
run(data, "bench", "ls") # a read verb can truncate too
run(data, "bench", "rm", "http://new.test/")
after = hashlib.sha256((board / "links.md").read_bytes()).hexdigest()
assert before == after