test(control): pin that the canonical config view serves a pool as stored (#429) - #437
Conversation
…ored (#429) `_writable_config_canonical` must serve a pool's stored fields as stored, inventing neither `user` nor `pass`. That property is what makes #415's masked-secret round-trip work: the feed masks a SET `pass` to the `{"__secret__": true}` sentinel and leaves an unset one absent, so "no marker" means "there is nothing here to keep" and `_control_commit` can resolve a returned marker against the stored value. Until now the property was carried by a paragraph in `_control_commit`, and a paragraph does not redden. `parse_config` normalizes on the way to XMRig — a missing `pass` becomes the literal "x", a missing `user` becomes "" — so pointing the canonical view at `POOLS_JSON` ("use the parsed pools, we already have them") advertises a password on every pool that stores none: the mask turns the invented "x" into a sentinel, the sentinel comes back on the next pools edit, `stored` finds nothing to keep, the marker survives the merge, and the edit is rejected `unresolvable-secret-marker` — naming a secret the operator never set. The row asserts the served pool's KEY SET rather than the absence of two values: a later `// ""` default would satisfy an equality row while reintroducing the defect, and the key set also catches the `keepalive` / `tls` / `enabled` keys the normalized array invents. A second pool in the SAME served block does store a password and comes back as the sentinel, so "no pass key" cannot be read off a fixture that never reached the mask. The hinge comment in `_control_commit` now says the guard exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STpQCJ87o7tjpFM4US1mDP
…arry Over-engineering pass on my own diff before opening the PR: ten comment lines for three rows restated what the commit message already says. Kept the chain that explains why the row exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STpQCJ87o7tjpFM4US1mDP
e93a11c to
63463f5
Compare
VERDICT: PASS — non-author review at
|
| Claim | Source | Result |
|---|---|---|
| Control run 2041/3 | job 101228050772 (e93a11c2) |
rigforge tests: 2041 passed, 3 failed ✔ |
| Clean run 2044/0 | job 101229244062 (63463f53) |
rigforge tests: 2044 passed, 0 failed ✔ |
| Same population | 2041+3 = 2044; the broken commit touched rigforge.sh only |
controlled pair ✔ |
| New row fired | ✗ … expected [url], got [enabled,keepalive,pass,tls,url,user] |
✔ |
| Both in-block controls green under the fire | ✓ control — a stored pass … masks to the sentinel, ✓ control — the stored password itself is not in the block |
✔ |
| The other two reds are pre-existing | ✗ …unset pass has no marker (#415) expected [false], got [true]; ✗ sister-API feed shape matches the fixture (#351) |
✔ |
| The three new rows individually ran green on the clean head | three ✓ … (#429) lines in the log, not just a green check label |
✔ |
1. The "not regenerated" argument holds against #351 — harder than the body claims
The body says a contract fixture invites "update the fixture". tests/contract/v1/README.md § The change rule does not invite it, it prescribes it: "Regenerate by hand — copy the relevant block out of the guard's failure output, jq -S it, and paste it in." And § Versioning pre-classifies this exact drift as the benign kind: "A purely additive change (a new key…) stays in v1." Measured from the control log, the drift is purely additive to the fixture's pool — enabled: true, keepalive: true, tls: false, user: "" appear, url is unchanged, pass changes value not type. Nothing is removed and no type changes, so the documented rule routes it to an in-place paste rather than a v2/ break. The failure output is even formatted for that paste. tests/README.md:46 says the same thing in one line.
Diagnostic cost, measured rather than asserted: the #351 failure prints the whole feed twice — 97 lines on the expected side before got [ — and carries two independent deltas inside that dump (the pool key set, and config_meta.revision 50c51399833d2a28 → fcb3d1c630b37dee). The #429 row fails in one line naming the property.
2. The argument the body did NOT make is the one that earns the row: the two existing reds are not two independent barriers
The #415 row is a named behavioural row, not a fixture, so "not regenerated" says nothing about it. Attack it directly: it constrains one key — has("pass") — on a fixture that stores a user. Its red is repairable without abandoning the refactor: teach the mask to skip parse_config's literal "x" default, a value this suite already names elsewhere (✓ an absent pass still defaults to x, unchanged (#415)). (This repair path is a hypothetical I constructed — not an observed event.) Under it, the #415 row goes green while user: "", enabled, keepalive, tls are still served, and the only remaining red is the contract fixture — i.e. back to the doc-prescribed paste in §1.
The new row does not survive-and-go-green under that hack: its got side is still enabled,keepalive,tls,url,user ≠ url. That is what the key-set form buys over has()/has(), and it is why the row is not redundant with #415. So the honest statement of what ships is narrower than "first-line detection" (correctly disclaimed) and wider than "a nicer diagnostic": it is the only check on this property that a single plausible wrong repair cannot clear.
3. The barrier I owe you against my own PASS
A fixture paste-over is not free. tests/run.sh:9478 documents that pithead's fakes fetch and byte-compare these files, and tests/contract/v1/README.md says a bump here needs a paired pithead PR. So the drift surfaces again cross-repo. That is a real second chance — but it fires in the other repo, later, where the _writable_config_canonical mechanism is invisible and the same "update the fixture" reflex applies. It lowers the value of this row; it does not zero it. Net of it, +16 additions-only lines at the unit tier is still worth paying.
Q2 — key set vs has("user") / has("pass"): right instrument, and the brittleness worry does not have a mechanism
Checked structurally, not by taste. _writable_config_canonical (rigforge.sh:5283-5285) passes pools through verbatim — jq -c '.pools // []' "$CONFIG_JSON" — and _api_config_json (rigforge.sh:5303) only masks or deletes. There is no per-pool synthesis path in the canonical view at all. So a bare pool's served key set can only grow three ways: (a) the operator stores more keys — the fixture is a literal, so never; (b) a new masked key is added — mask deletes an unset key, so a bare pool's key set is unchanged; (c) somebody adds per-pool defaults or enrichment — which is precisely the property under guard. There is no legitimate-addition class that reddens this row without being the thing it exists to catch.
Two confirmations while I was there: keys sorts (unlike keys_unsorted), so field order is not a false-failure source; and the row fails closed — if cfgblk returns nothing, the jq yields empty and the row reds rather than passing vacuously.
The genuine cost of the key-set form is that it reds on a superset, so a future legitimate served pool field forces a hand edit of the row. For a guard whose subject is literally "as stored", that is the correct behaviour, and the row's name tells the editor what to think about. Keep it.
Non-blocking
- The disclosed gap is the one worth filing. No row exercises the defect at its actual symptom — feed → replay →
_control_commiton a pool that stores no pass. The control run proves the gap rather than arguing it: every_control_commitcontrol-apply defaults a missing poolpasstoxand the feed hides whether one is set, so a consumer editingpoolswipes the credential silently #415 row stayed green under the substitution, including✓ round-trip: replaying the feed's own pools preserves the password (#415), because that fixture stores a pass. A "bare pool round-trips and commits" row would red on the substitution and is unregenerable. Worth an issue; not a condition on this PR. - The 2-line
rigforge.sh:4376-4377comment claims what actually landed (tests/run.sh:7794-7799), stored-pass control included. Accurate.
Merge preconditions (not met yet at the time of this read)
Test suite is green at this head, but run 33937878288 is not terminal: Test suite (macOS) and End-to-end (Docker) were still in_progress. mergeStateStatus is BLOCKED — the merge-base already equals the develop tip, so this is checks/review, not behind-base. Merges on the terminal green rollup at this sha; a re-read of the head is required first.
|
VERDICT: PASS at The disclosure I re-derived, because it is the one the decision turns onYou said so a pool that stores no It does not change the answer, for the reason you gave, and I want that reason on the record The controls are correctly placed, and I checked they discriminate
I also checked the row can only go green the intended way: under the The
|
|
MERGE-READY: non-author PASS at State at that head, measured not assumed:
Two things for whoever merges this, and I am the author so it is not me:
|
|
Correction to my MERGE-READY comment above: all nine checks are green at The cause is worth recording, because it will mislead the next reader the same way: Nothing else in that comment changes. The recorded non-author PASS is at |
|
RE-PASS: the verdict above stands unchanged at the new head The head moved because I ran the I verified the updated head carries your change and nothing else, by a different route than the Two files, +16, additions only — identical to what I reviewed at I will merge this on the green rollup at Taking the author's instrument note with it, since it bears on how I read that rollup: settle check |
Closes #429.
What this pins
_writable_config_canonicalserves a pool's stored fields as stored, inventing neitherusernor
pass. One row intests/run.sh, beside the existing_api_config_jsonrows, drives the feedon a config whose first pool carries only a
urland asserts the served pool's key set isexactly
url.The key set rather than two
has()checks, for two reasons. It is one measurement instead of twoderived from the same served object; and it also catches the
keepalive/tls/enabledkeysthat
parse_config's normalized array invents, which a pair ofhas("user")/has("pass")rowswould let through. Asserting absence rather than an empty value is the issue's own point: a later
// ""default would satisfy an equality row while reintroducing the defect.The control
A second pool in the same served block stores a password and comes back as the
{"__secret__": true}sentinel, with a row asserting the password itself is nowhere in the block.Without it, "no
passkey" cannot be told apart from a fixture that never reached the mask. Bothcontrol rows stayed green in the broken run below, which is what they are for.
Why the property matters
parse_confignormalizes on the way to XMRig: a missingpassbecomes the literal"x", a missinguserbecomes"". Point the canonical view atPOOLS_JSON— "use the parsed pools, we alreadyhave them" — and every pool that stores no password is advertised as holding one. The sentinel comes
back on the next pools edit,
storedfinds nothing to keep, the marker survives the merge, and_control_commitrejects the editunresolvable-secret-marker, naming a secret the operator neverset. The paragraph in
_control_committhat carried this now says the guard exists.What was RUN
The rows were pushed on a deliberately broken tree first, so the guard was seen to FAIL before its
clean run could be read as evidence. That commit pointed
_writable_config_canonicalatPOOLS_JSON— the exact refactor #429 forbids — on top of the guard. It has since been dropped fromthis branch by rebase; the review head is the guard alone, and the diff against
developis +16lines, additions only.
The control run (
Test suite, 2026-09-05T01:59Z, superseded head): 2041 passed, 3 failed. Thenew row failed with
and both of its control rows stayed green. The other two failures are named under "Disclosed gaps".
Local execution was limited to
bash -non both changed files: CI is this window's only test gate.The canonical and mask
jqprograms were additionally driven by hand over a raw pool array and overa
POOLS_JSON-shaped one, before the branch existed; that prediction — key seturlbecomingenabled,keepalive,pass,tls,url,user— is what the control run then printed verbatim.Disclosed gaps
No test pins that the canonical config view serves pools as stored — the hinge is a comment, not a guard #429 says nothing in
tests/pins this property. Two existing rows redden on the samesubstitution, both confirmed in the control run: the control-apply defaults a missing pool
passtoxand the feed hides whether one is set, so a consumer editingpoolswipes the credential silently #415 rowconfig: unset pass has no marker — nothing to keep(expected [false], got [true], its fixturealso stores no
pass), and the Cross-repo wire contracts have no automated drift guard against pithead's fixtures #351 contract rowsister-API feed shape matches the fixture,because
tests/contract/v1/feed.jsonpinsrigforge.config.poolsas[{"pass":{"__secret__":true},"url":"h:3333"}]. Neither was mentioned in the issue and I did notpredict the second one.
exists. It buys a guard that (a) names the property instead of asserting a whole-feed byte shape,
failing with a one-line diagnostic rather than a JSON diff; (b) carries the mechanism in a comment
where the next maintainer meets it; and (c) is not the kind of check anyone regenerates. That
last one is the substantive argument: a contract fixture that reddens on a shape change invites
"update the fixture", which is precisely the path by which this defect would ship anyway. A named
behavioural row is not regenerated. If the reviewer weighs that as insufficient, closing No test pins that the canonical config view serves pools as stored — the hinge is a comment, not a guard #429 as
already-covered with this evidence recorded is a defensible outcome and I will not argue it.
jqoutput; thebehaviour it guards has no rig-side half.
_control_commit's resolver end to end — it pins the feed's half of thecontract only. The resolver's own rows already exist from control-apply defaults a missing pool
passtoxand the feed hides whether one is set, so a consumer editingpoolswipes the credential silently #415.🤖 Generated with Claude Code
https://claude.ai/code/session_01STpQCJ87o7tjpFM4US1mDP