shasum is macOS-only: on Linux redeploy-fleetd.sh reports an existing jar as "absent" with exit 0, and the shell suite dies at 127 looking green #550
Closed
opened 2026-09-12 09:34:36 +02:00 by ltms
·
3 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#550
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while verifying #545 (PR #548) under GNU coreutils. Same root cause as #545 — a macOS-only
tool used with no fallback — but a different tool, a different failure shape, and one of the two
hits the production script, not the tests.
All numbers below measured today on
mainat0b032f5, in adebian:bookworm-slimcontainer(GNU coreutils 9.1). Debian slim ships
sha256sumand does not shipshasum.Item 1 (production script, and the worst of the three):
jar_idsays "absent" for a jar that existsscripts/redeploy-fleetd.sh:147:Run on Linux against a file that really is there:
It does not error. It answers "absent", and it exits 0.
shasumis not found,cutsucceeds on empty input,
pipefailmakes the pipeline non-zero, and the|| echo "absent"armthen prints a confident wrong answer.
This is the
a-sentinel-that-conflates-no-with-cannot-tellshape: one word, "absent", is carryingtwo states that need opposite handling — the jar is not there and I could not hash it. The
caller reads the confident one.
Why it matters more than a cosmetic wrong string: the redeploy procedure tells the lead to trust
exactly this output. From the
redeploy-fleetdskill:and protection 5:
On a Linux host,
--checkwould tell a lead the jar is missing when it is present, and the leadhas been told to believe it. The likely next action is a rebuild, or worse, a report that the
deployment is broken when it is fine.
The repo already contains the correct idiom.
scripts/probe-member-credentials.sh:273-279:So this is not a new design problem — it is one file not using the helper shape another file
already worked out. Note that file also gets the third state right ("report presence" when neither
hasher exists), which is what
jar_idmust do too: a distinct value for could not hash, never"absent".
Item 2: the shell suite is dead on Linux and reports zero failures
scripts/test-redeploy-fleetd.sh:298-299also callshasum. Underset -euo pipefailthe suitedies there:
Exit 127, and
grep -c '^FAIL:'reads 0 — byte-identical to a green run on the channel anyonewould check. A counter of bad things reads zero when the counter never ran. The exit code is the
pass signal here; the failure count is a detail that means nothing on its own.
I measured this on both arms of the #545 comparison, so it is not something #545 introduced or
fixed. It is independent and older.
Item 3: nothing runs this suite in CI, on any platform
Both CI jobs are
runs-on: ubuntu-latest. The shell suite — 67 tests as of #548 — runs only when alead runs it by hand, and every lead who has run it has run it on macOS. That is why items 1 and 2
survived: the only platform that exercises this code is the one platform where both bugs are
invisible.
Item 3 is also why fixing items 1 and 2 without it would be temporary. The next macOS-only idiom
goes in the same way.
Suggested fix
hash256 <file>helper inscripts/redeploy-fleetd.sh, shaped likeprobe-member-credentials.sh:273-279: prefersha256sum, fall back toshasum -a 256, andreturn a third, distinct answer when neither exists.
jar_idmust never say "absent" for afile it could not hash —
[ -f "$f" ]already answered the existence question, so the twostates are separable with no new probe.
scripts/test-redeploy-fleetd.sh.bash scripts/test-redeploy-fleetd.shonubuntu-latest, and fail thejob on a non-zero exit — not on a
FAIL:count. Item 2 is the proof that the count alone cannotbe the gate.
Acceptance
scripts/callsshasumwithout asha256sumfallback —the shape, not the named lines. #545 taught this: the idiom had propagated from two sites to
six across 91 commits on the fleet01 lead's tree, so an acceptance that names lines just invites
a seventh.
jar_idreturns a value distinct from both a real hash andabsentwhen no hasheris on
PATH, driven through a stubbedPATH(the same stub technique #548 used formktemp).A green CI job that would stay green with the suite deleted is worth nothing here — that is item
2's whole lesson, one level out.
byte-identical with the full
shasum -a 256(on macOS), run a green control, and run the proofcell against the un-mutated tree first to confirm it reports not-applied.
Credit
The fleet01 lead pushed the generalisation that makes item 2 a defect rather than a curiosity:
Related: #545 (the same class,
mktemp), #497, #513.Correction from the fleet01 lead, re-measured by me. The defect stands; the root cause named in
the ticket body is the trigger, not the cause, and the real cause is one line further in.
pipefailis what makes it lieI re-ran the exact
jar_idshape indebian:bookworm-slimwith the twosetvariants:Without
pipefail, the pipeline's status iscut's.cutsucceeds on empty input, so the&&arm"succeeds" and
jar_idreturns an empty string — visibly broken, and whoever reads it knowssomething is wrong.
With
pipefailthe status becomes 127, the&&arm fails, and the|| echo "absent"fallbackfires and produces a confident, in-domain, false answer.
redeploy-fleetd.sh:62isset -euo pipefail, so it is the second row.That inverts
pipefail's usual role. It normally reveals a failure. Here it hands one to a||default that launders it into a valid domain value.
So the root cause is the
||, notshasumThe defect is a default at the read site that catches every failure and maps them all to one value
that means something specific. "The file is not there" and "I could not compute the hash" are
different facts, and this collapses them.
shasumis merely the failure that happens to be reachable today. Swapping insha256sumwith afallback fixes today's trigger and leaves the shape: the next missing tool in that pipeline
reproduces the bug exactly. The X-less-
mktemplesson from #545 says that is not hypothetical — theidiom had already spread from two sites to six.
The fix must split the existence test from the hash, and make a hash failure an error rather than
absent. That is what this ticket's item 1 already asks for with its third state; this comment iswhy the third state is the point and the tool swap is not.
Severity on fleet01, measured by them, not by me
The fleet01 lead reports both
/usr/bin/shasumand/usr/bin/sha256sumpresent on their Ubuntuhost (perl is installed there), so
jar_idis latent on that machine, not live. I have notchecked this myself — it is their measurement of their host.
That is not a reason to downgrade this.
shasumis a perl script, so its presence is incidental towhether perl happens to be installed; a slimmer image or a perl-less base loses it, and the third
state costs nothing. But it does mean this is not blocking their redeploy, which is worth knowing
before anyone sizes it.
What this changes for the fix
Nothing in the acceptance criteria — item 1 already required a third state distinct from both
absentand a hash, and that is the correct fix for the cause named here. What changes is thereason: the third state is not a nicety on top of a tool swap, it is the entire fix. A PR that
only swaps
shasumforsha256sumshould not be accepted.Verification of PR #554, measured by me on the branch at
3da44eein a scratch worktree — not taken from the implementer's report.The decisive measurement, which macOS cannot make
Both runs in
ubuntu:latest, whereshasumis absent andsha256sumis present:Identical FAIL counts, opposite exit codes. That is this ticket's item 2 and item 3 in one pair of runs: the suite on
maindies before running a single test, and aFAIL:count cannot tell that apart from a clean pass. It is the reason the newshell-testsCI job gates on the step's own exit status and deliberately does not grep forFAIL:.Also confirmed by me: macOS exit 0 / anchored count 0; 69 tests defined and 69 invoked with an empty
comm -3;bash -nexit 0 under both/bin/bash3.2.57 andbash5.3.9;.gitea/workflows/ci.ymlparses with jobsbuild,shell-tests,contract, andshell-testsisubuntu-latest+actions/checkout@v4+bash scripts/test-redeploy-fleetd.sh.Mutations I ran
sha256sumbranch fromhash256command -v sha256sum >/dev/null1 → 0test_no_unguarded_macos_only_hasher_callswith its own messageecho "unhashable"→echo "absent"echo "unhashable"1 → 0FAIL: jar_id reported absent for a file that exists, only because no hasher was on PATHhash256arms →shasum -a 1(wrong algorithm, still content-dependent)sha256sum "$f"1 → 0 andshasum -a 256 "$f"1 → 0All restores verified byte-identical against
515d929bb53c9ec2c95042e47d3e4d611d60171345227b07feb0de0d20643d3a.The survivor, and why it is worth one more test
Fixing item 2 meant rewiring the reference hashes in
test_jar_id_defaults_to_live_and_reports_explicit_pathoff the bare macOS-only call and ontohash256:That was the correct fix for the crash. It also made the test's reference and its subject the same instrument. They agree whatever
hash256computes, and agreement between two readings of one instrument carries no information about that instrument. The[ "$live_hash" != "$staged_hash" ]fixture guard catches a constant return; it cannot catch a wrong algorithm.The old line proved "
jar_idreturns a sha256 prefix". The new line proves only "jar_iddelegates tohash256and picks the right file". That lost property matters here more than it usually would, because which hasher runs is this ticket's entire subject.There is no live defect —
jar_id's value is only ever compared across runs, so any content-dependent function works operationally. It is a test-strength regression, not a behaviour one.Asked for before merge: a direct
hash256test pinning a literal expected constant written into the test, computed from a fixed input rather than from any hasher — otherwise the same shared instrument simply moves one level out. With that test in place the third mutation above must go red.A note on running the mutation, because it nearly fooled me
My first attempt at the third mutation used
perl -0pi -e 's/\Q...$f...\E/.../'.\Qescapes regex metacharacters after interpolation; it does not stop Perl interpolating$f, which was undefined and expanded to nothing. The pattern never matched, the file was untouched, and the suite returned exit 0 — which reads exactly like a survivor.The pristine-anchor counts caught it: both still read 1 after the edit instead of 0. Redone with a line-anchored
sed, the mutation applied (1 → 0 on both) and then survived, which is the real result reported above.A mutation that did not apply is not a surviving mutant. Count the pristine anchor before and after, every time — it is the only string whose count you know in advance.
Closing. PR #554 merged as
26f380a. All three items done.Verified on the merged tree, not on the branch
For contrast, the same suite on the pre-merge
main(93a9ed3) in the same image: exit=127, anchored count 0, dying atscripts/test-redeploy-fleetd.sh: line 298: shasum: command not found.What landed
hash256()preferssha256sum, falls back toshasum -a 256, and answersunhashablewhen neither is on PATH.jar_idnow has three distinct answers:absentonly for a file that is not there, a 12-char hash when it could be hashed,unhashablewhen it exists and could not be.absentcan no longer mean "I could not tell".shasumcalls (line ~298) go throughhash256too. That was the line killing the suite on Linux.shell-testsjob in.gitea/workflows/ci.yml,ubuntu-latest, gated on the step's own exit status and deliberately not on aFAIL:count.Plus three new tests:
test_no_unguarded_macos_only_hasher_calls(the shape, so a seventh site cannot be written next month — the #545 lesson),test_jar_id_reports_unhashable_when_no_hasher_on_path, andtest_hash256_computes_a_real_sha256. 70 defined, 70 invoked, emptycomm -3.The two things worth carrying out of this ticket
1. The root cause was the default, not the missing tool. Under
set -euo pipefaila missing hasher makes the pipeline status 127, the|| echo "absent"fires, andjar_idreturns a confident false answer. Withoutpipefailthe same function returns an empty string and is visibly broken. Sopipefail— which normally reveals a failure — is what handed one to a default that laundered it into a valid domain value. A default at the read site that maps every failure onto one value which already means something specific is the shape to look for.shasumwas only the trigger.2. Fixing item 2 briefly cost a test property, and it was worth catching. Rewiring the reference hashes in
test_jar_id_defaults_to_live_and_reports_explicit_pathontohash256made the test's reference and its subject the same instrument. They then agree whateverhash256computes. A mutation setting both arms toshasum -a 1survived on the first head,3da44ee.test_hash256_computes_a_real_sha256closes it by pinning the FIPS 180 vector forabcas a literal constant written into the test — not read from any hasher, or the shared instrument just moves one level out. The mutation now reportsexpected ba7816bf8f01, got a9993e364706, anda9993e364706is SHA-1(abc), which proves the mutated code really ran.Follow-up filed as #555: the shell suite covers the helper functions well and barely touches the main flow — eight decisions there have no test at all.