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:
+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
|
||||
|
||||
Reference in New Issue
Block a user