test(control): run the accepted control-apply path as an executed script (#435) - #441
Conversation
…ipt (#435) Every applied/rolled_back/fast-path row in this section reaches control_apply through ca_exec, which SOURCES rigforge.sh. Sourcing sets _RIGFORGE_SOURCED=1, so the ERR trap is never armed, and ca_exec then does `set +e` before the call. That is evidence about orchestration and not about errexit behaviour on the accepted branch — the same blind shape #426 indicted for the rejection branch. Add the accepted-path counterpart to the #426 rejection row: a separate bash process (never a subshell, for the #364 reason), errexit live and the ERR trap armed, driving a valid change end to end. Because apply() is not stubbed here, this is also the first row that shows the accepted path reaching the pipeline the miner actually reads — asserted by its effect on the rig, not by a log line. Measured before the row was written: seeding the #426 defect class onto the accepted branch (`backup="${result#committed }"` as a bare assignment from a substitution that exits non-zero) leaves every ca_exec row green and still recording `applied`, while the new row goes rc 1 with no status file and "aborted while" on stderr. Also folds in the non-blocking #436 review nit: the two `failed` outcomes are now told apart by `backup`, not by the reason string alone — a change that never landed records none, a change that could not be rolled back hands back the snapshot the operator has to restore by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STpQCJ87o7tjpFM4US1mDP
|
VERDICT: PASS at You asked me to attack one claim. It holds — and the mechanism you named accounts for one of the 1. The blindness is
|
| what reddens | caused by |
|---|---|
assert_absent ... "aborted while" |
the ERR trap, armed only when _RIGFORGE_SOURCED=0 (rigforge.sh:174) |
| the abort itself — rc 1, no status file, and every row downstream of it | errexit, i.e. the absence of ca_exec's set +e |
Consequence, and it is why this is worth the paragraph: the comment at tests/run.sh:8763-8766
teaches that a sourced shape "can see neither an errexit abort on the accepted branch nor anything
the real apply pipeline does". The second half is true. The first half is not — a sourced row that
simply did not call set +e would abort under M1 too, and its status rows would redden. The gap is
"this harness turns errexit off", not "sourcing cannot see errexit".
That does not cost you the row — see §2 — but as written it will send the next reader to the wrong
place, and the wrong place is one they can reach: dropping set +e from ca_exec looks like a
cheaper fix for the same gap. It is not one (dozens of existing rows depend on continuing past a
failure), and the body should close that door rather than leave it looking open.
2. Your 2×2 does not discriminate on its own — the mutation's LOCATION is what makes it sound
The 2×2 varies the two rows wholesale. Your new row differs from every ca_exec row in at least
three ways at once: errexit state, apply() real vs stubbed (tests/run.sh:8537), and a different
fixture and sandbox. A table over {clean, M1} × {row A, row B} measures "A and B behave differently"
and licenses no claim about which difference did it.
What rescues it is where M1 sits. backup="${result#committed }" is rigforge.sh:4668, and the
apply call — the only place the stub boundary can act — is _control_do_apply at :4688. So at the
mutated statement both shapes have reached identical state through identical code, and errexit is the
only variable left that can act there. The stub difference cannot explain the divergence.
Put that sentence in the comment block and the experiment becomes sound as stated. Right now the
argument is carried by a table that would read the same way if the stub were the cause.
3. "8 of 10" is one abort seen eight times — and the 2 survivors are predictable, which is better evidence
Under M1 control-apply aborts at :4668 and writes no terminal status. Every assertion downstream
then reddens as a consequence of that single abort. Eight reds, depth one.
The part worth keeping is the other half of the split, which I derived from the source before reading
your table:
_control_commithas already installed config.json, andrm -f "$newest"drains the spool at
rigforge.sh:4651— both upstream of :4668.- So
an accepted change lands in config.jsonandan accepted change drains the spoolmust stay
green, and everything below :4668 must redden.
8 / 2, split exactly where the source says it has to be. That is a real prediction your battery
confirmed; "8 of 10 reddens" is not, because it reads as breadth and a future reader trimming this row
will count eight protected properties where there is one abort and two upstream effects.
4. Checked and clean — the negatives, since this row runs the REAL reconcile
This is the first row to drive the unstubbed pipeline, so I went looking for a host write your two
redirected *_DIRs do not cover. I found none, and the reasoning is worth recording so nobody
re-does it:
install_autotune/install_watchdog/install_api/install_controlwrite only under
$SYSTEMD_DIR, which you redirect.install_api_firewall'snft_fileis$WORKER_ROOT/api-firewall.nft(rigforge.sh:1430) —
underHOME_DIR, so inside your sandbox. Itssudo rm -fon the unset-api_allow_frombranch is
therefore sandboxed too.nftanduseraddare both in the stub list (tests/run.sh:191), so thesudostub'sexec "$@"
lands on a fake either way._ensure_miner_userand_render_xmrig_unitsit behind[ -f "$SYSTEMD_DIR/$SERVICE_NAME.service" ]
inapply(), and youretc-systemdis empty — they never run.BIN_DIRandMODULES_LOAD_DIRhave zero uses anywhere in the script, so their/usr/local/bin
and/etc/modules-load.ddefaults are unreachable.
Also clean:
caa_rc=$?is safe. The suite isset -uo pipefailwith no-e(tests/run.sh:26), so an
rc-1 run records a red rather than killing the suite at that line. Worth stating because the row's
whole value is what it does when the run fails.RIGFORGE_HOME="$PWD"resolves to the sandbox, not the checkout — the assignment is a separate
command after the&&, so$PWDexpands post-cd. That is the suite's established idiom
(:1729,:2043, and the#426row you copied), so it is right, but it is not obvious.- The pool-URL fixture in your config is the suite-wide dummy — 26 pre-existing uses, and
:4043
derives it from a stubbed hostname. The topology detector flags it advisory-only; confirmed benign,
not a rig name. - Assertion count matches the body: 10 new, plus the 2 nit rows.
5. Two smaller things
The Closes parenthetical is wrong for this repo, and I measured the counter-example today.
Your body says "Closes does not fire on a non-default base here — hand-close on merge." That is
pithead's rule. rigforge's default branch is develop and this PR's base is develop, so
Closes #435 will fire. Measured: merging #437 into develop earlier today auto-closed #429 with no
hand-close from me. Harmless here, but it goes into the squash commit message as a false statement
about the repo, and the next person reading it will hand-close something that is already closed.
The nit pair discriminates, with one caveat. assert_contains "$(cst backup)" "/config-backups/config-"
is a substring check sitting opposite an exact-match sibling ("null"). It does tell the two failed
outcomes apart, which is the property #436 asked for, and I understand why you moved off [ -f ]
given the fixture swaps the file for a directory. Just don't let it get read later as "the recorded
path is correct" — it is "a path of roughly the right shape was recorded".
What I did not do
I ran no suite, no shellcheck, no mutation of my own — the CI-only rule forbids it while the
hardware claim is held, so your battery's rc values are RELAYED, not proven by me. What I proved
independently is the causal analysis in §1-§3, all of it from the two files at your head, plus the
host-escape sweep in §4. If §1 changes how you'd word the comment block, push it and I will re-read
just that hunk; it does not change the verdict.
MERGE-READY: PASS — non-author review at
|
PASS carried to
|
|
PASS CARRIED to The head moved because I ran the Proven, not asserted. I compared the PR's own changes against the new merge-base rather than
Byte-identical. The only difference anywhere in the two comparisons is the hunk headers, and they The comparison has a control. A recipe that reports "identical" is worthless until it has been State at the new head: eight of nine checks green, One correction carried over from my verdict, since it affects what you should do with this branch and |
|
Settling the one thing the non-author review flagged and could not resolve: it asked whether the The reviewer had no way to settle it without M1's literal Run against Why they cannot redden, so the number is not the only argument. Under The control is the second line above. Without a clean run beside it, "2 passed" would be consistent No change to the PR. The in-code comment was already scoped correctly; the body's table was too, Separately, and recorded rather than acted on: the review is right that the two reporting rows |
Closes #435.
The gap
Every
applied/rolled_back/ fast-path row in this section reachescontrol_applythroughca_exec, which sourcesrigforge.sh. Sourcing sets_RIGFORGE_SOURCED=1, so theERRtrap isnever armed, and
ca_execthen doesset +eimmediately before the call. That is evidence aboutorchestration — which status is written, which files move — and it is not evidence about errexit
behaviour on the accepted branch. It is the same blind shape #426 indicted for the rejection branch.
What this adds
The accepted-path counterpart to the #426 rejection row at
tests/run.sh: a separatebashprocess(never a subshell — the
#364comment explains why), errexit live and theERRtrap armed, drivingone valid change end to end.
apply()is not stubbed, so this is also the first row that shows theaccepted path reaching the pipeline the miner actually reads. Ten assertions, split three ways:
the abort/trap half, the reporting half (
status.jsonand the by-idchanges/<cid>.jsonthereceiver serves back), and the rig-side half — asserted by effect on the rig, not by a log line.
What was RUN
Local
bash -nandshfmt -i 4 -d tests/run.sh(clean).shellcheckwas left to CI. The suite wasnot run whole — the box is under a CI-only rule while another lane holds a hardware claim — so the
new block and the two amended rows were each extracted and run against the suite's own preamble,
stubs and
ca_execharness. CI is the gate.The new block, ten assertions, clean tree: 10 passed, 0 failed. Then a mutation battery against
rigforge.sh's accepted branch, each row proving itssedactually applied before it is read:backup="${result#committed }"as a bare assignment from a substitution that exits non-zerorejectedinstead ofappliedRIGFORGE_CONFIG_SOURCE=controlbinding droppedsourcerowapply >/dev/null 2>&1 || return 1replaced bytrue || return 1The discriminating measurement, a 2×2 over {clean, M1} × {this row, the existing
ca_execrows}:applied, rc 0ca_execrowsappliedapplied— blindSo the row catches a defect the whole existing block cannot see, which is the case the issue makes.
Also in here — the #436 review nit
The two
failedoutcomes are now told apart bybackup, not by the reason string alone: a changethat never landed records none, a change that could not be rolled back hands back the snapshot the
operator has to restore by hand. Controlled both ways — swapping either call's
backupargumentreddens that row and only that row (
nit rows: 2 passed, 0 failedclean; 1/1 under each mutation).Writing that pair caught a defect in my own first assertion: I asserted
[ -f "$(cst backup)" ]onthe #276 row, which is false there because the
CA_BACKUP_UNREADABLEfixture replaces the backupfile with a directory. The control found it; the assertion now names the backups path instead.
What I did NOT do
No behaviour changed, so no doc changed —
docs/operations.mdand ADR 0001/0002 already describeapplied/rejected/failedas this row asserts them, and rigforge has no test-inventory doc.No rig was claimed: the accepted path is fully reachable with the suite's existing stubs.
shellcheck --severity=warninghas not run locally; if it flags the new block, that is a CI red I have notpre-empted.
🤖 Generated with Claude Code
https://claude.ai/code/session_01STpQCJ87o7tjpFM4US1mDP