Compare commits

...
Author SHA1 Message Date
jason.woltje 90be6cccdd row 292 (#1797) gate results on 0d0021454: r2 old-test red compactions-launch:229 (exit-22 race, Vikunja 588), mos-1411 diagnostic 0/10 with one not-holder exit 22 seen in the clear test, r3 cli rerun 910/910, ten shells green, M1-M9 killed; rebase receipts (sage-3183, sage-3185, sage-3193) 2026-10-23 00:17:07 -05:00
jason.woltje 0d00214545 row 292 (#1797) packet: targeted mutant kill rules per sage-3152 (pattern, stack lines and text per mutant; anything else is a STOP; a survivor gets one whole-file run), short TMPDIR per stage, run plan after Duckworth's round 2026-10-22 23:31:49 -05:00
jason.woltje 2db30c687f row 292 (#1797) packet: rebase onto b23783f39 with the 0585b3213 resolution (row 203's console and federation text kept word for word, Gander's text for any other key), range-diff, the three patch-ids, C1/N3, mutants M1-M9 (unrun) and the run plan (sage-3146, sage-3148) 2026-10-22 23:31:49 -05:00
jason.woltje b48e415e44 row 292 (#1797) C1 and N3 for duckworth-0229: a pre-270 loader stand-in of its own (C1's loader without the row 270 concat, the list before bce491715) refuses compactionReadBytes and compactionStreams on a full activate and rollback, naming the key; C1's loader reads both; the missing-loader case uses compactionStreams 1024 and checks row 203's console and federation text word for word with no loader 2026-10-22 23:31:48 -05:00
jason.woltje 61fdb6a8d7 docs(release): record static approval and post-compaction review conditions 2026-10-22 23:31:48 -05:00
jason.woltje 9f15216f72 docs(release): record any-key config plan and static review packet 2026-10-22 23:31:48 -05:00
jason.woltje 89e58f4b1b fix(release): validate non-console config keys with target loader 2026-10-22 23:31:48 -05:00
15 changed files with 687 additions and 14 deletions

No files matched your search

@@ -0,0 +1,7 @@
{
"base": "01983cd3f",
"test": "packages/cli/tests/stack-release.test.mjs",
"base_is_byte_exact_prefix": true,
"old_assertions_changed": 0,
"old_console_refusal_text_unchanged": true
}
@@ -0,0 +1,48 @@
# Target config validation for every key implementation plan
Goal: #1797, mos-1353, brief f0e3dcdf4:docs/plans/2026-10-22_config-checks-any-key-brief.md. Base01983cd3f, branchgander/config-checks-any-key. Reviewer Duckworth. Queue number pending Sage; packet moves to queue-N when assigned. No Node, test or push before Sage gap/CLEAR. Rows270/271/203 land first; rebase under a new branch name before the gate.
Architecture: config_checks classifies missing/parseable/unreadable config independently of console presence. With a loader, every existing parseable config goes through the target's own validate. Missing config passes. Malformed JSON refuses. With no loader, allow only the oldest four top-level keys: configVersion, environment, dataRoot, execution. Evidence: c3d29e796:scripts/mosaic-config.mjs:114, the first loader commit, rejectUnknownKeys; row200 fixture's LOADER_OLD has the same list. A missing loader plus a later key refuses and names that key, without inventing validation support. The legacy public refusal code config-console-key and existing console-specific message stay byte-identical; new non-console refusals name the loader's first diagnostic and preserve restore-config-backup instruction. The target loader's resolved stdout is discarded. Its first stderr diagnostic is reported and may echo an invalid value.
Files: scripts/stack-release.sh config_checks/comment only; packages/cli/tests/stack-release.test.mjs appended regression coverage only; docs/TOOLS.md broadens the current console-only release-check description. Existing row200 tests and assertions stay unchanged. No fixture-module edit, flags, pointers, migration, schema, data, live unit or host change.
Tests to write before implementation, unrun until CLEAR:
- A target loader rejecting a non-console key refuses activate and rollback before any unit stop/start, pointer/unit/store mutation; one refused log entry; --plan preserves state and lists refusal. Reuse refused() and planOnly(). Use execution, already known to the current loader, to prove console-free dispatch before row270 integration; add compactionReadBytes/compactionStreams cases on final post270 base if needed.
- An accepting target loader is actually called with validate and the selected MOSAIC_CONFIG, succeeds activate/rollback without console, and resolved stdout is not leaked.
- Missing-loader target permits oldest-key config but rejects a later key with backup instruction. A stand-in rejecting the existing execution key exercises the generic target-validation branch; missing-loader later-key test may use console until final post270 base supplies non-console new keys.
- Missing file passes the isolated real config_checks function; malformed JSON refuses there, since load_config otherwise exits before this function. Tests extract only that function and provide copies/path/check_refuse stand-ins, never live state.
- Existing console-key row200 tests and malformed-load test run whole unchanged.
Mutants, each unique target, syntax preflight and byte-identical restore:
M1 restores the console-only skip, killed by the named non-console activate/rollback refusal test.
M2 ignores failed target validation, killed by the same refusal assertions.
M3 allows a missing-loader target with a later key, killed by the explicit missing-loader regression.
M4 treats malformed JSON as absence, killed by isolated parse guard test.
M5 skips the loader for an oldest-key config, killed by accepting-loader invocation witness.
Gate after static review and required rebases: intended RED/GREEN; touched stack-release.test.mjs whole, CLI suite whole and every scripts/test-*.sh whole; M1-M5 with fresh passing controls, no timeout counted as kill, restored bytes/status before next. TMPDIR mktemp-d /mnt/storage/scratch/tmp/gXXX; pipefail/tee and busy check before each start. Node timeout-k10 uses at least3x measured duration plus margin; stack-release prior full receipt around584s implies at least1871s, to be recorded from actual receipts before CLEAR. Shell3x table retained with receipts. No test ran and no behavior passes claimed.
Review focus: parser races after load_config; target stderr naming only the refused key, stdout never persisted; current versus target loader selection; unknown key with no console; missing loader compatibility set from immutable history; upcoming row270 loader-list fixture drift. Final integration audits every test dependency and hash pin against the named base. Session registration remains Sage's.
Implementation ready, unrun. Four appended tests cover full activate/rollback refusal and plan; actual accepting validate witness/configpath and stdout nonleak; oldest-only missing loader with both compaction keys; missing/invalid JSON isolated realfunction. Stand-in loaders are ESM, matching .mjs. Every old test byte is preserved as a prefix. Existing console refusal text including missing-loader diagnostic is unchanged. Missing-loader compatibility is top-level key compatibility only; current load_config still validates the whole config before normal release operations. Three-path manifest, no fixture-module changes. Static target uniqueness M1-M5 passes. No Node parse, Bash execution or behavior run. Exact full compaction activate/rollback cases must be added against post270 loader, before final gate.
Execution state:
- [x] Read brief, config_checks and row200 assertions.
- [x] Pin oldest key set from immutable loader history.
- [x] Write regression tests, implement bounded scope, prepare unique mutants.
- [ ] Duckworth static review.
- [ ] Move packet to queue-N on assignment, rebase after270/271/203 and add full compaction-key cases.
- [ ] On CLEAR run intendedRED/GREEN, touched whole file, CLI whole, M1-M5, ten shells; commit/push records only when authorized.
## Duckworth0229 static approval, gate conditions
Source0585b3213, packet e7371025f, manifestd6226d28 approved by static read, note a5a717391:agents/duckworth/work/row1797/ROUND1-0585b3213.md. No runtime proof yet.
C1: after270/271/203 land, rebase under a new branch name and open a new static round. Add exact compactionReadBytes and compactionStreams full activate/rollback refusals. The existing LOADER_OLD uses the current loader plus concat after270, so it is not a pre270 witness; the new test requires its own pre270 stand-in. Reviewer checks the integrated manifest/test before CLEAR. N3 joins that delta: compactionStreams fixture value1024, matching the real type, instead of ['messages'].
C2: every existing B/C1/C release now validates its copied real loader, including external/upgrade suites. Any old CLI red is a change-related suspect and goes to Mos/Sage under mos-1272; no automatic placement as an unrelated fixture.
N1 console plus another refused key retains the old console text by design. N2 missing-loader wording is retained pending final integration review. N4 corrected above: only resolved stdout is suppressed; first stderr may echo a value, so no blanket secrecy claim. No runtime/Node/queue/push authorization is inferred from static approval.
@@ -0,0 +1,3 @@
eea06caad07c746d194f793603052b210e437b7184009f8b1617c88db49a5517 docs/TOOLS.md
a35c49c5703e02f35073450e4a98e1b90cffcda581490f97a70cbdc6d2afee59 packages/cli/tests/stack-release.test.mjs
f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150 scripts/stack-release.sh
@@ -0,0 +1,9 @@
{
"reviewer": "duckworth",
"verdict": "static approve, conditions C1-C2",
"message": "duckworth-0229",
"source": "0585b3213",
"manifest": "d6226d28",
"note": "a5a717391:agents/duckworth/work/row1797/ROUND1-0585b3213.md",
"next": "wait for row number and post270/271/203 base, new integration static round before gate"
}
@@ -0,0 +1,5 @@
{
"docs/TOOLS.md": "eea06caad07c746d194f793603052b210e437b7184009f8b1617c88db49a5517",
"packages/cli/tests/stack-release.test.mjs": "a35c49c5703e02f35073450e4a98e1b90cffcda581490f97a70cbdc6d2afee59",
"scripts/stack-release.sh": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150"
}
@@ -0,0 +1,14 @@
{
"issue": 1797,
"row": "pending Sage assignment",
"branch": "gander/config-checks-any-key",
"base": "01983cd3f",
"source": "0585b321309e4d8a199afb1a1024cf6091f56770",
"manifest": "d6226d281c778677a45e90993bbd9c985de89ba0f870d4a3ca12b6923a497e4a",
"runtime": "NOT RUN; no Node, tests or push authorized",
"after": [
270,
271,
203
]
}
@@ -0,0 +1,32 @@
[
{
"id": "M1",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "804a86442527f2728f633f485eaeca02552df66a34350eac5101127046415188"
},
{
"id": "M2",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "1c2acb6397e29ab898d8a89bc5bc691a294be749ca7d7f7897c83ec1b3e9f9ef"
},
{
"id": "M3",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "954c9a9735904e1c566d844001b2e85c13cd16eb16e7b4b91b752974bb6dc754"
},
{
"id": "M4",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "953d91bf28073b11e4bc5a6da516a0db1b8dfff892083cd3e6fcebe9c47b6bbf"
},
{
"id": "M5",
"occurrences": 1,
"before": "f8fe1348796d40a096362fb0c6d5072c15ca68ff8fe10384a3eaa708eb92c150",
"after": "310c32b16f0fc554d5a7ffeeb0742ea7dd9fc84e1ddafabb25d7ca3f449865ef"
}
]
@@ -0,0 +1,37 @@
[
{
"id": "M1",
"file": "scripts/stack-release.sh",
"old": " [ \"$key\" != 1 ] || return 0\n if [ \"$key\" != 0 ]; then",
"new": " [ \"$key\" != 1 ] || return 0\n [ \"$extra\" = '\"console\"' ] || return 0\n if [ \"$key\" != 0 ]; then",
"killer": "config-any-key: a non-console target refusal"
},
{
"id": "M2",
"file": "scripts/stack-release.sh",
"old": "elif out=\"$(MOSAIC_CONFIG=\"$cfg\" node \"$loader\" validate 2>&1 >/dev/null)\"; then return 0",
"new": "elif out=\"$(MOSAIC_CONFIG=\"$cfg\" node \"$loader\" validate 2>&1 >/dev/null)\" || true; then return 0",
"killer": "config-any-key: a non-console target refusal"
},
{
"id": "M3",
"file": "scripts/stack-release.sh",
"old": " [ -n \"$extra\" ] || return 0",
"new": " return 0 # drop missing-loader later-key refusal",
"killer": "config-any-key: missing-loader copies"
},
{
"id": "M4",
"file": "scripts/stack-release.sh",
"old": " if [ \"$key\" != 0 ]; then\n check_refuse config-console-key",
"new": " if [ \"$key\" = 99 ]; then\n check_refuse config-console-key",
"killer": "config-any-key: missing config passes but malformed JSON refuses"
},
{
"id": "M5",
"file": "scripts/stack-release.sh",
"old": " if [ ! -f \"$loader\" ]; then",
"new": " [ -n \"$extra\" ] || return 0\n if [ ! -f \"$loader\" ]; then",
"killer": "config-any-key: accepting loaders run validate"
}
]
+230
View File
@@ -0,0 +1,230 @@
# 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: <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.
+60
View File
@@ -0,0 +1,60 @@
# Row 292 gate r2: the old-test red (read-only notes, no node)
Untracked until the results commit, so the round 1 digest 2fa7ae17 stays (sage-3185). Written for sage-3188.
## The red
Gate r2 on 0d0021454, cli suite stage, 04:54:03Z 10-23: 910 tests, 909 pass, 1 fail.
compactions-launch.test.mjs:206, row 270's store-unreadable test, fails at :229
`deepEqual(ends[0], ends[1])`. The first run ended `['session-invalid', 22, null]`, and the second
ended `['stopped', 0, null]`. Every assertion before :229 passed. Log: gizmo-292-out/r2/suite/cli.log:1023-1045.
## Where exit 22 comes from (read from the code, not run)
- packages/harness/src/runner.mjs:43 `EXIT.session` is 22. :55 `SESSION_GONE` is unauthenticated,
not-holder and run-ended. The runner exits 22 when a broker call gets one of those:
message.receive at :529, state at :550, the reply at :580, and the landing state at :505.
- packages/seat/src/session.mjs:340 maps 22 to "session-invalid" for the end event.
- The test's runs end by idle. The fixture sets `launch.threadIdleSeconds = 1`
(compactions-launch.test.mjs:72).
- The idle stop is in packages/cli/src/launcher.mjs:703-717. It calls the broker op `threadIdle`
first. packages/bus/src/broker.mjs:1044-1058 releases the claim in that call (`#claimEnd(...,
'release')` at :1055). After the op returns, the launcher records `thread-idle` and only then sends
SIGTERM (:716). The order is deliberate. Row 121 round 2, Dewey's F3 (broker.mjs:1040-1043), wants
a message sent after the snapshot to wait for the next holder.
- The window: the claim is already released, but the runner's SIGTERM handler hasn't set
`stopping` yet. A message.receive poll that the broker answers in that window gets `not-holder`.
Polls come every `pollInterval`, which defaults to 2000 ms (runner.mjs:57). The catch at
:526-531 sets `code = EXIT.session` without looking at `stopping`. A call already in flight when
the release lands ends the same way, even if SIGTERM arrives before its answer. Either way the run
ends session-invalid/22 instead of stopped/0.
## Reading
This is a race in the product's idle stop (launcher, broker and runner ordering), not in row 292,
and not in row 270's collection code. Row 270's test exposes it because :229 asserts that both runs'
ends are equal, which in practice means both are idle stops ending `stopped`. Row 292's diff
(stack-release.sh, stack-release.test.mjs, TOOLS.md, packets) doesn't reach any of these files.
I haven't confirmed this. The 10-run diagnostic Sage proposed to Mos (sage-3186) measures how
often it happens. Two fixes are possible if it reproduces, and both go to a separate row, as Sage
ruled:
- Product: in the runner, treat `not-holder` as stopped when it comes after a SIGTERM. That doesn't
close the window where the poll lands before SIGTERM, so a cleaner fix is for the launcher to
record that it is the one stopping the run, and map the end to stopped.
- Test: compare only the reason classes the test cares about at :229. That loosens an assertion,
which needs Mos under mos-1272.
## Mos's ruling and the diagnostic (mos-1411, sage-3190, CLEAR sage-3193)
Mos ruled that :229 stays as written and that the race gets its own row (owner Gizmoduck, reviewer Dewey,
Vikunja 588). 292's gate counts after one whole, unedited cli suite rerun, provided that rerun is green.
Diagnostic, 05:01:01Z to 05:03:31Z 10-23, on 0d0021454: compactions-launch.test.mjs whole and unedited,
10 isolated runs, each with its own TMPDIR. All ten exited rc=0 with 5/5 pass and no red, so `ends[0]`
was stopped/0 every time. A side loop copied each runner.log every 100 ms before the test removed its
root. 72 logs end `runner: exiting 0`. 27 were deleted before their last line was copied. One ends
`runner: session stopped working: not-holder` and then `runner: exiting 22`: run 2, launch
r846b8d083502, in the clear test (:305). That test doesn't compare ends, so it passed anyway. So the
race happens, roughly once in a hundred idle stops, and the code is `not-holder`, which matches the
reading above. Output: gizmo-292-out/diag.out and diag/run2/logs/r846b8d083502/runner.log.
@@ -0,0 +1,75 @@
# Row 292 (#1797) gate results
Tested head: 0d002145453adc6a47ecf0b8125ab3ee612c1a19 (sage-3185). That's 3fd8f42f6 rebased onto
c902a767d. It's on gizmoduck/row292-r2. The round 1 manifest digest is still 2fa7ae17 (13 paths). This
file and RED-r2.md came after the gate and sit outside the manifest.
## Rebase receipts (sage-3183 item 1)
`git range-diff -s b23783f39..3fd8f42f6 c902a767d..0d0021454` shows all six commits as `=`:
```
1: 3b17c7550 = 1: 89e58f4b1
2: a54baff21 = 2: 9f15216f7
3: 9c6e39b25 = 3: 61fdb6a8d
4: 4fa7d6834 = 4: b48e415e4
5: f8b9c2f73 = 5: 2db30c687
6: 3fd8f42f6 = 6: 0d0021454
```
Stable patch-ids are unchanged: 15f1a38e, 8e9b78ac (e7371025f), fc50b630 (d4c796e44), b5a6c8e8,
d918d3cd, 9e2b2c2b. Every one of the 13 manifest blobs on 0d0021454 matches the manifest.
## Gate r2 (04:33Z to 04:54Z 10-23, CLEAR sage-3183)
- stack-release.test.mjs whole: rc=0, 224/224, wall 608 s.
- cli suite whole: rc=1, 910 tests, 909 pass, 1 fail. The failure is in an old test,
compactions-launch.test.mjs:206 (row 270's store-unreadable test), at :229
`deepEqual(ends[0], ends[1])`. ends[0] was `['session-invalid', 22, null]` and ends[1] was
`['stopped', 0, null]`. Under mos-1272 the run stopped with the red unplaced. RED-r2.md has the
reading: the idle stop releases the claim before it sends SIGTERM, so a poll in between gets
`not-holder` and exits 22. Mos ruled in mos-1411 that :229 stays as written and the race gets its own
row, Vikunja 588.
## Diagnostic (mos-1411, CLEAR sage-3193)
05:01:01Z to 05:03:31Z: compactions-launch.test.mjs, whole and unedited, run 10 times, each with its own
TMPDIR. All ten passed with rc=0 and 5/5, so ends[0] was stopped/0 every time. In the runner.logs, 72
end in exit 0, 27 were deleted before the copy loop reached their last line, and one ended
`session stopped working: not-holder` then `exiting 22`. That one was run 2's launch r846b8d083502, in
the clear test (:305), which doesn't compare ends. So the race happens and the code is `not-holder`.
## Gate r3 (05:03:50Z to 05:16:32Z, run-gate-r3.sh)
- Stage 1 didn't rerun because it was green in r2.
- cli suite whole and unedited: rc=0, 910/910, 0 skipped, wall 630 s. Under mos-1411, this green rerun
is what makes 292's gate count.
- The ten scripts/test-*.sh all passed with rc=0:
| Suite | Result |
| --- | --- |
| auth | 15/0 |
| conductor | 17/0 |
| config | 55/0 |
| discord | 81/0 |
| extension-package | 18/0 |
| foundation | 44/0 |
| land-work | 86/0 |
| queue | node 156/156, suite 28/0 |
| release | 14/0 |
| task | 91/0 |
The tree was clean afterwards.
- There were two skips, both the usual ones. test-queue skipped verify/render --check because this
checkout isn't the canonical root. test-task skipped the live provider cases (row 244). No Docker group
was skipped.
- All of M1-M9 were killed by the rule: each had fail 1 on the named test, every listed stack line and
the kill text. Each restore checked sha256 8f535d53 and found the tree clean. No survivors.
- When it finished, no node process had its cwd in the worktree.
Logs are in /mnt/storage/scratch/gizmo-292-out: r2.out, r2/, diag.out, diag/, r3.out and r3/.
## For the landing note (mos-1411)
The first red was compactions-launch.test.mjs:229 in gate r2, with ends[0]
`['session-invalid', 22, null]`. The race is Vikunja 588. The gate counts on the green r3 cli rerun.
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
# Row 292 mutants. M1-M5 are Gander's (agents/gander/work/config-checks-any-key/changes.json), with
# the same edits on the resolved config_checks; M1 restores the console-only skip (the brief's
# required mutant). M6-M9 are the resolution's: row 203's console and federation text. `apply <id>`
# edits the worktree and prints the touched path; each edit asserts its anchor appears exactly once.
# `check` prints each anchor's count and kill. K is read from the tests, not run. A restore is `git checkout -- <path>`, then sha256 against the blob in BUILD.md.
import sys
WT = "/mnt/storage/scratch/gizmo-292/"
S = "scripts/stack-release.sh"
T = "packages/cli/tests/stack-release.test.mjs"
M = {
"M1": (S, ' [ "$key" != 1 ] || return 0\n if [ "$key" != 0 ]; then',
' [ "$key" != 1 ] || return 0\n [ "$extra" = \'"console"\' ] || return 0\n if [ "$key" != 0 ]; then'),
"M2": (S, 'elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0',
'elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)" || true; then return 0'),
"M3": (S, ' [ -n "$extra" ] || return 0', ' return 0 # drop missing-loader later-key refusal'),
"M4": (S, ' if [ "$key" != 0 ]; then\n check_refuse config-console-key',
' if [ "$key" = 99 ]; then\n check_refuse config-console-key'),
"M5": (S, ' if [ ! -f "$loader" ]; then', ' [ -n "$extra" ] || return 0\n if [ ! -f "$loader" ]; then'),
"M6": (S, 'Object.hasOwn(d,"federation")?"federation":', ''),
"M7": (S, 'const k=Object.hasOwn(d,"console")?"console":', 'const k='),
"M8": (S, ' if [ "$extra" = \'"console"\' ] || [ "$extra" = \'"federation"\' ]; then\n check_refuse',
' if [ "$extra" = \'"console"\' ]; then\n check_refuse'),
"M9": (S, '[ "$extra" = \'"federation"\' ]; then out="it has no scripts/mosaic-config.mjs"',
'[ "$extra" = \'"federation-off"\' ]; then out="it has no scripts/mosaic-config.mjs"'),
}
# K: (--test-name-pattern, the test.mjs lines the failing stack must show, the text the failure
# must show). Kill rule (sage-3152): the named test fails, its first error's stack names every
# listed line, and the log holds the text (no quote characters, which node's inspect may escape). A fail anywhere else, or a crash first, is a STOP to Sage.
# A survivor of its targeted run gets one whole-file run before it counts as a survivor.
K = {
"M1": ('^config-any-key: a non-console target refusal stops activate and rollback before any stop$', [517, 2619], "actual: 0"),
"M2": ('^config-any-key: a non-console target refusal stops activate and rollback before any stop$', [517, 2619], "actual: 0"),
"M3": ('^config-any-key: missing-loader copies accept only the four oldest keys$', [2670], "actual: 0"),
"M4": ('^config-any-key: missing config passes but malformed JSON refuses without calling the loader$', [2684], "actual: 0"),
"M5": ('^config-any-key: accepting loaders run validate with the selected config on activate and rollback$', [2654], "actual: []"),
"M6": ('^config-any-key: missing-loader copies accept only the four oldest keys$', [2677], "to validate configuration key: "),
"M7": ('^config-any-key: missing-loader copies accept only the four oldest keys$', [2677], "to validate configuration key: "),
"M8": ('^config-any-key: missing-loader copies accept only the four oldest keys$', [2677], "is refused by old"),
"M9": ('^config-any-key: missing-loader copies accept only the four oldest keys$', [2677], "to validate configuration key: "),
}
cmd = sys.argv[1]
if cmd == "list":
print(" ".join(M)); sys.exit(0)
if cmd == "check":
for k, (f, a, b) in M.items():
print(k, f, open(WT + f).read().count(a), "kill:", *K[k])
sys.exit(0)
f, a, b = M[sys.argv[2]]
s = open(WT + f).read()
assert s.count(a) == 1, (sys.argv[2], s.count(a))
open(WT + f, "w").write(s.replace(a, b))
print(f)
+8 -3
View File
@@ -632,9 +632,14 @@ onto the pointer's copy from the target, past any same-copy re-pin, checked
against the set's `SHA256SUMS`. `--send` and `--tester-port` follow the pointer.
When that set is missing, fails its sums, holds no readable argv or already ran
`--sso`, the run refuses `sso-rollback-flags`. Any activate or rollback also
refuses `config-console-key` before anything stops when the config holds a
`console` key the target's `scripts/mosaic-config.mjs validate` refuses (a copy
before row 192 refuses the key). The message says to remove the key first. So a
refuses `config-console-key` before anything stops when the target's
`scripts/mosaic-config.mjs validate` refuses the config, including a non-console
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. 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.
+85
View File
@@ -2598,3 +2598,88 @@ test('row196 rehearsal selects socket only for marked copies and uses scratch da
assert.equal(untouched(e),before);
});
});
// #1797: validate every config with the target loader, even without console.
function anyConfigLoader(file,text){
const old=existsSync(file)?readFileSync(file):null;
if(old!==null)chmodSync(file,0o644);
mkdirSync(dirname(file),{recursive:true});writeFileSync(file,text);
return ()=>{if(old===null)rmSync(file);else{writeFileSync(file,old);chmodSync(file,0o444);}};
}
const ANY_CONFIG_MSG=(e,to,op,why)=>`${e.cfg} is refused by ${to}'s loader (${why}); remove the refused key first (restore the config.json backup from the window's step 1), then run the ${op} again`;
test('config-any-key: a non-console target refusal stops activate and rollback before any stop',async t=>{
const e=await mk(t);await seed(e);
let r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
const why='mosaic-config: unsupported configuration key: "execution"';
const undo=anyConfigLoader(join(COPIES,B,'scripts/mosaic-config.mjs'),`console.error(${JSON.stringify(why)});process.exitCode=2;\n`);
try{
assert.equal(Object.hasOwn(JSON.parse(readFileSync(e.cfg,'utf8')),'console'),false);
for(const op of ['activate','rollback']){
const args=op==='activate'?['activate',B,'acme']:['rollback','acme'];
await refused(e,args,'config-console-key',ANY_CONFIG_MSG(e,B,op,why));
}
const out=await planOnly(e,3,'rollback','acme');
assert.deepEqual(out.filter(l=>l.startsWith('refuses ')),[`refuses config-console-key: ${ANY_CONFIG_MSG(e,B,'rollback',why)}`]);
}finally{undo();}
});
// Duckworth C1 (duckworth-0229): B's fixture loader (LOADER_OLD) is today's loader with a shorter
// first list, so it still reads the row 270 keys. This stand-in is C1's loader without the row 270
// concat, the list the loader had before bce491715 added the first of the two keys.
const PRE270=(now)=>{const old=now.replace('.concat(["compactionReadBytes", "compactionStreams"])','');assert.notEqual(old,now);return old};
test('config-any-key: a pre-270 target refuses each row 270 key on a full activate and rollback, naming the key',async t=>{
const e=await mk(t);await seed(e);
let r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
const original=readFileSync(e.cfg,'utf8'),now=join(COPIES,C1,'scripts/mosaic-config.mjs');
assert.equal(Object.hasOwn(JSON.parse(original),'console'),false);
const undo=anyConfigLoader(join(COPIES,B,'scripts/mosaic-config.mjs'),PRE270(readFileSync(now,'utf8')));
try{
for(const [key,value] of [['compactionReadBytes',1048576],['compactionStreams',1024]]){
writeFileSync(e.cfg,JSON.stringify({...JSON.parse(original),[key]:value}));
// C1's own loader reads the key, so the refusal is the target's.
const ok=spawnSync(NODE,[now,'validate'],{encoding:'utf8',env:{...e.env,MOSAIC_CONFIG:e.cfg}});assert.equal(ok.status,0,ok.stderr);
const why=`mosaic-config: unsupported configuration key: "${key}"`;
await refused(e,['activate',B,'acme'],'config-console-key',ANY_CONFIG_MSG(e,B,'activate',why));
await refused(e,['rollback','acme'],'config-console-key',ANY_CONFIG_MSG(e,B,'rollback',why));
}
}finally{undo();writeFileSync(e.cfg,original);}
r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);assert.equal(pointer(e).copy,B);
});
test('config-any-key: accepting loaders run validate with the selected config on activate and rollback',async t=>{
const e=await mk(t);await seed(e);const witness=join(e.root,'loader-calls.jsonl'),canary='resolved-config-must-not-leak';
const undos=[B,C1].map(id=>anyConfigLoader(join(COPIES,id,'scripts/mosaic-config.mjs'),`import fs from 'node:fs';fs.appendFileSync(${JSON.stringify(witness)},JSON.stringify({copy:${JSON.stringify(id)},args:process.argv.slice(2),config:process.env.MOSAIC_CONFIG})+'\\n');console.log(${JSON.stringify(canary)});\n`));
try{
for(const args of [['activate',C1,'acme'],['rollback','acme']]){
const r=await run(e,...args);assert.equal(r.code,0,r.out+r.err);assert.equal((r.out+r.err).includes(canary),false);
}
assert.deepEqual(lines(witness).map(JSON.parse),[
{copy:C1,args:['validate'],config:e.cfg},{copy:B,args:['validate'],config:e.cfg},
]);
}finally{for(const undo of undos)undo();}
});
function isolatedConfigChecks(e){
const source=readFileSync(SR,'utf8'),body=source.split('config_checks() {')[1]?.split('\n}\n')[0];assert.ok(body,'config_checks exists');
return spawnSync('bash',['-c','config_checks() {'+body+'\n}\ncheck_refuse(){ printf "%s: %s\\n" "$1" "$2" >&2; exit 3; }; config_checks'],{
encoding:'utf8',env:{...e.env,COPIES:join(e.root,'target-copies'),TO:'old',OP:'activate'},
});
}
test('config-any-key: missing-loader copies accept only the four oldest keys',async t=>{
const e=await mk(t);let r=isolatedConfigChecks(e);assert.equal(r.status,0,r.stderr);
const original=JSON.parse(readFileSync(e.cfg,'utf8'));
for(const [key,value] of [['compactionReadBytes',1048576],['compactionStreams',1024]]){
writeFileSync(e.cfg,JSON.stringify({...original,[key]:value}));r=isolatedConfigChecks(e);
assert.equal(r.status,3,r.stderr);assert.match(r.stderr,new RegExp(`configuration key: "${key}"`));
assert.ok(r.stderr.includes('restore the config.json backup from the window\'s step 1'));
}
// Row 203's text for a console or federation key stays word for word with no loader too, even
// when a later key comes first in the file.
for(const [key,value] of [['console',{}],['federation',{self:'hub',peers:[]}]]){
writeFileSync(e.cfg,JSON.stringify({...original,compactionStreams:1024,[key]:value}));r=isolatedConfigChecks(e);
assert.equal(r.status,3,r.stderr);assert.equal(r.stderr,`config-console-key: ${KEY_MSG(e,'old','activate','it has no scripts/mosaic-config.mjs')}\n`);
}
});
test('config-any-key: missing config passes but malformed JSON refuses without calling the loader',async t=>{
const e=await mk(t),file=join(e.root,'target-copies/old/scripts/mosaic-config.mjs'),witness=join(e.root,'validate-called');
mkdirSync(dirname(file),{recursive:true});writeFileSync(file,`import fs from 'node:fs';fs.writeFileSync(${JSON.stringify(witness)},'called');\n`);
rmSync(e.cfg);let r=isolatedConfigChecks(e);assert.equal(r.status,0,r.stderr);assert.equal(existsSync(witness),false);
writeFileSync(e.cfg,'{');r=isolatedConfigChecks(e);assert.equal(r.status,3,r.stderr);assert.match(r.stderr,/cannot be read as JSON/);assert.equal(existsSync(witness),false);
});
+21 -11
View File
@@ -1073,27 +1073,37 @@ slot_problem() {
[ "$size" != 0 ] || echo "is empty"
}
# Row 200 item 3 and the design's phase 2 rollback order: a config.json with a `console` key must be
# one the target's loader reads, or its bus, Console and seats fail on their next read. Runs the
# 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. 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 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")||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.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"
return 0
fi
if [ ! -f "$loader" ]; then out="it has no scripts/mosaic-config.mjs"
if [ ! -f "$loader" ]; then
[ -n "$extra" ] || return 0
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 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
}
# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose