fix(board): live harnesses leave the owner's portfolio where they found it, measured (ready-153) - #209
Open
baron-3dl wants to merge 5 commits into
Open
fix(board): live harnesses leave the owner's portfolio where they found it, measured (ready-153)#209baron-3dl wants to merge 5 commits into
baron-3dl wants to merge 5 commits into
Conversation
…153) Every run of live-write-roundtrip.mjs and live-roundtrip-both-ways.mjs provisions a fresh, unarchived kind-30301 board and left it behind forever — 54 measured on the relay before this change (44 at the item's original measurement, more added by the dispatch that filed it). Every one showed up in the owner's `rd board` portfolio, a view that only ever grew. Each harness now runs `rd board archive <coord>` on its own throwaway board in its `finally` clause (not a happy-path line at the end), so a run that fails partway still does not leave a permanent stray. Proved by running both harnesses in full: the owner's unarchived-board count was 25 before either run and 25 after both (total board count on the relay rose by 2, both immediately archived). archive-stray-boards.mjs cleans up the pre-existing strays: a kind-only, `until`-paged relay walk (never `authors` — it silently under-returns on wss://relay.3dl.network) enumerates every board this key owns, matches the naming prefixes the harnesses (and their prior item-numbered names) have used, and archives whatever is still unarchived — the count is always derived from the live relay, never asserted. Ran once: 54/54 archived. live-portfolio.mjs was named in the item's context as a third offender but is read-only (an oracle walk + `rd list --offline`, no `rd init` anywhere) — it creates no throwaway boards and needed no change; noted as a finding rather than fixed on assumption. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed (ready-153 rework)
An archive failure in the finally block was logged as a WARNING and did not
count toward `failures`, so `process.exit(failures === 0 ? 0 : 1)` could still
exit 0 with a stray board left behind — the exact outcome the block exists to
prevent, reached silently. And nothing asserted the cleanup actually ran: the
prior version reported 18/18 and 9/9 even with the whole finally block
reverted.
Both live-write-roundtrip.mjs and live-roundtrip-both-ways.mjs now:
- count this key's unarchived boards on the relay before the run and again
after the finally block completes, and fail the run if they differ
(verified live: deleting the archive call turns a 9/9 run red — boards
39 -> 40 — while the fix keeps it 40 -> 40);
- count an archive failure as a failure instead of swallowing it.
Full live runs against wss://relay.3dl.network: 18/18 and 9/9 assertions held,
both harnesses now including a passing ready-153 board-count check.
archive-stray-boards.mjs (already on this branch) swept the remaining strays
from testing; --dry-run now reports zero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ot a claim in a commit message (ready-153 round 3) Rounds 1 and 2 argued this item's done condition — "a full run of every live harness leaves the owner's portfolio with the same board count it started with" — from a manual before/after count typed into a commit message. Nothing ran again, and nothing went red when the cleanup stopped running. Round 2 also missed live-stranger-walk.mjs, which merged as 2640e40 with `rd init` at line 590 and a `finally` clause that archived nothing. THE CONTRACT IS NOW ONE MODULE, scripts/throwaway-board.mjs, and it derives everything from the RELAY rather than from the harness's local variables: - what to archive is whatever the relay says this key owns under the board-ds the run registered. No local `coord` is consulted, so the `if (coord && rdBin)` leak is gone: a run that dies inside `rd init` — the exact case a finally block exists for — still cleans up. (Measured while building this: `rd init` appends the kind-30301 board to the LOCAL log only; the board first reaches the relay on the run's first write. So a run that died before writing has nothing in the portfolio to clean, and the guard correctly archives nothing.) - a failed `rd board archive` is a recorded failure that reaches the exit code, never a console.error the run exits 0 past. - the archived marker is READ BACK off the relay, polled to a deadline. An `rd board archive` that exits 0 without the event landing is not success. - the run is bracketed by the owner's unarchived board count, before and after, and a mismatch fails the run. That is the done condition as an assertion. EVERY HARNESS THAT RUNS `rd init` IS BOUND TO IT — live-write-roundtrip.mjs, live-roundtrip-both-ways.mjs and live-stranger-walk.mjs, the list derived from the tree, not from memory. live-parity.mjs and live-portfolio.mjs run no `rd init` and provision no board. THE TESTS, both in CI via board-ci.yml (vitest include now covers scripts/**/*.test.mjs): - scripts/throwaway-board.test.mjs, 19 tests, hermetic. The relay socket and the `rd` binary are the only fakes; the in-memory relay honours kinds/limit/until and stores an archive as a republish, so the paging walk, latest-wins, the marker read and the count bracket are all the real code. Covers: a cleanup that does not actually archive; an `rd board archive` that exits 0 without publishing; one that throws; a board published by an init that then failed; no rd binary with and without a published board; a relay that goes away mid-run; and that no filter this module sends ever carries `authors`. - scripts/harness-cleanup.test.mjs, 20 tests: for every live-*.mjs in the tree that runs `rd init`, it imports the guard, registers its board-d BEFORE init, closes the guard inside the finally clause, feeds the result into `failures`, and keeps no hand-rolled `rd board archive` that could drift. PROVED BY DELETION, as asked. Removing the `exec(...)` call from archiveBoard turns 8 of the 19 module tests red, including the happy path's board-count assertion. Removing the `guard.close(...)` line from live-stranger-walk.mjs turns harness-cleanup.test.mjs red on that file alone. Both restored, suite green. PROVED LIVE against wss://relay.3dl.network with the real rd, twice, by driving the guard through a real `rd init` + write + `rd board archive`: - working archive: 41 boards before, board provisioned and archived, 41 after, marker read back, 0 failures. - archive that exits 0 and publishes nothing: 41 before, 42 after, 2 failures reported ("the archived marker ... is NOT on wss://relay.3dl.network", "41 before, 42 after — it left a stray behind"). The deliberate stray was then swept by archive-stray-boards.mjs. Strays re-derived from the relay after all of it, not remembered: 0 unarchived boards matching [b2blive, b4359, c191live, c4359, s48f]. Full suites: web/board 927 tests green (54 files, now including the two new ones), tsc -b --noEmit clean, vite build clean, `go test ./...` green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wrong reason (ready-153 round 4) Round 3 made the cleanup contract executable. Two holes in the harness it was executed against meant the contract could still pass on a run that left a stray behind — the same defect class the invariant was filed to catch. 1. THE FAKE COULD NOT PRODUCE THE CASE THE CLEANUP EXISTS FOR. The fake relay served a published board on the very next read, so the run's own board had ALWAYS propagated by cleanup time and the archive could not fail. That hid a real defect: close() took its "what do I have to archive" snapshot ONCE, before the read-back loop. A board this run published that had not propagated by that first read was never in the snapshot, so it was never archived — and then nothing contradicted it, because the marker read-back had nothing to prove and the board-count bracket cannot see a board the relay is withholding either. 1 before, 1 after, green, permanent stray. The fake can now withhold an event it has accepted for the next N reads, and the ownership read moved INSIDE the poll loop: what to archive is re-derived every poll, so a board that appears on poll 3 is archived on poll 3 (archived once, not once per poll). A board the relay never serves inside the window now fails the run — it is indistinguishable from a board that was never published, the two have opposite consequences, and the guard is not allowed to guess. The honest cost, asserted: a run that died before publishing anything is reported too. That run has already failed for its own reason; one loud line on a red run beats a silent stray. 2. A TIMEOUT READ AS "NO BOARDS". wsReq resolved with whatever partial page had arrived when its timeout fired, so an unresponsive relay was indistinguishable from a key that owns nothing: fetchBoards reported a clean portfolio, and the board-count invariant passed vacuously — 0 before, 0 after, equal, green, on a dead relay. wsReq now REJECTS on silence, and the rejection propagates: a dead relay cannot even open the guard, so the BEFORE count is never invented. Asserted on the real wsReq against a fake WebSocket, with the distinction that has to survive tested both ways: a live relay with nothing to say resolves empty and closes the subscription; a silent one rejects, as does a page that arrives without EOSE. All seven new assertions go RED against round 3's module (verified by running this suite against c13c9f0's throwaway-board.mjs). Board suite 936 passed, go test ./... clean, typecheck and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e relay, and the sweep sees the Go strays too (ready-153 closing round) Four rounds made the tests better and left the outcome unmeasured. This round takes the measurement and fixes what it exposed. THE DONE CONDITION, ACTUALLY RUN. Every live harness in web/board/scripts was run against wss://relay.3dl.network: live-write-roundtrip (public and confidential), live-roundtrip-both-ways (public and confidential), live-stranger-walk, live-cache, live-cache --only a, live-portfolio, live-portfolio-timing. Every board-provisioning run printed "board count unchanged, 25 before and 25 after"; the portfolio measured 25 unarchived boards before the sequence and 25 after. Two of those runs FAILED partway on an unrelated rail defect and cleaned up anyway — the path the `finally` clause exists for, and the one no previous round had exercised. THE SWEEP'S SCOPE WAS ITS OWN DIFF. archive-stray-boards.mjs carried a five-prefix list that was exactly the files round 1 had touched. Walked live it covered 7 of the owner's 17 strays; 10 `ready-livetest-*` came from pkg/sync/live_relay_key_test.go and nothing in the sweep had ever looked at the Go side. The stray set is now DERIVED (stray-boards.mjs): harness sources are found by shape — web/board/scripts/live-*.mjs, plus any *_test.go gated on RD_NOSTR_LIVE_RELAY — their board-d expressions are read out of the source, and run-varying parts become wildcards. It classifies the live walk exactly: 17 strays, 25 real projects, each stray naming the file and line that made it. All 17 are archived; the owner's portfolio is now his 25 projects and nothing else. The derivation legitimately produces ^ready$, because live tests write BoardD: "ready" — so the protected list is load-bearing, and a test asserts that removing it puts this repo's own production board on the archive list. ONE WALK, NOT TWO. archive-stray-boards.mjs re-implemented the relay walk, and its copy resolved with a partial page on the 45s timeout instead of rejecting: a mute relay printed "0 unarchived stray(s) ... nothing to archive" and exited 0. It now imports fetchBoardEvents/latestBoardsOwnedBy, which reject on silence, and reads the archived markers back rather than trusting exit codes. THE READ-BACK TIMEOUT IS MEASURED. It decides a verdict — past it a board the relay has not shown fails the run — so prose was not support for it. Measured live, three trials: the board appears 139/157/170ms after the run's first write, the archived marker 148/153/193ms after `rd board archive`. (Re-confirmed too: `rd init` plus `rd relay flush` puts nothing on the relay.) 30000ms is now a stated ~150x margin over a measurement, not a guess. live-cache.mjs arrived on main after round 4 and provisions a board with no cleanup — the tree-derived harness list caught it on rebase, which is what that list is for. It is now bound, and registers its board-d inside the branch that provisions it, so `--only a` (which provisions none) does not fail for a board it never made. A second, independent detector (BOARD_D assignment) must agree with the `rd init` detector, so neither can silently shrink. NOT COVERED, and said plainly rather than left to be assumed: the Go live-relay tests still publish per-run boards with no cleanup. The sweep recognises them; that is cleanup, not prevention. Follow-up item filed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the board-list hygiene item ready-153: live test harnesses were publishing a throwaway board per run and never removing it, so the product's primary view was being filled by its own test suite.
The measurement, actually taken
Every live harness in
web/board/scriptswas run againstwss://relay.3dl.network:live-write-roundtrip.mjslive-write-roundtrip.mjs --confidentiallive-roundtrip-both-ways.mjslive-roundtrip-both-ways.mjs --confidentiallive-stranger-walk.mjslive-cache.mjslive-cache.mjs --only a(provisions no board)live-portfolio.mjs,live-portfolio-timing.mjsThe owner's portfolio measured 25 unarchived boards before the sequence and 25 after. Two of those runs failed partway and cleaned up anyway — the path the
finallyclause exists for, and the one no previous round exercised.The strays are gone
The portfolio held 42 unarchived boards, of which 17 were test-generated. All 17 are archived; it now holds the owner's 25 real projects and nothing else.
What was wrong with the previous rounds
archive-stray-boards.mjscarried a five-prefix list — exactly the files round 1 touched. It covered 7 of 17 strays. The other 10 came frompkg/sync/live_relay_key_test.go. The stray set is now derived from the tree (stray-boards.mjs): harness sources found by shape (live-*.mjs, plus any*_test.gogated onRD_NOSTR_LIVE_RELAY), board-d expressions read out of the source, run-varying parts turned into wildcards. It reproduces the live classification exactly, and every stray names the file and line that made it.0 unarchived stray(s) … nothing to archiveand exited 0. It now imports the walk that rejects on silence, and reads the archived markers back.rd board archive. 30000ms is now a stated ~150x margin over a measurement.live-cache.mjslanded on main after round 4 with no cleanup. The tree-derived harness list caught it on rebase — which is what that list is for. It is bound now, registering its board-d inside the branch that provisions it so--only adoes not fail for a board it never made. A second independent detector (BOARD_D assignment) must agree with therd initdetector, so neither can silently shrink.Not covered
The Go live-relay tests still publish per-run boards with no cleanup — 11 of the 17 strays came from them, and their next run will leave more. The sweep recognises and archives them, but that is cleanup, not prevention. Filed as ready-57f with the measured constants and the call sites.
Tests
go test ./...green;web/boardvitest 992 passed (57 files), including the newstray-boards.test.mjs, which pins the classifier against the real 42-board live walk and proves the protected-board shield is what stops the derivation from archiving this repo's own production board.🤖 Generated with Claude Code