feat(u6): benches — a registry with identity, and the rule enforced
The standing link board carried three jobs because only one of them had a surface. Re-measured before contracting, its 221 rows split into 178 booth announcements (156 already dead) and 43 non-booth rows, of which 8 are the same bench re-posted. U5 gave the booth announcement a home; this gives the running service one, and refuses the one shape that now has somewhere better to go. - booth/benches.py (new, stdlib-only and sibling-free): the Bench record, URL normalization as the identity, a lenient read on the render path and a strict read on the write path, atomic replace under an flock, and a stated total order (state rank, name casefolded, id). - links.booth_target: ONE predicate for "is this a booth URL", consumed by the CLI refusal, the board's dead marker and bench import. Host-agnostic, path-shaped, percent-decoded, never raises. - booth link refuses a booth URL, names `booth new --why`, and writes nothing — not the row, not the board directory, not the announcement. - The board marks rows whose booth has been swept. Nothing here deletes a row: removal stays the operator's two clicks through the existing bulk control. - booth bench add|ls|state|rm|import. import writes nothing without --apply and never edits links.md. - docs/archive/links-2026-09-22.md: the board archived verbatim into git. Identity is the FULL normalized URL, not the origin, and that was measured: origin identity collapses the 43 non-booth rows to 19 groups by merging eight distinct gitea repositories into one row, three unrelated HuggingFace model cards into one, and the two LRPG surfaces on 10.100.10.50:8321 — the design doc's own example of two real benches — into one. Full-URL identity still collapses both cases that doc names: talk 5 to 1, Peedlar 3 to 1. booth link is NOT deprecated. Roughly 14 of the 35 distinct non-booth targets are reference bookmarks for which the board is the right and only home; the design doc's plan to deprecate it would have evicted a third of its live content. Corrected there, along with what "normalized URL" means. The seam review found three real defects in the contract before any code: the claim that test_stdlib_only already forbids sibling imports (it exempts `booth` on purpose), naming resolve_booth as the dead marker's existence check (it raises HTTPException(404), so one swept booth would have 404'd the whole board page), and silence on percent-encoding (booth links are emitted through quote(name, safe=""), so a raw comparison marks every encoded booth dead forever). That both list_booths and sweep_once skip the registry was verified against the real functions rather than assumed. 444 -> 555 tests. Deployed and verified live: 23/23 booths 200, and the board renders 156 dead of 221 rows, matching an independent pre-implementation count. NOT TAGGED: both cold gates are in flight (contract review 01M35BWCJ806MT75NA630Y4WFH, code review 01M35CK8YKEKMV7T15JXEF6A8N) and the bug-hunt has not run. Per the v0.2.0 lesson, the tag waits for the gates.
This commit is contained in:
@@ -346,3 +346,163 @@ def test_answer_does_not_poll_forever_on_a_pick_that_cannot_be_answered(tmp_path
|
||||
"BOOTH_URL": "http://booth.invalid"})
|
||||
assert r.returncode != 0
|
||||
assert "broken" in r.stderr.lower() or "cannot" in r.stderr.lower()
|
||||
|
||||
|
||||
# ---- U6: benches ------------------------------------------------------------
|
||||
#
|
||||
# The CLI half of the unit. `docs/contracts/u6_benches.contract.md`.
|
||||
|
||||
REFUSED = 2
|
||||
|
||||
# Shared with tests/test_benches.py::BOOTH_URL_TABLE — INV-2 says ONE predicate
|
||||
# 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
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url,is_booth", [(u, e is not None) for u, e in BOOTH_URL_TABLE])
|
||||
def test_link_refuses_exactly_what_booth_target_matches(booth, url, is_booth):
|
||||
"""INV-2. Defeating change: a `case "$url" in *':8090/b/'*)` in the shell,
|
||||
which would classify the host-agnostic and percent-encoded rows differently
|
||||
from the Python predicate the board's dead marker uses."""
|
||||
data, _ = booth
|
||||
r = run(data, "link", url, "a description")
|
||||
assert (r.returncode == REFUSED) is is_booth, (url, r.returncode, r.stderr)
|
||||
|
||||
|
||||
def test_a_refused_link_writes_nothing_at_all(booth):
|
||||
"""INV-3. Defeating change: putting the refusal AFTER the `mkdir -p` /
|
||||
announce block, which is where it would naturally land if written without
|
||||
thinking. Asserting only that links.md lacks the row would PASS under that
|
||||
change — so this asserts the board directory does not exist."""
|
||||
data, _ = booth
|
||||
board = data / "links"
|
||||
assert not board.exists()
|
||||
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())}"
|
||||
|
||||
|
||||
def test_the_refusal_names_the_alternative(booth):
|
||||
"""The teaching moment belongs at the point of use: 17 handles have the
|
||||
muscle memory, and a bare 'refused' sends them to a human."""
|
||||
data, _ = booth
|
||||
r = run(data, "link", "http://10.100.10.50:8090/b/some-booth/", "nope")
|
||||
out = r.stderr + r.stdout
|
||||
assert "--why" in out and "some-booth" in out
|
||||
|
||||
|
||||
def test_a_reference_bookmark_is_still_a_link(booth):
|
||||
"""The board keeps its residual job. Measured: ~14 of the 35 distinct
|
||||
non-booth targets are repos, model cards and docs, for which the board is
|
||||
the right and only home. A second refusal would break that."""
|
||||
data, _ = booth
|
||||
r = run(data, "link", "https://gitea.phasefinal.com/vh/peedlar", "the repo")
|
||||
assert r.returncode == OK, r.stderr
|
||||
assert "the repo" in (data / "links" / "links.md").read_text()
|
||||
|
||||
|
||||
def test_bench_add_is_an_upsert(booth):
|
||||
data, _ = booth
|
||||
for i in range(3):
|
||||
r = run(data, "bench", "add", "https://talk.nh3.phasefinal.com:8092/", f"talk v{i}")
|
||||
assert r.returncode == OK, r.stderr
|
||||
r = run(data, "bench", "ls")
|
||||
assert r.returncode == OK, r.stderr
|
||||
assert r.stdout.count("talk v") == 1 and "talk v2" in r.stdout
|
||||
|
||||
|
||||
def test_bench_verbs_round_trip(booth):
|
||||
data, _ = booth
|
||||
assert run(data, "bench", "add", "http://x.test/", "ex").returncode == OK
|
||||
assert run(data, "bench", "state", "http://x.test/", "promoted").returncode == OK
|
||||
assert "promoted" in run(data, "bench", "ls").stdout
|
||||
assert run(data, "bench", "rm", "http://x.test/").returncode == OK
|
||||
assert "ex" not in run(data, "bench", "ls").stdout
|
||||
|
||||
|
||||
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."""
|
||||
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
|
||||
|
||||
|
||||
def test_bench_add_refuses_a_bad_url_with_the_reason(booth):
|
||||
data, _ = booth
|
||||
r = run(data, "bench", "add", "ftp://x.test/f", "ex")
|
||||
assert r.returncode != OK
|
||||
assert "http" in (r.stderr + r.stdout).lower()
|
||||
|
||||
|
||||
def test_bare_bench_names_the_bench_verbs(booth):
|
||||
"""Seam review SR-6: `bench` is the first two-word verb in this script, and
|
||||
falling through to the generic usage hides which word was wrong."""
|
||||
data, _ = booth
|
||||
r = run(data, "bench")
|
||||
assert r.returncode != OK
|
||||
assert "add" in r.stderr and "import" in r.stderr
|
||||
|
||||
|
||||
def _seed_board(data):
|
||||
board = data / "links"
|
||||
board.mkdir(parents=True, exist_ok=True)
|
||||
(board / "links.md").write_text(
|
||||
"- [a booth](http://10.100.10.50:8090/b/gone/) <sub>· x · 2026-09-01 00:00</sub>\n"
|
||||
"- [talk](https://talk.nh3.phasefinal.com:8092/) <sub>· x · 2026-09-01 00:00</sub>\n"
|
||||
"- [talk again](https://talk.nh3.phasefinal.com:8092/) <sub>· x · 2026-09-02 00:00</sub>\n"
|
||||
"- [a repo](https://gitea.phasefinal.com/vh/peedlar) <sub>· x · 2026-09-03 00:00</sub>\n"
|
||||
"- [bad](ftp://x.test/f) <sub>· x · 2026-09-04 00:00</sub>\n"
|
||||
)
|
||||
return board
|
||||
|
||||
|
||||
def test_import_writes_nothing_without_apply(booth):
|
||||
"""INV-8. A proposal that writes is not a proposal."""
|
||||
data, _ = booth
|
||||
board = _seed_board(data)
|
||||
before = (board / "links.md").read_text()
|
||||
r = run(data, "bench", "import")
|
||||
assert r.returncode == OK, r.stderr
|
||||
assert not (data / ".benches.json").exists()
|
||||
assert (board / "links.md").read_text() == before
|
||||
|
||||
|
||||
def test_import_classifies_into_three_groups(booth):
|
||||
data, _ = booth
|
||||
_seed_board(data)
|
||||
out = run(data, "bench", "import").stdout
|
||||
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
|
||||
|
||||
|
||||
def test_import_apply_collapses_the_repost(booth):
|
||||
data, _ = booth
|
||||
_seed_board(data)
|
||||
assert run(data, "bench", "import", "--apply").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.
|
||||
assert len([l for l in ls.splitlines() if "talk" in l]) == 1, ls
|
||||
assert "peedlar" in ls
|
||||
assert "gone" not in ls, "a booth row was imported as a bench"
|
||||
|
||||
|
||||
def test_nothing_in_the_unit_touches_links_md(booth):
|
||||
"""INV-8. Defeating change: `import --apply` tidying up the rows it
|
||||
consumed. The whole CLI surface runs against one board and the file must
|
||||
come out byte-identical."""
|
||||
import hashlib
|
||||
data, _ = booth
|
||||
board = _seed_board(data)
|
||||
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", "add", "http://new.test/", "new")
|
||||
run(data, "bench", "state", "http://new.test/", "retired")
|
||||
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