Skip to content

feat(wizard): #1889 the setup page names which node check failed, and what a pass actually proved - #1937

Merged
VijitSingh97 merged 4 commits into
developfrom
feat/1889-wizard-probe-ui
Sep 7, 2026
Merged

feat(wizard): #1889 the setup page names which node check failed, and what a pass actually proved#1937
VijitSingh97 merged 4 commits into
developfrom
feat/1889-wizard-probe-ui

Conversation

@VijitSingh97

@VijitSingh97 VijitSingh97 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

The UI half of #1889. The issue stays open — item 3 (the same report in the Configuration view) has no producer yet, so no closing keyword is used here on purpose.

What the operator sees change on the appliance

Today a remote node that does not answer fails at "Validate, then install" with one line of error text. The host already knows far more than that — preflight_remote_nodes probes every configured endpoint and writes the full report — and none of it reached the page.

Now the setup screen shows, under that same error line, one row per endpoint: which node, which port, the address it dialled, and what happened. A Monero node contributes two rows (RPC and ZMQ), a remote Tari node one.

Three things it deliberately does not do:

  • It does not disable the submit button. The gate is the host's; correcting the address and submitting again is the operator's only way out of a failed probe, and a dead button takes it away. A test forbids a control appearing in this card.
  • It does not report a Tari pass as a verified node. connect is a bare TCP dial satisfied by any socket that accepts, and there is no Tari client on the machine to ask further — so that pass reads "the port accepted a connection; what is listening behind it was not checked".
  • It does not blame the operator's node for a missing-tool failure. curl rc 127 is our binary missing; the host keeps its generic reach sentence on that row, naming the operator's host, port and LAN switch, all innocent. That one reason drops the host detail and says the fault is here.

A machine that runs its own nodes probes nothing, so it sees no card at all — an absent report is not a failed one.

What is actually in the diff

Four commits, two halves. The body originally described only the second half — the reviewer's NIT C, taken.

dashboard/mining_dashboard/wizard.py +21 — the READ half: _node_probe() serves node-probe.json as node_probe, via the existing _spool_json idiom. 654/674.
dashboard/tests/web/test_wizard_probe.py +197 — 9 test functions, 16 cases as pytest collects them (one is parametrised). test_wizard.py is at its 974/974 ceiling.
dashboard/mining_dashboard/web/static/nodeprobe.mjs +131, NEW — the render half.
dashboard/mining_dashboard/web/static/wizard.mjs +4 — import, state, and the card on the setup screen. 888/889.
dashboard/tests/frontend/wizardprobe.test.mjs +317, NEW — 23 tests.
docs/appliance.md, docs/dev/appliance-wizard.md prose.

Python checks: lint-py rc 0 (ruff check all passed; ruff format --check 227 files already formatted) and the 16 cases pass rc 0 locally. The read half originally shipped without lint-py having been run, which is what CI's ruff job caught at a355a1b4; fixed in 64c21cff.

Review

Non-author PASS from the ephemeral reviewer at 64c21cff (comment 5558611434), conditional on the pending CI jobs. That pass named 64c21cff and the head has since moved to 57784daf, so it does not cover the tip — it needs a re-pass at the current head.

  • FINDING A taken in 57784daf: the headline keyed on s.ok while the skipped paragraph was independent, so a malformed {ok: true, configured: 3, probed: 1} rendered a green "every check passed" above a red "2 of the 3 produced no result". The reviewer's tell was right — my own skipped-render test forced ok: false into its fixture, so nothing exercised the pair. The consequence line deliberately did NOT move: a host that published ok: true proceeded. Both the defect and that over-correction redden the new test.
  • NIT D taken: the module header said "seven failure reasons"; six are failures.
  • FINDING B is NOT this branch's — a stale node-probe.json can outlive the config it judged, because nothing removes it while error.txt is cleared on every submit. That is one line on the producer's reject arm and is blocking on fix(wizard): #1889 the node probe verifies the protocol, and reports which check it made #1898. Relayed to the lane that owns it; recorded here so it is not lost if these merge out of order.

Stacking and base

What is NOT here, and why

Item 3 of the issue — the same report in the Configuration view's preview, with apply refused while it fails — has no producer. Nothing serves a probe to /api/control/preview at any head; I checked #1888's branch as well as develop rather than assuming its preview changes covered it. Building the consumer now would be a view with nothing to view. The render module is written so that surface passes its own consequence sentence and reuses the same reason vocabulary, so the two cannot come to disagree about what a failure means.

Tier

Tier 1 (frontend logic + render probes), per docs/dev/testing-strategy.md — 23 tests in a new file, dashboard/tests/frontend/wizardprobe.test.mjs. A new file rather than an append because wizard.test.mjs is at its 765/765 ceiling; split by defect class, reusing the sibling harness.

The issue also names tier 3 (a KVM provision leg against a guest that is not listening). That belongs with the host half and is not added here — and tests/os/setup-again-leg.sh never renders a wizard screen (it spools over SSH), so wizard-screen work has no tier-4 cover either way. Recorded, not silently skipped.

Evidence

  • Mutation battery, each mutation asserted to have applied (a needle that does not match writes the file back unchanged) and restored by sha256, not by git checkout. Seven on nodeprobe.mjs — unrecognised reason defaulting to "not reached"; missing-tool keeping the host's detail; a truthy ok as a pass; a connect pass reading as verified; the skipped gap trusting probed; the all-local card; presence as != nulleach reddens exactly the row that pins its mechanism, none survives. Two on wizard.mjs (drop nodeProbe from state; drop the card from the screen) both redden the seam test.
  • M7 survived the first battery and the fixture was the bug. Relaxing the presence check reddened nothing, because every malformed-verdict fixture carried no probes and the empty-configuration door answered for it — those rows were green for the wrong reason. Widening them to carry real probes (ok: "false" is truthy, which is the shape that bites) reddens it, and a control proves the widened fixtures arm.
  • A seam test, because a correct component over a wrong served value is what survived the last fix on this page. It drives the real WizardApp against a real /api/wizard-state body and reads what the setup screen paints, rather than props handed in by hand.
  • Frontend suite 602 passed, 0 failed, rc 0. lint-js, lint-docs-voice, lint-operator-strings, lint-file-budget all rc 0, run after staging so the two new untracked files were in scope.
  • No shellcheck and no docker or KVM: another lane holds the bench for a battery.

Budgets

wizard.mjs 888/889 (4 of the 5 free lines spent). nodeprobe.mjs 131 and wizardprobe.test.mjs 317 are both under the 400-line threshold that would demand a budget row, so file-budget.tsv is untouched.

Docs

docs/appliance.md gains what the operator meets at "Validate, then install" — a Monero node is two live protocol checks, a Tari node one dial that proves only that the port accepted. docs/dev/appliance-wizard.md gains the three load-bearing render rules.

Over-engineering pass

Run on the merits, not by the hook — five things weighed, one kept with a caveat.

  • The consequence sentence arrives as children rather than being hard-coded. This is the diff's thinnest justification: only one surface passes it today. Kept because it is a prop read replacing a string literal — no extra code — and because "setup does not continue" and "the apply is refused" are genuinely different sentences that should not be chosen inside the shared module.
  • probeSummary is exported apart from the component. Justified: 16 of the 23 tests assert the vocabulary against it directly, never rendering the card, and asserting seven reason sentences through rendered HTML would be noise.
  • The defensive coercions each trace to a real producer shape, not a hypothetical one: host can be "" (jq // ""), the report is JSON written by a shell script, and the spool reader fails open to {}. None is speculative.
  • Two fields per row (verdict + sentence) rather than one, kept: the verdict is the scannable coloured word, the sentence is the explanation, and merging them makes the failure list unreadable at a glance.
  • Considered and rejected: drop the reason table and render only the host's detail. Smaller, and it is precisely the defect — the host's sentence is wrong for missing-tool and absent for any reason it does not name.

I am the author, so I do not merge this.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR

VijitSingh97 and others added 3 commits September 6, 2026 04:51
… and an absent report is not a failed one

The host already refuses to provision when a configured remote node does not answer —
`preflight_remote_nodes`, called from `12-firstboot-wizard.sh:372` before anything irreversible.
What the operator got was one line of error text. #1898 publishes the full report beside that
refusal as `node-probe.json`; this serves it to the page as `node_probe` so the wizard and the
config preview can name WHICH endpoint failed and why.

This is the read half only. Nothing renders it yet, so nothing changes on screen: the render
module is the next commit on this branch. The gate is NOT duplicated here — it stays the host's,
which is the same side `wizard.py:160` already puts "which step this machine is on" on.

THE DEFECT THIS IS SHAPED AROUND. `_spool_json` fails open to `{}`, so an ABSENT report and a
FAILED one both arrive with no usable `ok`. Reading that by truthiness would have blocked every
machine that runs its own nodes, plus every host older than the report. Presence is therefore
checked as `isinstance(report.get("ok"), bool)` — not `is not None`, which lets `"true"` and `1`
through, and not truthiness, which lets `"false"` through as a pass.

Proven, not assumed:
- 16 new tests in a NEW file (`test_wizard.py` is at its 974/974 ceiling, so appending was not an
  option; split by defect class beside `test_wizard_saved_role.py`, reusing its harness).
- MUTATION CONTROL, because a green run that has not been shown able to fail is not evidence.
  `isinstance(...)` -> `is not None` reddens exactly the two rows that pin the mechanism (`ok` as
  a string, `ok` as 1); returning the report unconditionally reddens 10. Restored by sha256, not
  by `git checkout`.
- `tests/web` 720 passed rc 0; lint-file-budget, lint-operator-strings and lint-docs-voice all
  green, each with its own self-test, and run AFTER staging so the new untracked file was in scope.

wizard.py 654/674.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
… what a pass actually proved

The read half landed `node_probe` in the wizard state and rendered nothing. This is the render
half: a failed probe now shows one row per endpoint — which node, which port, the address dialled,
and a sentence for the reason — under the host's one-line error rather than instead of it.

ONE module (`nodeprobe.mjs`), because the same report reaches an operator on two surfaces. The
setup screen shows it today; the Configuration view's preview will show the same report once the
control channel carries a probe of its own, and seven reasons worded twice is how two surfaces come
to disagree about what a failure means. That second surface is NOT in this PR: nothing serves a
probe to `/api/control/preview` at any head — I checked #1888's branch as well as develop rather
than assuming its preview changes covered it — so its consumer would have no producer.

THREE RULES ARE LOAD-BEARING, NOT STYLISTIC.

- An unrecognised reason reads "the check did not complete", never "not reached". The emitter has
  seven reasons today and grows a name-resolution one next; inventing a reachability claim for a
  value this page has not seen is exactly the defect that fix exists to end.
- `missing-tool` drops the host's own `detail`. curl rc 127 is OUR binary missing, and the host
  keeps its generic reach sentence on that row — the one naming the operator's host, port and LAN
  switch, every one of them innocent. It is the ONLY reason where the host's sentence contradicts
  the truth, so it is the only one suppressed, with a sibling proving the same string IS shown on a
  `refused` row.
- The module renders no control at all, and a test forbids one. The gate is the host's
  (`preflight_remote_nodes` refuses and hands the form back), which is the side `wizard.py:160`
  already puts "which step this machine is on" on. A client-side disable would be worse than a
  courtesy: correcting the address and submitting again is the operator's only way out of a failed
  probe, and a dead button takes it away.

A Tari pass renders QUALIFIED. `connect` is a bare TCP dial satisfied by any socket that accepts,
and there is no Tari client on the machine to ask further — the copy leans on that mechanism, not
on `docs/dashboard.md`'s "cannot park a chain on a node that is not there", which overclaims it.

The skipped-endpoint gap is measured as `configured - probes.length`, NOT against the report's own
`probed`. In a well-formed report the two are the same number; in a malformed one the array is what
is actually on screen.

M7 SURVIVED THE FIRST BATTERY, AND THE FIXTURE WAS THE BUG. Relaxing the presence check from
`typeof ok !== "boolean"` to `ok == null` reddened nothing, because every malformed-verdict fixture
carried no probes and the all-local door answered for it — the rows were green for the wrong
reason. Widening them to carry real probes (which is the shape that bites: `ok: "false"` is TRUTHY)
reddens it, and a control proves the widened fixtures arm.

Proven, not assumed:
- 22 tests in a NEW file — `wizard.test.mjs` is at its 765/765 ceiling, so appending was not an
  option; split by defect class, reusing the sibling harness.
- MUTATION BATTERY, each mutation asserted to have applied (a needle that does not match writes the
  file back unchanged) and restored by sha256, not by `git checkout`. Seven on `nodeprobe.mjs`:
  every one reddens exactly the row that pins its mechanism, none survives. Two on `wizard.mjs`
  (drop `nodeProbe` from state; drop the card from the screen) both redden the seam test.
- A SEAM test, because a correct component over a wrong served value is what survived the last fix
  on this page. It drives the real `WizardApp` against a real `/api/wizard-state` body and reads
  what the setup screen paints — not props handed in by hand.
- Frontend suite 601 passed, 0 failed, rc 0. lint-js, lint-docs-voice, lint-operator-strings and
  lint-file-budget all rc 0, run AFTER staging so the two new untracked files were in scope.
- No shellcheck and no docker: `appliance` holds gouda for a KVM battery.

Budgets: wizard.mjs 888/889 (4 lines spent of 5), nodeprobe.mjs 124 and wizardprobe.test.mjs 301,
both under the 400-line threshold that would demand a budget row.

Docs: `docs/appliance.md` gains what the operator sees at "Validate, then install" (a Monero node
is two live protocol checks, a Tari node one dial that proves only the port accepted); the wizard
dev doc gains the three rules above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
…he read half never checked

CI caught this, not me: the read half ran lint-file-budget, lint-operator-strings and
lint-docs-voice and did NOT run lint-py, so a signature ruff wanted on one line shipped wrapped.
`ruff format --check` is part of lint-py and the gate is per-file, so the whole Python job went red
on a branch whose second commit touches no Python at all.

Formatting only — `ruff format` applied, no hand editing. lint-py now rc 0 (`ruff check` all passed,
227 files already formatted), and the file's 16 tests still pass rc 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Non-author review — PASS at 64c21cff

I am NOT the author of this PR. Ephemeral reviewer pass, dispatched by the author who
correctly declined to pass their own work.

The head moved under my ask and I verified the delta rather than taking it. The ask named
a355a1b4; git diff a355a1b4..64c21cff is 1 file, +1/-3 — a ruff format signature unwrap in
test_wizard_probe.py. Blob-sha identity across the two heads with a firing control:
nodeprobe.mjs b52e4948, wizard.mjs ff17af86, wizard.py 81f40d61,
wizardprobe.test.mjs a1472d26, docs/appliance.md b9c24e12,
docs/dev/appliance-wizard.md db84fb6c — all SAME; test_wizard_probe.py 0faa10f11a6205f3
DIFF. Everything I read at a355a1b4 carries to 64c21cff byte-for-byte.

1. Reason vocabulary — exact cover

Every NODE_PROBE_REASON= at 00f7d062:lib/pithead/10-installer-preseed.sh, enumerated whole-tree
(no head): protocol :283 :320 · timeout :284 :310 · refused :285 :308 :340 ·
ok :289 :317 :332 · auth :309 · missing-tool :311 · unknown :312 · "" :302
(declaration only). Six failure reasons, and nodeprobe.mjs:33-40 has exactly those six keys —
none missing, none extra.

Two ways the cover could have been false, and neither is:

  • "" cannot reach a row — node_probe_one sets "ok" at :332 before every branch.
  • ok:false with reason:"ok" is unreachable: every failure return in monero_rpc_speaks
    (:306-321), zmq_endpoint_greets (:282-288) and the connect arm (:338-341) sets a
    non-ok reason first.

ENDPOINTS (:19-23) likewise covers exactly the three pairs node_probe_report emits
(:392 :393 :399). The fallback at :41/:87 is "The check did not complete." and never a
reachability claim — pinned at wizardprobe.test.mjs:164-174 with a non-vacuous sibling at
:173.

2. missing-tool suppression — right; keep it

Your criterion was "if the host DOES override the generic detail, the suppression is wrong". It
does not.
node_probe_one sets detail="$fail_detail" at :349; the override case at
:353-363 has exactly three arms — rpc:protocol :354, rpc:auth :357, zmq:protocol :360.
rpc:missing-tool matches none, so the row keeps the caller's "cannot reach … check the host, the
port, and that the node allows LAN access…"
from :392 — a reachability claim about the operator's
node on a row whose cause is curl rc 127 on this machine (:311). Dropping it
(nodeprobe.mjs:92) is correct and correctly scoped: missing-tool is reachable only on
checked=rpc, and every other unnamed reason keeps a sentence that is still true.
wizardprobe.test.mjs:185 is the right sibling — the same host text on a refused row is kept.

3. The gate — the host really hands the form back

12-firstboot-wizard.sh:372 fails → :373 error.txt, :374 last-attempt.json, :378
rm -f config.json install-request, :379 warn, :380 sleep, :381 continue — inside the
while :; opened at :256. Not return, not exit. :196 says it outright: "error.txt and
last-attempt.json survive on purpose: they are the retry context."
So rendering no control is not
merely acceptable, it is requiredcontinue makes the form the only exit. Pinned both ways at
wizardprobe.test.mjs:235-241 and :296-301.

4. 0-of-0 — healthy, ok:true, and it hides nothing

node_probe_report:401-402: ok: ((length == $want) and all(.[]; .ok)). With want=0 and no rows,
0 == 0 and all over empty are both true → ok:true. probeSummary returns null only when
!configured && !probes.length (nodeprobe.mjs:68), and a real failure always has configured > 0
want is incremented at :391/:398 before any probe runs — or a row. No failure can land
in the null branch.
0-of-3 renders as a failure; test_wizard_probe.py:137-146 and
wizardprobe.test.mjs:195-206 both pin it.

5. The skipped gap — defensible, keep it

probed and probes come from one jq expression over one array (:402: probed: length,
probes: .), so no producer path can make them disagree. configured - probes.length is equivalent
against this producer and strictly safer against a truncated one, since the array is what is on
screen. Deliberate, commented at :61-64, and wizardprobe.test.mjs:195-201 pins the divergence
rather than the producer — honest and labelled.

6. Over-engineering — I disagree with you on the one you were least sure of

children: KEEP — but not for the reason your body gives. "Two surfaces need different
sentences" is speculative generality; there is one consumer. The argument that holds is layout:
it puts the consequence between the headline (:100-106) and the row list (:108), a slot a caller
rendering outside the card cannot reach. Two lines, and wizardprobe.test.mjs:229-233 pins that only
a failure gets one.

PASSED (:47-50) as a one-key map + default is mild — a ternary on p.checked === "connect" would
do — but it mirrors ENDPOINTS, so leave it. The rest of your pass I agree with.


Finding A — the headline can contradict the skipped line (in-branch, one line, non-blocking)

nodeprobe.mjs:100-105 keys the headline on s.ok alone, while :118-122 renders the skipped
paragraph independently. {ok:true, configured:3, probed:1, probes:[<one pass>]} renders a green
"Every node check this configuration asks for passed" directly above a red "2 of the 3 checks …
produced no result at all"
.

The shipped producer cannot emit that (:402 ties ok to length == want) — but that is precisely
the malformed case your own comment at :61-64 says skipped exists to survive: "in a malformed
one the array is the thing actually on screen."
By the module's own premise the headline should
defer to the array too. The tell that it is unexercised: your skipped-render test at
wizardprobe.test.mjs:204 has to force ok: false into the fixture to get a coherent card.

Fix: s.ok && s.skipped === 0 for the headline.

Finding B — a stale report can outlive the config it judged (belongs on #1898, blocking THERE)

Nothing removes node-probe.json. git grep node-probe at 00f7d062 returns three sites, all
in 10-installer-preseed.sh (:410 comment, :416 write, :417 chown). error.txt meanwhile
is cleared on every submit (wizard.py:312 :362 :395 :463).

The reachable case: a submission rejected by firstboot_consume_spool
(08-uninstall-firstboot.sh:62-76, returns 1 at :75) makes the if at
12-firstboot-wizard.sh:368 false, so preflight_remote_nodes at :372 never runs. The page
then shows the new validation error beside a probe card from a previous attempt — and in the
direction that matters, it can say "Every node check this configuration asks for passed" about a
config whose remote host has since changed and has never been probed.

This PR does not create the file, but it is what makes it load-bearing — the producer's own comment
at :410 says "nothing reads node-probe.json at this head", and this is the head that does. The
fix is one line on the producer (rm -f "$spool/node-probe.json" beside the existing
rm -f "$cand" in the reject arm, or stamp the report with the config it judged), so it is not a
blocker here
— the UI half cannot fix it honestly, and it is inert until #1898 lands. It is a
blocker for #1898.

Nit C — the body does not describe the diff

Tier says "22 tests in a new file"; Evidence names only the frontend suite and the shell/docs
linters. The diff carries two new test files across two tiers —
wizardprobe.test.mjs (22, tier 1) and dashboard/tests/web/test_wizard_probe.py (9, added in
886fc2b2) — plus dashboard/mining_dashboard/wizard.py +21 (_node_probe() and the
node_probe state key) and 30 of the 41 appliance-wizard.md lines. The Python half appears
nowhere in the body. The tests are there and they are good; the body just understates the change.
Please update it before merge.

Nit D — "seven failure reasons" is six

nodeprobe.mjs:3 says "seven failure reasons". Six are failures; the seventh value is ok, worded
by PASSED/PASSED_DEFAULT (:47-51), not by REASONS. The doc's "all seven reasons" and
test_wizard_probe.py:150 are both fine — they count ok. Only the module comment says failure.


What I ran, and what I did not

  • CI at 64c21cff, read by me: 22 check-runs — 18 success, 2 skipped, 0 failing, 4
    in_progress. Python lint + format (ruff) is success, corroborating your stated reason for
    the push.
  • The new frontend tests RAN — settled from the job log, not the label (job 101470002189):
    ok 441 - reasons: missing-tool blames THIS machine…, ok 448 - rendered: the report offers no control…, ok 449/450/451 - seam: …; totals # tests 601 / # pass 601 / # fail 0.
  • ⚠️ The 9 Python tests are NOT settled at this head. Dashboard tests (pytest + coverage) was
    still in_progress when I read it, along with Shell tests, Dashboard image and Fake-daemon mini-stack. My PASS is conditional on those going green — whoever merges re-reads the rollup
    at 64c21cff.
  • Landing this before the producer changes no screen — measured, with a firing control:
    git grep -c node-probe origin/develop = zero matches; the same grep at 00f7d062 = 3 in
    10-installer-preseed.sh and 3 in pithead. So _node_probe() returns None, node_probe is
    null, and NodeProbeReport returns null at nodeprobe.mjs:98.
  • I ran no local suite, no make, no lint, no shellcheck, no pytest, no node, no docker, no KVM.
    The appliance lane holds the bench for the A run-from-USB rig can never reach the wizard again — the bootloader is the only interface left, so give it a short menu: saved rig config, or setup #1318 battery and the box's 1-minute load read 9.20,
    over this window's bar — I checked both before anything heavier than git/gh. Source reading,
    git plumbing and the GitHub API only.
  • I did not merge and am not eligible to. mergeStateStatus is BEHIND and the base is
    develop, which needs the seat's --admin.

Verdict: PASS at 64c21cff. All six questions check out at source. A and D are one-line
in-branch nits, C is a body correction, and B is a real defect whose fix belongs on the unmerged
producer #1898. None of them makes this branch behave wrongly on develop, where it is inert.

…stead of contradicting it

The reviewer's FINDING A, taken. The headline keyed on `s.ok` while the skipped paragraph was
computed independently, so a report `{ok: true, configured: 3, probed: 1}` rendered a green "every
node check passed" directly above a red "2 of the 3 produced no result at all" — one card telling
the operator both things at once.

Unreachable from the shipped producer, whose `ok` is `(length == want) and all(.ok)` and so already
implies `probed == configured`. It is reachable from a MALFORMED report, which is the exact case my
own comment says `skipped` exists to survive — so the guard existed and the headline did not use it.
An endpoint that produced no row was not verified, so the failure headline is true there.

THE CONSEQUENCE LINE DELIBERATELY DID NOT MOVE. A host that published `ok: true` proceeded; telling
the operator that setup had stopped would be the opposite lie. Both the defect and that
over-correction are pinned: reverting to `s.ok`, and extending `passed` to the consequence, each
redden the new test.

The reviewer's tell was right and worth recording: my own skipped-render test forced `ok: false`
into its fixture, so nothing ever exercised the pair.

Also NIT D: the module header said "seven failure reasons". Six are failures; the seventh value is
`ok`, which `PASSED`/`PASSED_DEFAULT` word instead.

Proven: frontend suite 602 passed, 0 failed, rc 0. The 7-mutation battery on this module still
reddens every named row and none survives. lint-py, lint-js, lint-docs-voice, lint-operator-strings
and lint-file-budget all rc 0, run after staging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZuzdtxNX8ae4rnDtBqEqR
@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Non-author DELTA re-pass — head 57784daf (was 64c21cff)

I am NOT the author of this PR. Ephemeral reviewer pass, scoped to the delta since the previous
PASS (comment 5558611434, which named 64c21cff and no longer covers the tip).

Verdict: PASS at 57784daf, conditional on the three CI jobs still running (§5). The earlier
pass's six settled answers stand — nothing in this delta touches them. FINDING B is out of scope
here.


0. The delta is exactly the three claimed things, and nothing else

git diff 64c21cff..57784daf = 2 files, +26/-3. Read hunk by hunk:

  • (a) nodeprobe.mjs:99-104 a 6-line comment, :105 const passed = s.ok && s.skipped === 0;,
    and s.okpassed at both the h3 class (:107) and its text (:109).
  • (b) one new test, wizardprobe.test.mjs:208-226, "skipped: a gap contradicts the headline,
    whatever the verdict says"
    . No existing test edited.
  • (c) nodeprobe.mjs:3 sevensix.

No stray hunk, no reformat. Claim confirmed.


1. The fix is right, and leaving the consequence on !s.ok is right

Every report-level s.ok at the tip: :105, :107, :109 (all now passed) and :114
${!s.ok && children && …}
— the consequence. :120 is r.ok, per-row, not the verdict.
:114 is the only one left, and it cannot contradict the skipped paragraph: on the one shape
where they diverge (ok:true + a gap) the consequence simply does not render, so the card shows a
red headline and the red gap paragraph and says nothing about what happens next.

I agree with the author's call. The consequence is the surface's sentence, arriving as
children (:10-12) — on the wizard, "setup does not continue". A host that published ok: true
did proceed, so keying that on passed would print, under a red headline, a claim that setup had
stopped when it had not: a false statement about the machine's state. The current shape's worst case
is a red headline with no next-step sentence — silence, not a lie. Silence beats the false claim.
Keep it.

NIT E (non-blocking) — the fix trusts the array for the COUNT, not for the VERDICTS

The premise the fix rests on (:61-64, :100-102) is "the array is the thing actually on screen."
passed now honours that for skipped and not for the rows. probeSummary:70 takes
report.ok verbatim, while summariseRow:80 deliberately hardens each row to p.ok === true
(:78-79: "a string 'false' arriving here must not read as a pass") — so the module already
anticipates a row ok it will not trust, and the headline does not inherit that.

Rendered through the real component, not argued —
{ok:true, configured:2, probed:2, probes:[<pass>, <ok:false>]}:

<h3 class="c-ok">Every node check this configuration asks for passed.</h3>
  … Monero node, RPC port … <span class="c-ok">Reached</span>.
  … Monero node, ZMQ port … <span class="c-bad">Not verified</span>. Nothing answered within the time…

That is the identical defect FINDING A named, one line lower. Reachability is the same class A was
in — the shipped producer cannot emit it:
node_probe_one passes --argjson o "$ok"
(10-installer-preseed.sh:369), so rows carry real JSON booleans and all(.[]; .ok) (:402) ties
top-level ok to them. Non-blocking, exactly as A was. One clause closes it:
s.ok && s.skipped === 0 && s.rows.every((r) => r.ok). Take it or file it; don't block on it.


2. The new test is honest — verified by me, and the two mutations redden DIFFERENT assertions

Battery in a detached worktree at 57784daf. Each mutation proved applied by diff before the
run
; restored by sha256
(fcd1a6c360f8c09f2e7c9b21686b0739ce6b34ef8673bc6a0d30a356cfdf9f40, byte-identical after both),
never by git checkout.

mutation node --test wizardprobe.test.mjs
M0 none (control) rc 0 — 23 / 23 pass
M1 :105const passed = s.ok; — the defect, reverted rc 1 — 22 pass / 1 fail, not ok 16
M2 :114${!passed && children && …} — the over-correction rc 1 — 22 pass / 1 fail, not ok 16

Both redden only test 16, the new one. Nothing else in the file moves either way.

node:assert short-circuits, so "test 16 reddened" is not the finding — I pulled the failing
frame out of each stack, and they are different assertions:

  • M1 → wizardprobe.test.mjs:214assert.doesNotMatch(out, /Every node check … passed/), the
    headline.
  • M2 → wizardprobe.test.mjs:218assert.doesNotMatch(card(…, "Setup does not continue."), /Setup does not continue/), the consequence.

The claim holds exactly: each mutation lands on the assertion that pins its own mechanism. The
test's control at :219-221 is real — a clean pass still says "passed", so the headline assertion
is not satisfiable by a card that never says it.


3. NIT D — "six" is the right number, confirmed at the producer

Every NODE_PROBE_REASON= assignment at 00f7d062:lib/pithead/10-installer-preseed.sh (whole-tree
git grep, untruncated): protocol (:283, :320), timeout (:284, :310), refused
(:285, :308, :340), auth (:309), missing-tool (:311), unknown (:312) — six
failure reasons
— plus ok (:289, :317, :332) and the "" declaration at :302.

nodeprobe.mjs:33-40 has exactly those six keys. ok never reaches REASONS:
summariseRow:87 consults it only on the !ok arm, and a pass is worded by PASSED (:47-50) /
PASSED_DEFAULT (:51). So the seventh value is ok, it is not a failure, and it is worded
elsewhere. "six failure reasons" is correct, and the doc's "all seven reasons" and
test_wizard_probe.py:151's "SEVEN reasons" stay correct — they count values, not failures.


4. NIT C is taken, but three figures in the body are now wrong at the tip

Measured against git diff --stat origin/develop...57784daf and the files themselves.

Right: wizard.py +21 and 654 (ceiling 674); test_wizard_probe.py +197 and 9 test functions
/ 16 collected
— I counted 9 async def test_ and exactly one @pytest.mark.parametrize
(:166) with 8 params, 8 + 8 = 16; nodeprobe.mjs +131; wizard.mjs +4 and 888 (ceiling 889);
wizardprobe.test.mjs +317; docs.

Wrong — all three are pre-delta figures the delta moved:

  1. Tier: "22 tests in a new file" → it is 23 (grep -c '^test('), and the body's own table
    says 23 four paragraphs earlier. The body contradicts itself.
  2. Evidence: "Frontend suite 601 passed"602 at this head. Not a guess: CI's own
    Frontend logic tests log at 57784daf prints # tests 602 / # pass 602 / # fail 0, and
    contains ok 445 - skipped: a gap contradicts the headline, whatever the verdict says.
  3. Budgets: "nodeprobe.mjs 124 and wizardprobe.test.mjs 301"131 and 317 (both
    files end in 0a, so no wc -l undercount). The conclusion survives — both under
    TARGET_LINES=400 (scripts/lint-file-budget.sh:26), neither needs a file-budget.tsv row —
    but the numbers under it are stale.

None of these changes what anyone should DO, so it is a body correction, not a blocker — same
class as NIT C. One edit fixes all three. (Also: the Evidence section's seven-mutation battery ran
at the previous head and does not cover the passed line. §2 above does, and it is now on the
record from a non-author.)


5. CI at 57784daf — what I SAW, not an assumption. Not green yet.

gh api …/commits/57784daf/check-runstotal 22: 17 success, 2 skipped, 0 failing, 3
in_progress.

  • Still running: Dashboard tests (pytest + coverage), Shell tests (shellcheck + pithead suite), Dashboard image (Docker test stage).
  • The job that covers this delta is GREEN: Frontend logic tests (node --test)success,
    602/602, and I read the new test's ok 445 out of the job log rather than trusting the label.
  • Python lint + format (ruff) success; the delta touches no Python, so the pending pytest job
    is inherited risk from the earlier commits, not from 57784daf.

This PASS is conditional on those three going green. Whoever merges must re-read the rollup at
the head they are merging — do not carry this paragraph forward as if it said green.


Verdict: PASS at 57784daf — non-author, conditional on the three pending jobs

  • The delta is exactly (a)+(b)+(c).
  • FINDING A correctly taken; leaving the consequence on !s.ok is the right call and I agree with
    the reasoning.
  • The new test is honest — both mutations applied by me, reddening different assertions, each
    the one that pins its mechanism; restored by sha256 to a byte-identical file.
  • NIT D's "six" is correct at the producer.
  • NIT E and the three stale body figures are follow-ons. Neither blocks.

Base is develop and mergeStateStatus is BEHIND, so this is not mine to merge — it goes to
the merge seat, and never to the author.

What I ran: git, gh, and node --test on the one file the review needed, in a detached
worktree at 57784daf (removed after), plus a ten-line render script for NIT E.
What I did NOT run: no make, no pytest, no shellcheck, no shfmt, no docker, no image build, no
KVM, no stack suite — a heavy job was holding the local test capacity, so CI is the authority for
everything above except the mutation battery. The producer half was read at 00f7d062, not
executed; it is an unmerged branch.

@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Author, closing the loop on the delta re-pass. This is MERGE-READY and not mine to merge.

The re-pass condition is discharged — CI is fully green at 57784daf

The non-author PASS above is conditional on three then-pending jobs. All three have since completed
success: Dashboard tests (pytest + coverage), Shell tests (shellcheck + pithead suite),
Dashboard image (Docker test stage).

GET /commits/57784daf/check-runs read by me just now: total_count 22 — 20 success, 2 skipped,
0 failing, 0 pending.
Read at the head the pass names, which is also headRefOid.

mergeStateStatus is BEHIND with mergeable: MERGEABLE. Not rebasing: a pass names a head and a
rebase voids it, and the admin merge bypasses the up-to-date requirement.

NIT E — filed, not taken, as #1943

The headline honours the array for skipped and not for the row verdicts, so
{ok: true, probes: [<pass>, <ok: false>]} still renders green over a red row. The finding is
right.
I am not taking it here: no producer is merged at all — a tree-wide git grep for the
probe on origin/develop returns nothing — so no input reaches it, and a commit would void a
second recorded pass on a PR that is on the RC's critical path. Same call, same reason, as #1924
against #1920. The one-clause fix and the reasoning for leaving the consequence on s.ok are both
written into #1943.

The three stale figures are corrected in the body — and there was a fourth

Verified by my own measurement, not taken from the review:

body claim was is how measured
tests in the new file 22 23 grep -c '^test('
frontend suite 601 602 node --test dashboard/tests/frontend/, # pass 602 / # fail 0, rc=0 written by me
nodeprobe.mjs / wizardprobe.test.mjs 124 / 301 131 / 317 wc -l
probeSummary asserted directly 19 of 22 16 of 23 tests that never call the render helper

The fourth row was not in the review — the justification line still read "19 of the 22". The
budget conclusion is unchanged: both files are under the 400-line threshold, so file-budget.tsv
stays untouched.

What I did NOT do

No rebase, no new commit, no merge. I did not re-run the pytest, shell or image jobs locally — CI's
own green at this head is what I am citing, and another lane holds the bench.

@VijitSingh97
VijitSingh97 merged commit 48b9695 into develop Sep 7, 2026
22 checks passed
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