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:
@@ -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))),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user