Skip to content

Commit c0827d6

Browse files
schmonzclaude
andcommitted
fix(bootstrap): a failed hasher now says WHICH way it failed, in one line
"(no 64-hex digest in its output)" was printed for a tool that is NOT INSTALLED, for one that RAN AND FAILED, and for one that RAN AND PRINTED SOMETHING ELSE. Three conditions, three different fixes, one message. On CI run 35537960554 it read as five missing tools on the Solaris guest and bought a whole cycle spent adding a sixth to the chain, when all five were present and working and the extractor was the bug. Worse, a tool that is not installed recorded NOTHING at all -- the `command -v` miss returned before touching KAT_TRIED -- so the chain's `sha256 -q` was simply absent from the guest's report with no explanation. One line per tool is kept; the line now distinguishes four verdicts: sha256sum -> ran but exited 1: sha256sum: unrecognized option shasum -a 256 -> ran (exit 0) but printed no 64-hex digest: MD5 (kat) = deadbeef sha256 -q -> command not found: no `sha256` on PATH digest -a sha256 -> ran (exit 0) but answered 0000...0000, which is not the known answer stderr is folded into the captured output deliberately: a tool that complains says why, and the extractor cannot be fooled by prose -- it wants exactly 64 hex characters. kat_snip takes the first line and truncates to 72 characters, so a chatty tool cannot turn one entry into a paragraph; the test asserts both the per-condition wording and that a three-line tool still yields one line. RED first: against the parent commit the case fails on the very first assertion -- a tool that is not installed does not say so. A diagnostic that cannot distinguish its own causes is a gate that cannot fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVks4skLBgHSDcjnQ4kHz6
1 parent 1ce54b2 commit c0827d6

2 files changed

Lines changed: 80 additions & 5 deletions

File tree

‎scripts/bootstrap-engine.sh‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,42 @@ hash_file() {
246246
first_hex64 "$(eval "$HASHER \"\$1\"" 2>/dev/null || :)" || :
247247
}
248248

249+
# ONE LINE, but a line that says WHICH failure this was. The old report printed
250+
# "(no 64-hex digest in its output)" for a tool that is NOT INSTALLED, for one that RAN
251+
# AND FAILED, and for one that RAN AND PRINTED SOMETHING ELSE -- three conditions with
252+
# three different fixes. Five identical lines read as five missing tools and bought a
253+
# whole CI cycle spent adding a sixth to the chain, when every one of the five was
254+
# present and working and the extractor above was the bug. A diagnostic that cannot
255+
# distinguish its own causes is a gate that cannot fail.
256+
kat_snip() {
257+
ks_s=${1%%"$HASH_NL"*}
258+
if [ -z "$ks_s" ]; then printf '(it printed nothing)'; return 0; fi
259+
printf '%.72s' "$ks_s"
260+
}
261+
249262
try_hasher() {
250263
th_word=${1%% *}
251-
command -v "$th_word" >/dev/null 2>&1 || return 1
252-
HASHER=$1
253-
th_got=$(hash_file "$TMP/kat")
254-
if [ "$th_got" = "$KAT_SHA" ]; then return 0; fi
264+
th_got=
265+
if ! command -v "$th_word" >/dev/null 2>&1; then
266+
th_why="command not found: no \`$th_word\` on PATH"
267+
else
268+
HASHER=$1
269+
# stderr is FOLDED IN deliberately: a tool that ran and complained says why, and
270+
# the extractor cannot be fooled by prose -- it wants exactly 64 hex characters.
271+
if th_raw=$(eval "$HASHER \"\$TMP/kat\"" 2>&1); then th_rc=0; else th_rc=$?; fi
272+
th_got=$(first_hex64 "$th_raw" || :)
273+
if [ "$th_got" = "$KAT_SHA" ]; then return 0; fi
274+
if [ "$th_rc" -ne 0 ]; then
275+
th_why="ran but exited $th_rc: $(kat_snip "$th_raw")"
276+
elif [ -z "$th_got" ]; then
277+
th_why="ran (exit 0) but printed no 64-hex digest: $(kat_snip "$th_raw")"
278+
else
279+
th_why="ran (exit 0) but answered $th_got, which is not the known answer"
280+
fi
281+
fi
255282
HASHER=
256283
KAT_TRIED="$KAT_TRIED
257-
$1 -> ${th_got:-(no 64-hex digest in its output)}"
284+
$1 -> $th_why"
258285
return 1
259286
}
260287

‎test/bootstrap-engine.test.cjs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,54 @@ exec "${realTr}" "$@"
749749
`a box whose hashers all work must resolve even when grep/tr are the legacy ones, got ${r.status}: ${r.err}`);
750750
});
751751

752+
// The diagnostic itself was the second half of that cost. "(no 64-hex digest in its
753+
// output)" was printed for a tool that is NOT INSTALLED, for one that RAN AND FAILED,
754+
// and for one that RAN AND PRINTED SOMETHING ELSE — three conditions with three
755+
// different fixes, reported identically. It read as "five missing tools" and bought a
756+
// whole CI cycle spent adding a sixth. One line per tool, but a line that DISTINGUISHES.
757+
shTest('a hasher that fails says WHICH way it failed: missing / non-zero / unparseable', () => {
758+
const d = mkdtemp();
759+
const { manifest: mf, base } = localPack(path.join(d, 'base'), { [hostTarget()]: FAKE_ENGINE(OK_TOKEN) });
760+
const run = (tool) => sh([], {
761+
CLODE_CACHE: path.join(d, `cache-${path.basename(tool)}`),
762+
CLODE_RELEASE_BASE: base,
763+
CLODE_BOOTSTRAP_MANIFEST: mf,
764+
CLODE_SHA256: tool,
765+
});
766+
767+
const missing = run(path.join(d, 'no-such-hasher-at-all'));
768+
assert.strictEqual(missing.status, 1, missing.err);
769+
assert.match(missing.err, /not found/i,
770+
'a tool that is not installed must SAY it is not installed, not "no 64-hex digest"');
771+
772+
const angry = run(fakeExe(path.join(d, 'angry'), '#!/bin/sh\necho "angry: I cannot read that" >&2\nexit 7\n'));
773+
assert.strictEqual(angry.status, 1, angry.err);
774+
assert.match(angry.err, /exited 7/,
775+
'a tool that ran and failed must report its exit status');
776+
assert.match(angry.err, /angry: I cannot read that/,
777+
'and what it said, so the reader does not have to reproduce it');
778+
779+
const chatty = run(fakeExe(path.join(d, 'chatty'), '#!/bin/sh\necho "MD5 (kat) = deadbeef"\n'));
780+
assert.strictEqual(chatty.status, 1, chatty.err);
781+
assert.match(chatty.err, /no 64-hex digest/,
782+
'a tool that ran fine but printed something else is the ONLY case that message fits');
783+
assert.match(chatty.err, /MD5 \(kat\) = deadbeef/,
784+
'and its actual output is the whole diagnostic value');
785+
786+
const liar = run(fakeExe(path.join(d, 'liar2'), `#!/bin/sh\necho ${'a'.repeat(64)}\n`));
787+
assert.strictEqual(liar.status, 1, liar.err);
788+
assert.match(liar.err, /not the known answer/i,
789+
'a well-formed WRONG digest is a lying tool, not an unparseable one');
790+
791+
// One line per tool is the constraint that keeps the report readable when six are
792+
// tried; a multi-line stub must not become a multi-line entry.
793+
const wordy = run(fakeExe(path.join(d, 'wordy'), '#!/bin/sh\nprintf "line one\\nline two\\nline three\\n"\n'));
794+
assert.strictEqual(wordy.status, 1, wordy.err);
795+
const entries = wordy.err.split('\n').filter((l) => l.includes(' -> '));
796+
assert.strictEqual(entries.length, 1, `expected one line for the one tool tried, got ${entries.length}`);
797+
assert.ok(!wordy.err.includes('line two'), 'the entry must be one line, so only the first is quoted');
798+
});
799+
752800
// ---------------------------------------------------------------------------
753801
// dash, when this box has one. The scar: shell discovery needs a shell BY NAME,
754802
// and /bin/sh behaviours vary. A resolver that only ever ran under this Mac's sh

0 commit comments

Comments
 (0)