From 10ddefd88ad937cc6e414d4e7c3ca994e6ce4558 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 16:56:01 +0000 Subject: [PATCH] fix(scripts): make the R7a bash-3.2 control hold on a host that IS bash 3.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R7a probed `mapfile` itself to prove the BASH_ENV harness really removes a builtin. On stock macOS `/bin/bash` is 3.2, so `mapfile` is absent from the PLAIN run too — `plain=` came back empty exactly like `sim.out=`, the assertion could not be satisfied by any correct harness, and `check:objectui-changeset` sat red on every macOS seat while CI (ubuntu-latest, bash 5) stayed green. Option B: the probe builtin is now chosen at RUNTIME from bash 2.x-era builtins the host itself reports as builtins and that have no external `/usr/bin` twin, so `enable -n` on one yields the same 127 "command not found" a missing bash-4 builtin yields on 3.2. No skip leg: a host offering no qualifying builtin reddens loudly. A second leg, R7a2, asserts the state R7b actually depends on — mapfile unavailable in the R7b shell — which holds on both hosts for different reasons, so neither host can pass R7b vacuously. `scripts/bump-objectui.sh` is untouched; R7/R7b/R7c still redden when a bash-4 builtin is reintroduced into it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 --- scripts/objectui-changeset-digest.mjs | 82 ++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/scripts/objectui-changeset-digest.mjs b/scripts/objectui-changeset-digest.mjs index cd0600ce71..a1a0fb327d 100644 --- a/scripts/objectui-changeset-digest.mjs +++ b/scripts/objectui-changeset-digest.mjs @@ -2770,22 +2770,90 @@ function selfTest() { // script under test inherits it through `spawnSync`. const noBash4 = join(tmp, 'no-bash4-builtins.sh'); writeFileSync(noBash4, 'enable -n mapfile readarray 2>/dev/null\n'); + // The instrument must not be vacuous. If BASH_ENV were ever ignored (a // posix-mode bash, a future harness change), R7b would pass by proving // nothing, so the disabling is measured on a probe FIRST, both ways. + // + // ⭐ #12254 — that probe used to be `mapfile` ITSELF, which made this leg a + // GUARANTEED FAIL on the one platform it exists to model. On stock macOS + // `/bin/bash` IS 3.2, so `mapfile` is missing from the plain run too: the + // leg reported `plain=` empty exactly like `sim.out=`, "the simulation + // removed something" could not be satisfied by any correct harness, and + // `check:objectui-changeset` sat red on every macOS seat while CI + // (ubuntu-latest, bash 5) stayed green — so nothing would ever catch it. + // A control written to stop a vacuous pass had become a platform- + // conditional failure, which is the SAME error family one level up: a + // check whose own precondition was never measured across the population of + // hosts it runs on. + // + // The fix is to stop hardcoding a belief about another platform's builtin + // table and ASK THE HOST. `probeBuiltin` is chosen at runtime from bash + // 2.x-era builtins that (a) this shell reports as a builtin and (b) have no + // external `/usr/bin` twin to fall through to — so `enable -n` on one + // produces the very same 127 "command not found" that a missing bash-4 + // builtin produces on 3.2, and the leg measures the HARNESS instead of the + // platform. Deliberately not a skip: a host that offers no qualifying + // builtin fails this leg loudly, because a control that quietly does + // nothing is the exact shape R7a exists to prevent. + const probeCandidates = ['shopt', 'caller', 'compgen', 'hash', 'umask']; + const pickProbe = spawnSync( + 'bash', + [ + '-c', + 'for b in "$@"; do ' + + '[ "$(type -t "$b" 2>/dev/null)" = builtin ] || continue; ' + + '[ -n "$(type -P "$b" 2>/dev/null)" ] && continue; ' + + 'printf %s "$b"; exit 0; done; exit 1', + 'pick-probe-builtin', + ...probeCandidates, + ], + { encoding: 'utf8' }, + ); + const probeBuiltin = pickProbe.stdout.trim(); + const builtinProbe = join(tmp, 'builtin-probe.sh'); + writeFileSync(builtinProbe, `${probeBuiltin} >/dev/null\nprintf 'PROBE-STATUS=%s\\n' "$?"\n`); + const noProbeBuiltin = join(tmp, 'no-probe-builtin.sh'); + writeFileSync(noProbeBuiltin, `enable -n ${probeBuiltin} 2>/dev/null\n`); + const probePlain = spawnSync('bash', [builtinProbe], { encoding: 'utf8' }); + const probeSim = spawnSync('bash', [builtinProbe], { + encoding: 'utf8', + env: { ...process.env, BASH_ENV: noProbeBuiltin }, + }); + const probeStatus = (r) => (r.stdout.match(/PROBE-STATUS=(\d+)/) ?? [])[1]; + check( + '#12071/#12254 R7a the BASH_ENV harness really removes a builtin THIS host has (else R7b proves nothing)', + probeBuiltin !== '' && + probeStatus(probePlain) !== undefined && + probeStatus(probePlain) !== '127' && + probeStatus(probeSim) === '127' && + probeSim.stderr.includes(probeBuiltin), + `builtin=${probeBuiltin || ``} ` + + `plain=${probePlain.stdout.trim()} sim=${probeSim.stdout.trim()} sim.err=${probeSim.stderr.trim()}`, + ); + + // …and separately, the state R7b actually depends on: in the shell R7b + // runs, `mapfile` is unavailable. This holds on BOTH hosts and for + // DIFFERENT reasons — `enable -n` removed it on bash 5, it was never there + // on bash 3.2 — which is why it is its own leg rather than folded into the + // harness leg above: neither leg alone is unconditional, and together they + // leave no host on which R7b can pass vacuously. The detail line names + // which way this host got there, so a macOS operator reading a GREEN run + // still learns that their `/bin/bash` is the genuine 3.2 article. const mapfileProbe = join(tmp, 'mapfile-probe.sh'); writeFileSync(mapfileProbe, 'mapfile -t x < <(printf "a\\n") && echo MAPFILE-WORKS\n'); - const probePlain = spawnSync('bash', [mapfileProbe], { encoding: 'utf8' }); - const probeSim = spawnSync('bash', [mapfileProbe], { + const mapfilePlain = spawnSync('bash', [mapfileProbe], { encoding: 'utf8' }); + const mapfileSim = spawnSync('bash', [mapfileProbe], { encoding: 'utf8', env: { ...process.env, BASH_ENV: noBash4 }, }); + const hostHasMapfile = mapfilePlain.stdout.includes('MAPFILE-WORKS'); check( - '#12071 R7a the simulated-3.2 harness really removes the builtin (else R7b proves nothing)', - probePlain.stdout.includes('MAPFILE-WORKS') && - !probeSim.stdout.includes('MAPFILE-WORKS') && - probeSim.stderr.includes('mapfile'), - `plain=${probePlain.stdout.trim()} sim.out=${probeSim.stdout.trim()} sim.err=${probeSim.stderr.trim()}`, + '#12071/#12254 R7a2 … and in the R7b shell mapfile is gone, however this host got there', + !mapfileSim.stdout.includes('MAPFILE-WORKS') && mapfileSim.stderr.includes('mapfile'), + `host-has-mapfile=${hostHasMapfile} (${ + hostHasMapfile ? 'bash 4+, so enable -n has to remove it' : 'host bash is already 3.2-era, nothing to remove' + }) sim.out=${mapfileSim.stdout.trim()} sim.err=${mapfileSim.stderr.trim()}`, ); // The R2 shape again — pushed, never merged — through a shell that has no