fix(u6): fold the cold contract panel — the import selection gap, and a document arguing with itself
/heid-contract-review panel 01M35BWCJ806MT75NA630Y4WFH. The headline arrived from all four arms independently and it is a missing feature, not a wording problem. `bench import --apply` registered every candidate, while the same contract says roughly 14 of 35 are reference bookmarks that must stay on the board. There was no selection mechanism between the dry-run report and the write -- so the write path did the exact thing this unit's rationale calls impossible, tell a bench from a bookmark by its URL, silently, to rows that belong where they are. The report existed precisely because the decision is not mechanizable. `--apply` now takes the ids the operator names; a bare `--apply` is refused and an unknown id is refused, both writing nothing. Two solo findings, both real: - A successful registration could push the registry past the size its own reader refuses, so the LAST bench added would make every other bench invisible while reporting success. The writer now respects the reader's cap. - The credential ban covered bench URLs and not `booth link`, the door this unit did not touch -- and the board renders on an unauthenticated LAN surface. A password can no longer reach it through either door. A small deliberate widening, named rather than smuggled. Cap semantics were readable three ways (refuse / clip-for-display / truncate-and-store) with a different build behind each, 4-of-4. Now stated per field: name and owner truncate, url and state are refused at the write and are DAMAGE at the read. url is not a display budget -- INV-7 promises the click goes to the posted address byte for byte, and a clipped URL keeps that promise in the type system while breaking it in the browser. The code had been clipping it; fixed. Two passages disagreed about one character: INV-7's specimen named "a trailing slash on a non-empty path" as something normalization changes, while the rule list keeps it and INV-6 makes the two spellings two benches. The rule list is right; the specimen was wrong. Found by 3-of-4. Also: INV-6's component list was illustrative where it had to be exhaustive and was short scheme and port; "writes nothing" appeared twice with different lists; the dead marker's predicate was readable two ways with 221 rows riding on it; and INV-2's falsifier read as though three callers agreeing pinned something, when three callers of one wrong predicate agree perfectly -- the table's expected values are the real check and now say so. 597 -> 604 tests.
This commit is contained in:
+26
-2
@@ -174,9 +174,21 @@ def _bench_from(bench_id: str, row: object) -> Bench:
|
||||
state = row.get("state", "live")
|
||||
if state not in BENCH_STATES:
|
||||
raise ValueError(f"{bench_id}: unknown state {state!r}")
|
||||
url = row.get("url", bench_id)
|
||||
if not isinstance(url, str):
|
||||
raise ValueError(f"{bench_id}: url must be text, not {type(url).__name__}")
|
||||
if len(url) > URL_MAX:
|
||||
# REFUSED, NOT TRUNCATED — unlike `name` and `owner`. Those are display
|
||||
# budgets and clipping one costs a few characters in a panel row. A
|
||||
# clipped URL is a DEAD ANCHOR, and INV-7 promises the click goes to the
|
||||
# posted address byte for byte; silently shortening it keeps the promise
|
||||
# in the type system and breaks it in the browser. Nothing this code
|
||||
# writes can get here (normalize refuses over-long input); a hand-edited
|
||||
# registry can, and it is damage, which is what the reader reports.
|
||||
raise ValueError(f"{bench_id}: url is longer than {URL_MAX} characters")
|
||||
return Bench(
|
||||
id=bench_id,
|
||||
url=_cap(row.get("url", bench_id), URL_MAX, "url"),
|
||||
url=url,
|
||||
name=_cap(row.get("name", ""), NAME_MAX, "name"),
|
||||
owner=_cap(row.get("owner", ""), OWNER_MAX, "owner"),
|
||||
state=state,
|
||||
@@ -275,9 +287,21 @@ def _write_all(root: Path, benches: dict[str, Bench]) -> None:
|
||||
# Per-pid scratch name so two writers cannot share it: the atomic-replace
|
||||
# promise is that a READER never sees a partial file, not that two writers
|
||||
# never collide on the way there.
|
||||
body = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
||||
# THE WRITER RESPECTS THE READER'S CAP. Without this, a successful
|
||||
# registration can push the file past BENCHES_MAX_BYTES and every
|
||||
# subsequent read fails — so the LAST bench somebody added is the one that
|
||||
# makes all the others invisible, and the write that did it reported
|
||||
# success. The reader is lenient about damage; it is not lenient about
|
||||
# size, and a writer that ignores a limit its own reader enforces is
|
||||
# manufacturing exactly the state the leniency exists to survive.
|
||||
if len(body.encode("utf-8")) > BENCHES_MAX_BYTES:
|
||||
raise ValueError(
|
||||
f"that registration would push the registry past {BENCHES_MAX_BYTES} "
|
||||
f"bytes, which its own reader refuses; nothing was written")
|
||||
tmp = path.with_suffix(path.suffix + f".tmp.{os.getpid()}")
|
||||
try:
|
||||
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
tmp.write_text(body)
|
||||
os.replace(tmp, path)
|
||||
except BaseException:
|
||||
# A write that dies between create and replace would otherwise strand
|
||||
|
||||
@@ -123,8 +123,26 @@ 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.
|
||||
**Caps, and what "applied" means for each — stated per field, because it is
|
||||
not the same verb for all of them.** The cold paraphrase panel found "applied at
|
||||
the write and again at the read" readable three ways (refuse / clip-for-display
|
||||
/ truncate-and-store) with a different build behind each, and 4-of-4 arms
|
||||
flagged it.
|
||||
|
||||
| field | cap | at the write | at the read |
|
||||
|---|---|---|---|
|
||||
| `name` | 120 | **truncated** | **truncated** |
|
||||
| `owner` | 64 | **truncated** | **truncated** |
|
||||
| `url` | 2048 | **refused** (`normalize_bench_url` raises) | **damage** — reported, never clipped |
|
||||
| `state` | one of three | **refused** | **damage** |
|
||||
| `id` | 2048 | **refused**, via the url it is derived from | **not applied** — see below |
|
||||
|
||||
`name` and `owner` are display budgets: clipping one costs a few characters in
|
||||
a panel row. **`url` is not a budget and must never be clipped**, at either end
|
||||
— INV-7 promises the click goes to the posted address byte for byte, and a
|
||||
shortened URL keeps that promise in the type system while breaking it in the
|
||||
browser. Nothing this code writes can store an over-long one; a hand-edited
|
||||
registry can, and that is damage.
|
||||
|
||||
**`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
|
||||
@@ -228,9 +246,14 @@ Three properties this refusal must have, each of which is an invariant below:
|
||||
- **It names the alternative.** The teaching moment belongs at the point of use;
|
||||
17 handles have the muscle memory and a bare "refused" would send them to a
|
||||
human.
|
||||
- **It writes nothing.** Not the row, and not the board's `.booth.json`
|
||||
announcement that `booth link` creates on first use — a refused call must not
|
||||
leave a new booth behind as a side effect.
|
||||
- **It writes nothing — nothing at all.** Not the row, not the board
|
||||
directory, not the `.booth.json` announcement `booth link` creates on first
|
||||
use, not a lock file. The test asserts the data root's entries are unchanged,
|
||||
not merely that `links.md` lacks the row.
|
||||
|
||||
*(Amended: this listed two items while INV-3 listed four, so a reader of the
|
||||
prose alone could conclude a lock file was permissible. One list now, and it
|
||||
is the strict one.)*
|
||||
- **It is the ONLY new refusal.** A reference bookmark is still a link.
|
||||
|
||||
## What renders
|
||||
@@ -244,6 +267,15 @@ On the standing board's page, above the rows:
|
||||
checkbox pre-reachable by the existing select-all, so the operator can tick
|
||||
and use the `unlink-many` control already on the page. **No new delete path.**
|
||||
|
||||
**Dead means exactly this, and both halves are load-bearing:**
|
||||
`booth_target(row.url)` is not None **AND** the name it returns is not a live
|
||||
directory in the data root. A row that is not a booth link is never dead, no
|
||||
matter what it points at — the Booth cannot know whether a gitea repo still
|
||||
exists and must not guess. A booth link whose booth is alive is not dead. No
|
||||
lifetime arithmetic is involved: a booth one minute from expiry is alive.
|
||||
*(Stated after 3-of-4 cold arms read the rule two ways — predicate-driven vs
|
||||
existence-driven — with 221 rows riding on which.)*
|
||||
|
||||
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.
|
||||
@@ -263,18 +295,29 @@ 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
|
||||
booth bench import [--apply] classify the board's rows; WRITES NOTHING
|
||||
without --apply, and never touches links.md
|
||||
booth bench import classify the board's rows; WRITES NOTHING
|
||||
booth bench import --apply <id>... register ONLY the ids you name
|
||||
|
||||
`<id|url>` takes EITHER form because the input is normalized before the lookup,
|
||||
and normalization is idempotent — an id normalizes to itself. So the id `ls`
|
||||
prints and the raw URL in the operator's scrollback both address the same row.
|
||||
Pinned by a test, because it is the property that makes the two-form promise
|
||||
true rather than merely intended.
|
||||
```
|
||||
|
||||
`import` prints three groups — **booth rows** (skipped; `booth_target` matched),
|
||||
**candidates** (would be registered, with the normalized id beside the raw URL
|
||||
so a collapse is visible before it happens), and **refused** (normalization
|
||||
raised, with the reason). Under `--apply` it upserts the candidates and prints
|
||||
the same three groups plus what it did. It is not automatic and it is not run by
|
||||
this unit: roughly 14 of 35 candidates are reference bookmarks that belong on the
|
||||
board, and a machine cannot tell a bench from a bookmark by its URL. The
|
||||
operator seeds the registry by reviewing that list.
|
||||
**candidates** (the normalized id beside the raw URL, so a collapse is visible
|
||||
before it happens), and **refused** (normalization raised, with the reason).
|
||||
|
||||
**`--apply` REQUIRES THE IDS. A bare `--apply` is refused.** This is the
|
||||
unit's sharpest correction and it came from all four arms of the cold paraphrase
|
||||
panel independently: the first draft registered every candidate, which made the
|
||||
write path do the exact thing this document's own rationale calls impossible —
|
||||
**tell a bench from a bookmark by its URL** — silently, to roughly 14 of 35 rows
|
||||
that belong on the board. The dry run prints ids; the operator names the ones
|
||||
that are benches; an id that is not a candidate is refused and nothing is
|
||||
written. There was no selection mechanism between the report and the write, and
|
||||
the report existed precisely because the decision is not mechanizable.
|
||||
|
||||
## The migration
|
||||
|
||||
@@ -282,7 +325,7 @@ operator seeds the registry by reviewing that list.
|
||||
**and committed to this repo**, before anything else. Nothing the operator
|
||||
wrote is destroyed, and the archive is version-controlled rather than living
|
||||
only on one box.
|
||||
2. `booth bench import` proposes; the operator applies.
|
||||
2. `booth bench import` proposes; the operator applies **by naming ids**.
|
||||
3. The 156 dead booth rows are marked, and removed by him or not at all.
|
||||
|
||||
## Scope — the blast-radius pass
|
||||
@@ -330,12 +373,20 @@ that the panel renders would pass under exactly that change.
|
||||
the only implementation, and the CLI's refusal, the dead marker and the import's
|
||||
classifier all call it.
|
||||
*Falsifiable:* the defeating change is a second implementation — a `/b/` check
|
||||
inlined in the shell for speed, or a regex in `app.py`. The test asserts
|
||||
AGREEMENT rather than behaviour: a table of URLs (trailing slash, no slash,
|
||||
nested path, query, uppercase host, a non-Booth host with a `/b/` path, a `/b/`
|
||||
with no name) is run through the CLI's refusal AND the render's dead marker, and
|
||||
the two must classify every row identically. A test that only checked the
|
||||
refusal would stay green while the marker drifted.
|
||||
inlined in the shell for speed, or a regex in `app.py`. One table of URLs
|
||||
(trailing slash, no slash, nested path, query, uppercase host, a non-Booth host
|
||||
with a `/b/` path, a `/b/` with no name, a percent-encoded name, a decoded `..`
|
||||
and a decoded separator) runs through the predicate, the CLI's refusal AND the
|
||||
render's dead marker.
|
||||
|
||||
**AGREEMENT IS THE WEAKER HALF AND IS NOT THE TEST.** Three callers of one
|
||||
wrong predicate agree perfectly, so agreement alone pins nothing — the table's
|
||||
**expected values** are the independent check, and the agreement rows exist to
|
||||
catch a second implementation drifting from the first. Both are asserted; only
|
||||
one of them would survive `booth_target` itself being wrong. *(Named after a
|
||||
cold arm pointed out that the falsifier reads as though agreement were
|
||||
sufficient.)* **A bare `/b/` with no name is NOT a booth link**, and the table
|
||||
pins that.
|
||||
|
||||
**INV-3 — a refused link writes nothing.** No row, no board directory, no
|
||||
`.booth.json`, no lock file.
|
||||
@@ -367,8 +418,16 @@ error rather than an empty panel. Case (d) is the one that matters: it is the
|
||||
shape that is currently 500ing the gallery elsewhere in this service.
|
||||
|
||||
**INV-6 — the identity collapses a re-post and nothing else.** Upserting the
|
||||
same normalized URL updates one row; upserting two URLs that differ in path,
|
||||
query or host creates two.
|
||||
same normalized URL updates one row; upserting two URLs that differ in **scheme,
|
||||
host, non-default port, path, or query** creates two. **That list is
|
||||
EXHAUSTIVE** — the only things normalization discards are a fragment, a
|
||||
scheme-default port, letter case in the scheme and host, a bare `/` path, and
|
||||
surrounding whitespace.
|
||||
|
||||
*(Amended: this said "path, query or host" with no "only", which reads as
|
||||
illustrative and left an implementer free to "fix" the rule from the
|
||||
invariant's wording — and it omitted scheme and port, two of the five. 3-of-4
|
||||
cold arms flagged it; the falsifier now carries vectors for both.)*
|
||||
*Falsifiable:* the defeating change is normalizing to the origin. The test
|
||||
registers the eight gitea URLs measured on the live board and asserts **eight**
|
||||
benches, then registers `talk`'s five rows and asserts **one** — the same
|
||||
@@ -378,9 +437,15 @@ pass under origin normalization, which is precisely the wrong rule.
|
||||
**INV-7 — `url` is what a click goes to; `id` is never rendered as an href.**
|
||||
*Falsifiable:* the defeating change is rendering `bench.id` in the anchor
|
||||
because it is "the clean one". The test registers a URL whose normalization
|
||||
differs from its raw form (a trailing slash on a non-empty path, an uppercase
|
||||
host, a fragment) and asserts the anchor's `href` is the raw string, byte for
|
||||
byte.
|
||||
differs from its raw form — **an uppercase host, an explicit default port, and
|
||||
a fragment** — and asserts the anchor's `href` is the raw string, byte for byte.
|
||||
|
||||
*(Amended: this parenthetical used to name "a trailing slash on a non-empty
|
||||
path" as one of the differences. **It is not one** — the rule list keeps a
|
||||
non-empty path verbatim, slash included, and INV-6 makes `…/p` and `…/p/` two
|
||||
benches. Two passages of this document disagreed about the same character, and
|
||||
3-of-4 cold arms found the contradiction. The rule list is correct; this
|
||||
sentence was wrong.)*
|
||||
|
||||
**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`
|
||||
|
||||
+43
-7
@@ -359,6 +359,19 @@ case "$cmd" in
|
||||
echo "booth link: the booth check answered something unrecognised; nothing was posted." >&2
|
||||
exit 3 ;;
|
||||
esac
|
||||
# CREDENTIALS DO NOT GO ON THE BOARD, through any door. `normalize_bench_url`
|
||||
# refuses userinfo for a bench; `booth link` is the door this unit did not
|
||||
# touch, and the board renders on an unauthenticated LAN surface. A small,
|
||||
# deliberate widening of the unit -- named rather than smuggled.
|
||||
case "$link_url" in
|
||||
*://*@*)
|
||||
{
|
||||
echo "booth link: that URL carries credentials (user:pass@host) and the board"
|
||||
echo " is readable by anyone who can reach this service. Nothing was posted."
|
||||
echo " strip the credentials and post it again."
|
||||
} >&2
|
||||
exit 2 ;;
|
||||
esac
|
||||
if [ -n "$refused_name" ]; then
|
||||
{
|
||||
echo "booth link: that is a booth, and a booth announces itself now."
|
||||
@@ -523,6 +536,7 @@ elif sub in ("state", "rm"):
|
||||
print("%s is now %s" % (moved.url, moved.state))
|
||||
elif sub == "import":
|
||||
apply = "--apply" in argv
|
||||
picked = [a for a in argv if a != "--apply"]
|
||||
board = root / os.environ["BOOTH_BOARD"] / "links.md"
|
||||
if not board.is_file(): die("no link board at %s" % board, 1)
|
||||
skipped, candidates, refused = [], [], []
|
||||
@@ -554,16 +568,38 @@ elif sub == "import":
|
||||
print(" %-52s %s" % (e["url"], why))
|
||||
if not apply:
|
||||
print()
|
||||
print("nothing was written. re-run with --apply to register the candidates.")
|
||||
print("NOTE: a machine cannot tell a bench from a bookmark by its URL —")
|
||||
print(" roughly 14 of 35 live candidates are repos, model cards and docs,")
|
||||
print(" for which the board is the right home. Review before applying.")
|
||||
print("nothing was written.")
|
||||
print(" booth bench import --apply <id>... register ONLY the ids you name")
|
||||
print()
|
||||
print("A MACHINE CANNOT TELL A BENCH FROM A BOOKMARK BY ITS URL. On the live")
|
||||
print("board roughly 14 of 35 candidates are repos, model cards and docs, for")
|
||||
print("which the board is the right and only home. So `--apply` takes the ids")
|
||||
print("YOU pick from the list above; it will not register the whole set.")
|
||||
raise SystemExit(0)
|
||||
for i, e in candidates:
|
||||
# SELECTION IS MANDATORY. A bare `--apply` would do exactly the thing this
|
||||
# rationale of this very unit says is impossible -- decide bench-vs-bookmark
|
||||
# URL -- and it would do it silently, to ~14 rows that belong on the board.
|
||||
# The dry-run prints the ids; the operator names the ones that are benches.
|
||||
if not picked:
|
||||
print()
|
||||
print("booth bench import --apply needs the ids to register.", file=sys.stderr)
|
||||
print(" nothing was written. copy the ids you want from the list above:",
|
||||
file=sys.stderr)
|
||||
print(" booth bench import --apply <id> [<id>...]", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
by_id = {i: e for i, e in candidates}
|
||||
unknown = [i for i in picked if i not in by_id]
|
||||
if unknown:
|
||||
print()
|
||||
for i in unknown:
|
||||
print("not a candidate id: %s" % i, file=sys.stderr)
|
||||
print("nothing was written.", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
for i in picked:
|
||||
e = by_id[i]
|
||||
upsert_bench(root, e["url"], e["desc"], e["who"] or who)
|
||||
print()
|
||||
print("applied: %d distinct benches registered. links.md was NOT modified."
|
||||
% len({i for i, _ in candidates}))
|
||||
print("applied: %d bench(es) registered. links.md was NOT modified." % len(set(picked)))
|
||||
' "$@"
|
||||
;;
|
||||
ask)
|
||||
|
||||
+113
-2
@@ -245,10 +245,28 @@ def test_oversized_registry_is_refused_by_size_before_parsing(tmp_path):
|
||||
"""INV-5. Defeating change: parsing first and checking length after, which
|
||||
costs the caller the whole file. A FIFO has st_size 0, so the guard must
|
||||
bound the READ, not trust the stat — the 2026-09-22 hang lesson."""
|
||||
from booth.benches import BENCHES_MAX_BYTES
|
||||
_write_raw(tmp_path, '{"http://a/": {"name": "' + "x" * BENCHES_MAX_BYTES + '"}}')
|
||||
import booth.benches as B
|
||||
_write_raw(tmp_path, '{"http://a/": {"name": "' + "x" * B.BENCHES_MAX_BYTES + '"}}')
|
||||
benches, err = read_benches(tmp_path)
|
||||
assert err and benches == []
|
||||
# AND PROVE THE PARSE WAS NEVER REACHED. Asserting only the eventual result
|
||||
# passes an implementation that loads the whole document and checks its
|
||||
# length afterwards — which costs the caller exactly what the cap exists to
|
||||
# save. Booby-trap json.loads: if it runs, the test says so. Cold panel,
|
||||
# hulda F11.
|
||||
import json as _json
|
||||
tripped = []
|
||||
real = _json.loads
|
||||
|
||||
def trap(*a, **k):
|
||||
tripped.append(True)
|
||||
return real(*a, **k)
|
||||
B.json.loads = trap
|
||||
try:
|
||||
benches, err = read_benches(tmp_path)
|
||||
finally:
|
||||
B.json.loads = real
|
||||
assert err and not tripped, "the oversized registry was parsed before it was refused"
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.geteuid() == 0, reason="root ignores the mode bit")
|
||||
@@ -328,6 +346,13 @@ BOOTH_URL_TABLE = [
|
||||
("http://10.100.10.50:8090/b/a%2Fb/", None), # decoded separator
|
||||
("https://gitea.phasefinal.com/vh/peedlar", None),
|
||||
("not a url at all", None),
|
||||
# THE ACCEPTED COST, MADE EXPLICIT. The predicate is host-agnostic on
|
||||
# purpose — a host allowlist fails OPEN on whichever name somebody reaches
|
||||
# this service by next — so a third-party URL with a `/b/<x>` path reads as
|
||||
# a booth link and is refused. The contract names this trade-off; the table
|
||||
# had no row exercising it, so nothing pinned the behaviour either way.
|
||||
# Cold panel, hulda F10.
|
||||
("https://example.com/b/not-ours/", "not-ours"),
|
||||
]
|
||||
|
||||
|
||||
@@ -355,6 +380,7 @@ def test_added_survives_reregistration_updated_does_not(tmp_path):
|
||||
assert not created
|
||||
assert second.added == first.added
|
||||
assert second.name == "two" and second.owner == "o2"
|
||||
assert second.updated >= first.updated
|
||||
|
||||
|
||||
def test_state_survives_reregistration(tmp_path):
|
||||
@@ -729,3 +755,88 @@ def test_the_dead_marker_classifies_the_SAME_table(tmp_path, url, expected):
|
||||
_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)
|
||||
|
||||
|
||||
def test_updated_is_replaced_and_added_is_not(tmp_path, monkeypatch):
|
||||
"""The other half of `test_added_survives_reregistration_updated_does_not`,
|
||||
which asserted only the half in the first clause of its own name.
|
||||
|
||||
The stamp has SECOND resolution, so a fast test cannot tell a replaced
|
||||
`updated` from a frozen one by comparing real clocks — `>=` passes either
|
||||
way, which is a falsifier that cannot fail. The clock is driven instead, so
|
||||
"was it rewritten" is answerable. Cold panel, hulda F12.
|
||||
|
||||
Defeating change: carrying `updated` forward from the prior record the way
|
||||
`added` is carried, which every real-clock assertion in this file survives.
|
||||
"""
|
||||
import booth.benches as B
|
||||
ticks = iter(["2026-01-01T00:00:00+00:00",
|
||||
"2026-06-06T06:06:06+00:00",
|
||||
"2026-12-31T23:59:59+00:00"])
|
||||
monkeypatch.setattr(B, "_now", lambda: next(ticks))
|
||||
|
||||
first, _ = upsert_bench(tmp_path, "http://a.test/", "one", "o")
|
||||
assert first.added == first.updated == "2026-01-01T00:00:00+00:00"
|
||||
|
||||
second, _ = upsert_bench(tmp_path, "http://a.test/", "two", "o")
|
||||
assert second.added == "2026-01-01T00:00:00+00:00", "added must survive an upsert"
|
||||
assert second.updated == "2026-06-06T06:06:06+00:00", "updated must be replaced"
|
||||
|
||||
# A STATE CHANGE IS A MUTATION and bumps it too — this is what the contract
|
||||
# was amended to say, after the panel read "most recent upsert" literally.
|
||||
third = set_bench_state(tmp_path, normalize_bench_url("http://a.test/"), "retired")
|
||||
assert third.added == "2026-01-01T00:00:00+00:00"
|
||||
assert third.updated == "2026-12-31T23:59:59+00:00"
|
||||
|
||||
|
||||
def test_a_registration_cannot_make_the_registry_unreadable(tmp_path):
|
||||
"""Cold contract panel, hulda solo: the write path permitted a file the
|
||||
reader then refuses on size — so the LAST bench somebody added would be the
|
||||
one that made every other bench invisible, and the write that did it
|
||||
reported success.
|
||||
|
||||
Defeating change: dropping the size check from `_write_all`. The reader is
|
||||
lenient about damage and deliberately NOT lenient about size; a writer
|
||||
ignoring a limit its own reader enforces manufactures exactly the state
|
||||
that leniency exists to survive."""
|
||||
from booth.benches import BENCHES_MAX_BYTES, NAME_MAX
|
||||
n = 0
|
||||
while True:
|
||||
n += 1
|
||||
try:
|
||||
upsert_bench(tmp_path, f"http://h{n}.test/{'p' * 1800}", "x" * NAME_MAX, "o")
|
||||
except ValueError as exc:
|
||||
assert "past" in str(exc) and str(BENCHES_MAX_BYTES) in str(exc)
|
||||
break
|
||||
assert n < 500, "never hit the cap; widen the fixture"
|
||||
# THE REGISTRY IS STILL READABLE, and still holds everything that fit.
|
||||
benches, err = read_benches(tmp_path)
|
||||
assert err is None, err
|
||||
assert len(benches) == n - 1
|
||||
|
||||
|
||||
def test_an_over_long_stored_url_is_damage_not_a_silent_clip(tmp_path):
|
||||
"""Cold contract panel, 4-of-4 on cap semantics: "applied at the read" did
|
||||
not say TRUNCATE or REFUSE, and the code had picked truncate for every
|
||||
field. For `name` and `owner` that is right — they are display budgets and
|
||||
clipping costs a few characters in a panel row. For `url` it is wrong:
|
||||
INV-7 promises the click goes to the posted address byte for byte, and a
|
||||
clipped URL keeps that promise in the type system while breaking it in the
|
||||
browser. Defeating change: routing `url` back through `_cap`."""
|
||||
from booth.benches import URL_MAX
|
||||
long_url = "http://a/" + "p" * (URL_MAX + 10)
|
||||
_write_raw(tmp_path, json.dumps({"http://a/": {
|
||||
"url": long_url, "name": "n", "owner": "o", "state": "live"}}))
|
||||
benches, err = read_benches(tmp_path)
|
||||
assert err and benches == [], "an over-long url was clipped into a dead anchor"
|
||||
|
||||
|
||||
def test_name_and_owner_ARE_clipped_at_the_read(tmp_path):
|
||||
"""The other half of the same rule, so the asymmetry is pinned in both
|
||||
directions rather than asserted in one."""
|
||||
from booth.benches import NAME_MAX, OWNER_MAX
|
||||
_write_raw(tmp_path, json.dumps({"http://a/": {
|
||||
"url": "http://a/", "name": "n" * 500, "owner": "o" * 500, "state": "live"}}))
|
||||
benches, err = read_benches(tmp_path)
|
||||
assert err is None
|
||||
assert len(benches[0].name) == NAME_MAX and len(benches[0].owner) == OWNER_MAX
|
||||
|
||||
+78
-6
@@ -440,11 +440,28 @@ def test_bench_verbs_round_trip(booth):
|
||||
|
||||
|
||||
def test_bench_state_and_rm_take_an_id_or_a_url(booth):
|
||||
"""`bench ls` prints ids; the operator has the URL. Both must address."""
|
||||
"""`bench ls` prints ids; the operator has the URL. BOTH must address.
|
||||
|
||||
This used to invoke both verbs with the URL only, twice, while its docstring
|
||||
claimed it covered the id — the same claim-not-evidence shape as the `ls`
|
||||
docstring. A raw URL whose normalization DIFFERS from it is used, so the two
|
||||
columns are genuinely distinct inputs. Cold panel, regin F8.
|
||||
"""
|
||||
data, _ = booth
|
||||
run(data, "bench", "add", "http://x.test/p/", "ex")
|
||||
assert run(data, "bench", "state", "http://x.test/p/", "retired").returncode == OK
|
||||
assert run(data, "bench", "rm", "http://x.test/p/").returncode == OK
|
||||
raw = "HTTP://X.Test:80/p/?b=2&a=1#frag"
|
||||
bid = normalize_bench_url_cli(raw)
|
||||
assert bid != raw, "pick a URL whose normalization actually differs"
|
||||
run(data, "bench", "add", raw, "ex")
|
||||
# by the ID the registry stores
|
||||
assert run(data, "bench", "state", bid, "retired").returncode == OK
|
||||
assert "retired" in run(data, "bench", "ls").stdout
|
||||
# and by the RAW URL the operator has in their scrollback
|
||||
assert run(data, "bench", "state", raw, "live").returncode == OK
|
||||
assert "live" in run(data, "bench", "ls").stdout
|
||||
assert run(data, "bench", "rm", raw).returncode == OK
|
||||
run(data, "bench", "add", raw, "ex again")
|
||||
assert run(data, "bench", "rm", bid).returncode == OK
|
||||
assert "ex" not in run(data, "bench", "ls").stdout
|
||||
|
||||
|
||||
def test_bench_add_refuses_a_bad_url_with_the_reason(booth):
|
||||
@@ -502,10 +519,51 @@ def test_import_classifies_into_three_groups(booth):
|
||||
assert out.count("https://talk.nh3.phasefinal.com:8092/") >= 2, out
|
||||
|
||||
|
||||
def test_bare_apply_refuses_and_writes_nothing(booth):
|
||||
"""THE SELECTION GAP — all four cold contract-review arms, independently.
|
||||
|
||||
`--apply` used to register every candidate, while the same contract says
|
||||
roughly 14 of 35 are reference bookmarks that must STAY on the board. That
|
||||
made the write path do the exact thing the unit's own rationale calls
|
||||
impossible — tell a bench from a bookmark by its URL — silently, to rows
|
||||
that belong where they are. The dry-run prints ids; `--apply` takes the
|
||||
ones the operator names, and refuses without them.
|
||||
|
||||
Defeating change: restoring the register-everything branch."""
|
||||
data, _ = booth
|
||||
_seed_board(data)
|
||||
r = run(data, "bench", "import", "--apply")
|
||||
assert r.returncode == REFUSED
|
||||
assert "needs the ids" in r.stderr
|
||||
assert not (data / ".benches.json").exists(), "a bare --apply wrote the registry"
|
||||
|
||||
|
||||
def test_apply_refuses_an_id_that_is_not_a_candidate(booth):
|
||||
data, _ = booth
|
||||
_seed_board(data)
|
||||
r = run(data, "bench", "import", "--apply", "http://not-on-the-board/")
|
||||
assert r.returncode == REFUSED
|
||||
assert "not a candidate id" in r.stderr
|
||||
assert not (data / ".benches.json").exists()
|
||||
|
||||
|
||||
def test_apply_registers_ONLY_the_named_ids(booth):
|
||||
"""The bookmark stays a bookmark unless the operator says otherwise."""
|
||||
data, _ = booth
|
||||
_seed_board(data)
|
||||
talk = normalize_bench_url_cli("https://talk.nh3.phasefinal.com:8092/")
|
||||
assert run(data, "bench", "import", "--apply", talk).returncode == OK
|
||||
ls = run(data, "bench", "ls").stdout
|
||||
assert "peedlar" not in ls, "an unnamed candidate was registered anyway"
|
||||
assert len([l for l in ls.splitlines() if "talk" in l]) == 1
|
||||
|
||||
|
||||
def test_import_apply_collapses_the_repost(booth):
|
||||
data, _ = booth
|
||||
_seed_board(data)
|
||||
assert run(data, "bench", "import", "--apply").returncode == OK
|
||||
talk = normalize_bench_url_cli("https://talk.nh3.phasefinal.com:8092/")
|
||||
repo = normalize_bench_url_cli("https://gitea.phasefinal.com/vh/peedlar")
|
||||
assert run(data, "bench", "import", "--apply", talk, repo).returncode == OK
|
||||
ls = run(data, "bench", "ls").stdout
|
||||
# ONE ROW, counted by line: "talk" appears in both the name and the
|
||||
# hostname, so a substring count would read 2 for a correctly collapsed row.
|
||||
@@ -524,7 +582,9 @@ def test_nothing_in_the_unit_touches_links_md(booth):
|
||||
before = hashlib.sha256((board / "links.md").read_bytes()).hexdigest()
|
||||
run(data, "link", "http://10.100.10.50:8090/b/x/", "refused")
|
||||
run(data, "bench", "import")
|
||||
run(data, "bench", "import", "--apply")
|
||||
run(data, "bench", "import", "--apply") # refused, writes nothing
|
||||
run(data, "bench", "import", "--apply",
|
||||
normalize_bench_url_cli("https://talk.nh3.phasefinal.com:8092/"))
|
||||
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
|
||||
@@ -553,3 +613,15 @@ def test_link_fails_CLOSED_when_the_booth_check_cannot_run(booth, tmp_path):
|
||||
assert r.returncode != OK
|
||||
assert "could not check" in r.stderr, r.stderr
|
||||
assert not (data / "links" / "links.md").exists(), "a row landed despite an unusable check"
|
||||
|
||||
|
||||
def test_a_credential_never_reaches_the_board(booth):
|
||||
"""`normalize_bench_url` refuses userinfo for a bench; `booth link` was the
|
||||
door this unit did not touch, and the board renders on an unauthenticated
|
||||
LAN surface. Cold contract panel, groa solo. A deliberate small widening of
|
||||
the unit, named rather than smuggled."""
|
||||
data, _ = booth
|
||||
r = run(data, "link", "https://user:hunter2@x.test/p", "leaky")
|
||||
assert r.returncode != OK
|
||||
assert "credentials" in r.stderr
|
||||
assert not (data / "links").exists(), "a credentialed URL created the board"
|
||||
|
||||
Reference in New Issue
Block a user