Skip to content

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
mainfrom
work/ready-153
Open

fix(board): live harnesses leave the owner's portfolio where they found it, measured (ready-153)#209
baron-3dl wants to merge 5 commits into
mainfrom
work/ready-153

Conversation

@baron-3dl

Copy link
Copy Markdown
Contributor

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/scripts was run against wss://relay.3dl.network:

harness exit board count
live-write-roundtrip.mjs 1 (unrelated rail defect) 25 → 25
live-write-roundtrip.mjs --confidential 1 (same defect) 25 → 25
live-roundtrip-both-ways.mjs 0 25 → 25
live-roundtrip-both-ways.mjs --confidential 0 25 → 25
live-stranger-walk.mjs 0 25 → 25
live-cache.mjs 0 25 → 25
live-cache.mjs --only a (provisions no board) 0 25 → 25
live-portfolio.mjs, live-portfolio-timing.mjs 0 25 → 25

The 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 finally clause 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

  • The sweep's scope was its own diff. archive-stray-boards.mjs carried a five-prefix list — exactly the files round 1 touched. It covered 7 of 17 strays. The other 10 came from pkg/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.go gated on RD_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.
  • Two copies of the relay walk, one of them unfixed. The sweep's own REQ helper resolved with a partial page on timeout, so a mute relay printed 0 unarchived stray(s) … nothing to archive and exited 0. It now imports the walk that rejects on silence, and reads the archived markers back.
  • A 30s timeout supported only by a comment. 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. 30000ms is now a stated ~150x margin over a measurement.
  • live-cache.mjs landed 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 a 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

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/board vitest 992 passed (57 files), including the new stray-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

alice and others added 5 commits July 30, 2026 21:02
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant