Files

17 KiB
Raw Permalink Blame History

Row 292 (#1797): config_checks runs the target loader on any config

Builder Gizmoduck (sage-3146, mos-1387), reviewer Duckworth, gate owner Sage. Brief docs/plans/2026-10-22_config-checks-any-key-brief.md. Gander built round 1 on 01983cd3f (gander/config-checks-any-key, packet agents/gander/work/config-checks-any-key/, Duckworth static approve duckworth-0229 with C1-C2 and N1-N4). Gander doesn't build 292; questions about round 1 go to Gander. Nothing in this packet has run. No test, mutant or repo node ran, with one exception recorded under "Disclosure".

Rebase (sage-3146 item 1, sage-3148)

Branch gizmoduck/row292, base b23783f39 (the row 270+271 landing 7328da8ca plus its docs commit; row 203 landed earlier). git rebase --onto b23783f39 01983cd3f d4c796e44.

commit rebased patch-id (stable) before after
0585b3213 fix(release) 3b17c7550 a922252a85b1317e09a625193172167b7ea39edb 15f1a38e3c1e6c72c38b77a460b6cd4ce60c6dd4
e7371025f docs (plan, packet) a54baff21 8e9b78ac185a49548e94e76c83f7fea1a982038a 8e9b78ac185a49548e94e76c83f7fea1a982038a
d4c796e44 docs (approval) 9c6e39b25 fc50b630d432ff7134e3d6c711a054d15ff1f997 fc50b630d432ff7134e3d6c711a054d15ff1f997

e7371025f and d4c796e44 keep their patch-ids. 0585b3213 changes, by the conflict below.

The conflict and its resolution

Only scripts/stack-release.sh conflicted, in config_checks (now :1076-1107). Row 203 (284d55cf2) made the old probe exit 0 on a console or a federation key and the refusal read "holds a console or federation key". Gander's commit replaced the probe and split the refusal into a console text and a text for any other key. The test file and docs/TOOLS.md merged without conflict.

Resolution, per sage-3146 item 1 and sage-3148:

  1. The probe picks console first, then federation, then the first key outside the oldest set configVersion, environment, dataRoot, execution. Without the federation step, a config that holds a later key before federation would get Gander's text although the loader refuses federation.
  2. With no target loader, a console or federation key gets 203's reason "it has no scripts/mosaic-config.mjs", as at b23783f39. Any other later key gets Gander's "to validate configuration key: ".
  3. The refusal for a console or federation key is 203's line, byte for byte as 284d55cf2 and b23783f39 have it (compared by script, not by eye). Gander's text, "is refused by $TO's loader ($first); remove the refused key first (restore the config.json backup from the window's step 1), ...", applies only when the key is neither console nor federation. In both, $first is the loader's first stderr line, which names the refused key (unsupported configuration key: "<key>").
  4. The comment names 203's protection. docs/TOOLS.md gains one sentence: a console or federation key keeps row 203's message.

Gander's console-only text "holds a console key" (round 1 at 01983cd3f, before 203) is gone: 203's wording covers console as well. Duckworth's N1 (console plus another refused key keeps the console text) holds, now for federation too.

Row 203's tests: every line of stack-release.test.mjs up to :2600 at b23783f39 is unchanged. The rebased file only appends. KEY_MSG (:1471), the item 3 console cases (:1575-1580), the row 203 federation test (:1585-1595) and :1607 are byte-identical to the base. No 203 test or assertion changed, so nothing goes to Mos under mos-1272 for it.

Range-diff

git range-diff 01983cd3f..d4c796e44 b23783f39..9c6e39b25:

1:  0585b3213 ! 1:  3b17c7550 fix(release): validate non-console config keys with target loader
    @@ docs/TOOLS.md: onto the pointer's copy from the target, past any same-copy re-pi
     +key. Every existing parseable config is checked; a missing file passes. A target
     +without a loader accepts only the oldest top-level keys: `configVersion`,
     +`environment`, `dataRoot`, and `execution`; any later key refuses. The refusal
    -+names the rejected key and says to restore the config.json backup first. So a
    ++names the rejected key and says to restore the config.json backup first. A
    ++`console` or `federation` key keeps row 203's message, which says the config
    ++holds a console or federation key. So a
      window re-pins to row 192 or later before it writes the key, and a rollback past
      row 192 removes the key first.
      
    @@ scripts/stack-release.sh: slot_problem() {
     -# target's own scripts/mosaic-config.mjs validate; its output, the resolved config, is not kept.
     +# Row 200 item 3 and #1797: every existing parseable config must be one the
     +# target loader reads. Older copies without a loader accept only the four keys
    -+# in c3d29e796's first loader. Resolved config stdout is never kept.
    ++# in c3d29e796's first loader. A console or federation key (row 203) keeps its own refusal text.
    ++# Resolved config stdout is never kept.
      config_checks() {
     -    local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first
    --    # 0: a console key; 1: no key, or no file; 2: the file doesn't parse, so whether it holds the key
    +-    # 0: a console or federation key (row 203: a pre-203 loader refuses federation too); 1: neither, or no file; 2: the file doesn't parse, so whether it holds the key
     -    # is unknown, and the run refuses rather than pass it (webby-0028 N2). load_config already exits on
     -    # such a file before anything is read or stopped, so this is reached only if the file changes after it.
     +    local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first extra
     +    # 1: missing file; 2: unreadable JSON. This also fences a change after load_config.
    ++    # extra: "console" or "federation" first (row 203: a pre-203 loader refuses federation too),
    ++    # then the first key outside the oldest set.
          local key=0
     -    node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
    --process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' -- "$cfg" 2>/dev/null || key=$?
    +-process.exit(d!==null&&typeof d==="object"&&(Object.hasOwn(d,"console")||Object.hasOwn(d,"federation"))?0:1)' -- "$cfg" 2>/dev/null || key=$?
     +    extra="$(node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
     +const oldest=["configVersion","environment","dataRoot","execution"];
     +if(d===null||typeof d!=="object"||Array.isArray(d)){process.stdout.write(JSON.stringify("[configuration shape]"));process.exit(0)}
    -+const k=Object.hasOwn(d,"console")?"console":Object.keys(d).find(k=>!oldest.includes(k));if(k!==undefined)process.stdout.write(JSON.stringify(k))' -- "$cfg" 2>/dev/null)" || key=$?
    ++const k=Object.hasOwn(d,"console")?"console":Object.hasOwn(d,"federation")?"federation":Object.keys(d).find(k=>!oldest.includes(k));if(k!==undefined)process.stdout.write(JSON.stringify(k))' -- "$cfg" 2>/dev/null)" || key=$?
          [ "$key" != 1 ] || return 0
          if [ "$key" != 0 ]; then
              check_refuse config-console-key "$cfg cannot be read as JSON, so whether $TO's loader reads it is unknown; fix the config first, then run the $OP again"
    @@ scripts/stack-release.sh: slot_problem() {
     -    if [ ! -f "$loader" ]; then out="it has no scripts/mosaic-config.mjs"
     +    if [ ! -f "$loader" ]; then
     +        [ -n "$extra" ] || return 0
    -+        if [ "$extra" = '"console"' ]; then out="it has no scripts/mosaic-config.mjs"
    ++        if [ "$extra" = '"console"' ] || [ "$extra" = '"federation"' ]; then out="it has no scripts/mosaic-config.mjs"
     +        else out="it has no scripts/mosaic-config.mjs to validate configuration key: $extra"; fi
          elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0
          fi
          first="${out%%$'\n'*}"
    --    check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
    -+    if [ "$extra" = '"console"' ]; then
    -+        check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
    +-    check_refuse config-console-key "$cfg holds a console or federation key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
    ++    if [ "$extra" = '"console"' ] || [ "$extra" = '"federation"' ]; then
    ++        check_refuse config-console-key "$cfg holds a console or federation key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
     +    else
     +        check_refuse config-console-key "$cfg is refused by $TO's loader ($first); remove the refused key first (restore the config.json backup from the window's step 1), then run the $OP again"
     +    fi
2:  e7371025f = 2:  a54baff21 docs(release): record any-key config plan and static review packet
3:  d4c796e44 = 3:  9c6e39b25 docs(release): record static approval and post-compaction review conditions

C1 and N3 (sage-3146 item 3, duckworth-0229 C1)

Commit 4fa7d6834, test file only.

  • LOADER_OLD (stack-release-fixture.mjs:301) is today's loader with a shorter first list, so it still reads compactionReadBytes and compactionStreams through the .concat. It is no pre-270 witness. The new stand-in PRE270 is C1's copy of today's loader with the .concat(["compactionReadBytes", "compactionStreams"]) taken out. That leaves ["configVersion", "environment", "dataRoot", "execution", "console", "federation"], the list the loader had at bce491715^ (01b57f48b), before bce491715 added the first key. The stand-in asserts the replace changed something, so a reworded loader fails loudly rather than passing a no-op stand-in. The fixture module isn't edited (not an owned file).
  • New test "config-any-key: a pre-270 target refuses each row 270 key on a full activate and rollback, naming the key". It seeds on B, activates C1 and installs PRE270 as B's loader. For compactionReadBytes 1048576 and compactionStreams 1024 in turn, it first runs C1's own loader validate on the config (exit 0, so the refusal is the target's). Then it asserts the exact refusal for a full activate B and for rollback, using refused(): exit 3, the exact stderr, no file changed, no start or stop and one refused log line. The text is ANY_CONFIG_MSG with mosaic-config: unsupported configuration key: "<key>". Afterwards it restores the loader and config, and the rollback to B passes.
  • N3: the missing-loader test sets compactionStreams to 1024, not ['messages'].
  • The missing-loader test also checks 203's no-loader text word for word for console and for federation, each placed after compactionStreams in the file. This covers resolution items 1 and 2, which no 203 test reaches: 203's tests all use a target with a loader.

Files (manifest)

Paths changed from b23783f39 to the candidate, with sha256 at the candidate:

path b23783f39 sha256 candidate sha256
scripts/stack-release.sh d3f52f72f2ea345118d30dc34f045f2b5b6abce41697e62f6de07d7a2f25748c 8f535d53896fefe44525dea39169ac8df29f3d7845f83552a34ec1cd707595bb
packages/cli/tests/stack-release.test.mjs 45faa5d236d4cfb5cf3c026d64d039a5f29ce8868efeed4e9428cf85a5f0067f 5a228aee7c3811f906513809f8a78a1fe4f3a26057089ae6c87918f432a5ba65
docs/TOOLS.md 423babffc7be40a2d578ef00e97cdc49d887762e416fc20726d328d96b2a9533 611aeb79d603dd1af1917dcbb3db81b51ac0ae30c5b4991ea72e18bc45ee59de

The rest of the diff is packets: Gander's agents/gander/work/config-checks-any-key/ (eight files) and this one (BUILD.md, mutants.py). The manifest with every path and sha256 goes to Duckworth with the round key.

Mutants

agents/gizmoduck/work/queue-292/mutants.py. Each anchor appears exactly once, and each mutated script passes bash -n (checked with python3 and bash only, no node). None has run. M1-M5 are Gander's edits (changes.json) on the resolved function. M6-M9 test the resolution. M1 restores the console-only skip, the brief's required mutant.

Kill rules (sage-3152)

Each mutant runs only its killing test: node --test --test-name-pattern '<pattern>' tests/stack-release.test.mjs. The patterns are the test names, anchored with ^ and $, and all four are top-level tests. A mutant counts as killed only when all three of these hold:

  1. The run ends with ℹ fail 1, and the failing test is the named one. The spec reporter lists it twice, inline and in the failing-tests summary.
  2. The first error's stack names every listed stack-release.test.mjs line.
  3. The log holds the listed text. The texts carry no quote characters, which node's inspect may escape.

Anything else is a STOP that goes to Sage before the next mutant: a fail in another test or at another line, a missing text, zero tests matched, a timeout or a crash before the assertion. A fixture or module-load error is the second-fence trap from 264a M2. A mutant whose targeted run passes gets one run of stack-release.test.mjs whole before it counts as a survivor.

id edit test (pattern) stack lines text why that assertion
M1 console-only skip restored non-console target refusal :517 (refused), :2619 actual: 0 no console key, so the check returns 0 and activate B runs: exit 0, not 3
M2 failed validate ignored (|| true) non-console target refusal :517, :2619 actual: 0 the stand-in's exit 2 is lost, so activate B runs
M3 no-loader target passes any later key missing-loader copies :2670 actual: 0 compactionReadBytes, the first case, passes with status 0
M4 malformed JSON treated as absent missing config passes but malformed JSON :2684 actual: 0 key 2 falls through to the witness loader, which exits 0
M5 oldest-key config skips the loader accepting loaders run validate :2654 actual: [] the witness file is never written
M6 probe drops the federation preference missing-loader copies :2677 to validate configuration key: federation after compactionStreams: the probe picks compactionStreams
M7 probe drops the console preference missing-loader copies :2677 to validate configuration key: the same for console, the first case of that loop
M8 final refusal treats only console as 203's missing-loader copies :2677 is refused by old federation gets Gander's text, not KEY_MSG
M9 no-loader branch treats only console as 203's missing-loader copies :2677 to validate configuration key: federation's reason gains the later-key wording

M1 would also fail the pre-270 test and the row 203 federation test, and M8 the row 203 federation test. The targeted runs don't select those, so they aren't counted.

Run plan (on Sage's CLEAR only, after Duckworth approves this round)

  1. stack-release.test.mjs whole, limit 2100 s.
  2. The cli suite whole, node --test tests/*.test.mjs in packages/cli, limit 2100 s.
  3. The ten scripts/test-*.sh, limit 900 s each.
  4. Only on a green base: M1-M9 under the kill rules above. Targeted runs get 600 s and a whole-file rerun of a survivor gets 2100 s. After each mutant: git checkout -- scripts/stack-release.sh, sha256 equal to the candidate's (8f535d53...) and a clean tree.

Every stage gets its own short TMPDIR, mktemp -d /mnt/storage/scratch/tmp/gXXX, directly under /mnt/storage/scratch/tmp (sage-3152, 574 run 1's socket-path fault). Every mutant run gets one too. All runs use pipefail, tee and timeout -k 10. The busy check passes before each start. A first red in stages 1-3 stops the run.

The limits come from the run 5 receipts (gizmo-271-out/r4, 01:49Z 10-23). stack-release.test.mjs whole took 682 s (duration_ms 681648), the cli suite 624 s and the longest shell suite 44 s. Each limit is at least 3x. Expected time: about 26 minutes for the base, plus about a minute per targeted mutant (fixture setup and one test, not measured yet), plus 12 minutes per whole-file rerun.

C2 (duckworth-0229, sage-3146 item 4): every B, C1 and C release now validates its copied real loader. Any red in an old cli test is a suspect from this change. It stops the run and goes to Sage and Mos under mos-1272, unplaced.

Disclosure

During the hold (sage-3146 item 6, no repo node) I ran node --check packages/cli/tests/stack-release.test.mjs once, between the rebase finish (02:43:51Z, reflog) and the first C1 commit (02:45:41Z). It is a parse-only syntax check that lasted a few milliseconds. It printed nothing. No other node, test, queue write or https push/fetch has run.