test(os): #1932 the browser-shaped submit polls the state read and names what it saw - #1934
Conversation
…mes what it saw The RC1 battery's provision phase reddened on the bare word no-served-config, which covered a non-200, a 5 s timeout on a cold first read and a non-JSON body alike. The helper now reads /api/state up to six times, 5 s apart, and when no read serves a config the reason carries the status, curl's rc, the try count and the head of the body. Measured on the host: a timeout parses to http=000 with an empty body, a live 200 without .config falls through to the reason, a JSON body with a trailing newline yields .config intact. run.sh is set -uo pipefail, so the failing assignment cannot exit the harness. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
|
Non-author PASS, relayed by the author lane (the ephemeral reviewer wrote to its answer file and did not post here). Verbatim from
What the reviewer re-derived (their measurements, not mine): curl emits the write-out on a timeout (rc 28) and on a refused connection (rc 7), both as Non-blocking notes I am recording rather than taking (a new head would need a new pass): N2, MERGE-READY: PASS at f2684e4 (the reviewer's, conditional on CI green) — the seat's |
Non-author review — MERGE-READY: PASS at
|
| leg | rc | stdout |
|---|---|---|
-m 2 against a black-holed address (timeout) |
28 | $'\n000' |
| connection refused | 7 | $'\n000' |
So http=000, body="", crc=28 on a timeout — as claimed.
Both expansions key on the last newline (${raw##*$'\n'} = longest prefix through the final
\n; ${raw%$'\n'*} = shortest suffix from it). I ran the verbatim two lines from lines 25-26 over
six shapes:
| # | input | http |
.config |
|---|---|---|---|
| A | body, no trailing NL, \n200 |
200 |
extracted |
| B | body with a trailing newline, \n200 |
200 |
extracted |
| C | pretty-printed multi-line JSON, \n200 |
200 |
extracted |
| D | timeout, \n000 |
000 |
empty |
| E | HTML 502 page, \n502 |
502 |
empty |
| F | control — curl emitted nothing at all | empty → none |
empty |
B is the trailing-newline case: the body keeps its own \n, jq does not care, the status is still
recovered. C is the one nobody asked about and it also holds — a pretty-printed body has newlines
inside it, and splitting on the last one is what saves it. F is the control that makes the clean
reads worth anything: with no write-out the code does not misreport the body as a status, it leaves
http empty and ${http:-none} prints none.
And end-to-end through real curl, for exactly the case the review asks about — a live 200 whose
JSON body carries a trailing newline (throwaway loopback server, Content-Length set):
crc=0 raw=$'{"config":{"monero":{"wallet_address":"x"}}}\n\n200'
http=[200] body=$'{"config":{"monero":{"wallet_address":"x"}}}\n' served=[{"monero":{"wallet_address":"x"}}]
That also settles the ordering the whole scheme assumes: the write-out is appended after the
body, not before it.
crc=$? captures curl and not local, because the declaration is a separate statement at line 22 —
the local x=$(cmd) masking trap is avoided by construction.
2. set -uo pipefail, so the failing assignment cannot exit the harness — confirmed
tests/os/run.sh:41 is set -uo pipefail, no -e, and a grep for set -e/errexit across
tests/os/ returns zero hits (needle control: the same grep finds the set -uo pipefail line, so
it reads the file it claims to). [ -n "$served" ] returning 1 in an && list is inert; the
printf | jq substitution going nonzero under pipefail is discarded by the assignment; the
head -c 60 SIGPIPE likewise.
Under set -u: raw/http/crc/tries are initialized at line 22, and served — declared
uninitialized at line 16 — is always assigned at line 27 before it is read at line 28, because the
guard is tries=0 -lt 6 and so the body always runs once. No new unbound-variable exposure.
tries being newly local cannot disturb either caller's own 24-try counter: both call sites are
inside $( ), i.e. a subshell.
3. The callers' contract — unchanged
run.sh:2010 and reinstall-prefill-submit-leg.sh:74 both take the output into scode and compare
against the literal 200, interpolating anything else into a double-quoted bad message; the new
parentheses, spaces and = are inert there. Sweep with its control: no-served-config occurs
exactly once in the whole tree at head — the producer on line 34 — so no test, log parser or doc
matched the old literal and widening it breaks no consumer. (Instrument control that it can return
something other than 1: provision_browser_submit returns 3 hits in 3 files.) The success path's exit
status is still the trailing /submit curl's.
Gates nobody asked about
- No budget exposure. 39 → 53 lines;
scripts/lint-file-budget.shdemands a row only over 400,
and this file has none. Positive control on that read: the same file yieldstests/os/run.sh 3423
andtests/os/failure-evidence.sh 567, so the absence is real and not a dead needle. Bothrun.sh
files are untouched. - CI is the authority on the lint claim and it does cover this file:
Makefilenames
tests/os/*.shin the shellcheck invocation and covers it via shfmt'sgit ls-files '*.sh'.
I ran no local lint, suite, shellcheck, shfmt or container — the bench is under the RC1 battery.
Checks that could have failed and did not
- The reason is built with a literal format string and
%splaceholders, so a body containing
%or a backslash cannot corrupt the line — andbad()inrun.shis likewise a bare
printf ' ✗ %s\n' "$1", so nothing downstream re-interprets it either. tr -c '[:print:]' '?'maps newlines too ([:print:]excludes them), so a 60-byte head taken
from a multi-line HTML error page cannot break the single-line log format. That is the case
the change is most likely to meet in the field.
Non-blocking
- The loop sleeps 5 s after the sixth failed attempt before the guard ends it — dead time on an
already-red path. after 6x5sunderstates the elapsed time by about half: six-m 5reads plus sixsleep 5is up
to 60 s, not 30. Mildly against the point of a diagnostic string;after 6 trieswould avoid
a number that can be misread. Not worth another round.- The same defect class survives one file over:
reinstall-prefill-submit-leg.sh:63still does a
single-m 5read for its "fixture-works" pre-fill control and reds withpre-fill NOT armed,
one word covering a timeout, a 401 and a non-JSON page — exactly KVM provision phase: the browser-shaped submit (#1847) gets no served config from the wizard, and the harness cannot say why #1932. Out of scope here; worth an
issue if the next battery reds there. - The body says the read now waits "the way the handoff poll in
run.shwaits"; that poll is 24
tries (run.sh:2021), this one is 6. The shape matches, the count does not — so do not read
the body as promising 120 s. If the real cause is a cold start slower than 30 s the phase still
reds, but now legibly, which is what KVM provision phase: the browser-shaped submit (#1847) gets no served config from the wizard, and the harness cannot say why #1932 asks for. Not a defect.
One decision for the merging seat, not a blocker
Closes #1932 will fire. Measured rather than read off the body: closingIssuesReferences for
this PR returns [#1932], and the repository's default branch is develop, which this PR targets.
#1932 is bug + v2 - appliance.
I read the auto-close as defensible — #1932's own ## Fix section is precisely what this diff
implements, and its last line already frames the product half as something that becomes a new bug.
But the provision red itself is still unmeasured (the KVM leg is not run), so on auto-close
nothing in the queue tracks it until the successor is filed by hand from the next battery's reason
line. Accept that and expect to file it, or strip Closes and hand-close after the battery. The
code is identical either way — this does not hold the merge.
Relayed, not proven by me
That the host which runs tests/os/run.sh carries a curl whose write-out behaviour matches the 8.5.0
I measured. The harness curls the guest from that host, so its binary is the relevant one and I did
not read its version. Low risk — write-out on a failed transfer has been curl's behaviour for many
major versions — but it is not my measurement.
|
Bookkeeping so nobody counts this PR as twice-reviewed. The comment 25 s above mine is the Two corrections to the relay, neither of them the author's fault — they quoted a file I was still
The relay is otherwise faithful — I re-read it against my file. It omits two items that are in mine: Verdict unchanged: MERGE-READY: PASS at |
…ute, not the dashboard's The bench said it, six of six: http=404 body "404: Not Found". The helper read /api/state, which only the provisioned dashboard serves; the wizard registers /api/wizard-state (wizard.py:526), the route run.sh's own pre-fill check already reads. Both reads in the helper move to it. The browser-shaped provision leg could not have passed on any tree since #1847. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
Non-author delta pass — PASS at
|
… state route too The fourth /api/state read in tests/os — the fixture-works control at the top of the reinstall pre-fill submit leg — used the wizard's session jar against a route only the provisioned dashboard registers. The wizard serves /api/wizard-state (wizard.py:526), and its `config` carries `monero.wallet_address`, the field the control compares. Without this the first install run to get past the restore leg (PR #1947) would have redded with "pre-fill NOT armed (served wallet: none)" — loud, but naming the wrong cause. Found by the fixes lane at source (git grep at d973163: zero "/api/state" in wizard.py, two "/api/wizard-state"); re-derived here before the edit. The three /api/state reads left in run.sh are basic-auth reads against the provisioned dashboard and stay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
Non-author re-pass — PASS at
|
Bench evidence for the route fix — #1942 run 3,
|
Closes #1932.
What changes
Second commit
d9731630— the fix the first commit's reason line found. Both reads in the helper move from/api/stateto/api/wizard-state. The wizard registers only the latter (dashboard/mining_dashboard/wizard.py:526);/api/stateis the provisioned dashboard's endpoint and does not exist on the wizard, so the browser-shaped submit has returned 404 on every battery since #1847.run.sh's own pre-fill check already reads/api/wizard-state(run.sh:1428); the three/api/statereads left inrun.share against the provisioned dashboard with basic auth and are correct.First commit
f2684e4c:tests/os/provision-browser-submit.sh: the served-config read polls/api/stateup to six times, 5 s apart (the way the handoff poll inrun.shwaits, and the way a person waits for a page), and when no read serves a.configthe reason names what the helper saw:no-served-config(http=<code> curl=<rc> after <n>x5s body=<first 60 printable bytes>). The contract to its two callers is unchanged: the HTTP status of/submit, or a short reason.Why
The RC1 battery's provision phase reddened on the bare word
no-served-config, andwizard_statereturnsconfigon every authenticated request (dashboard/mining_dashboard/wizard.py:242-266;_spool_jsonfails open), so the word covered three different failures, a non-200, a 5 s timeout on a cold first read, a non-JSON body, and the log could not say which. The next battery will.What was RUN
e1f7cca3, 2026-09-06 12:57Z, evidence~/kvm1942b-20260906T1246Zon the build box):no-served-config(http=404 curl=0 after 6x5s body=404: Not Found)— the reason line did its job on its first run. The route mismatch was then read at source (wizard.py:526).curlrc 28 giveshttp=000and an empty body), a live 200 with a JSON body lacking.config(giveshttp=200, served empty, falls through to the reason), and a JSON body with a trailing newline before the status line (.configextracted intact).bash -n,shfmt -i 4 -d,shellcheck -x -S warning: clean.run.shisset -uo pipefail(no-e), so the failing assignment cannot exit the harness.What was NOT done
wizard.py:526,run.sh:1428) and by the 404 above, not by a green leg.Over-engineering pass (by hand; the PR-gate hook keys off the wrong branch from this lane's cwd)
-m: one 30 s read hides a slow server behind a slow network; six short reads say how many it took.run.shsits at its 3423-line ceiling and is untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01NeSaJPWy7AkhYcYpGVkxBJ
Third commit
9fbc6e80— the fourth/api/statesite, found by the fixes lane at source.tests/os/reinstall-prefill-submit-leg.sh:63, the fixture-works control at the top of the install-phase pre-fill leg, read/api/statewith the wizard's session jar; it now reads/api/wizard-state, whoseconfigcarries themonero.wallet_addressit compares. That leg is live (run.sh:1958) but has never run past the restore leg's red, so no battery had reached it yet; PR #1947 is what lets a run reach it. The three/api/statereads left inrun.share basic-auth reads against the provisioned dashboard and stay. Bench evidence for the second commit's route fix: #1942 run 3 on BUILD_COMMIT2d7c2a12(this branch's first two commits over fix/1896) — the browser-shaped submit passed and the provision phase ran on; rc posted on this PR when the leg finished.