fix(hooks): a locate miss is an answer, not a locate failure - #37
fix(hooks): a locate miss is an answer, not a locate failure#37KageBinary wants to merge 2 commits into
Conversation
Ix#539 will make `ix locate <missing>` exit non-zero while still printing its JSON body. The hook already survives that -- it redirects stdout to a temp file and reads the file regardless of exit status -- but `wait || ix_capture_async` would file an error for every miss. Every symbol a user asks about that does not happen to exist would land in the error ledger, burying real failures. Only an empty body is a genuine failure now; a non-zero exit that still produced output is logged as a miss and used. Two things this turned up: `wait` cannot be tested bare here. These hooks run under `set -euo pipefail`, so an unguarded non-zero `wait` kills the hook outright -- my first attempt did exactly that and took 19 of the 83 tests with it. `_loc_status=0; wait || _loc_status=$?` is both set -e-safe and keeps $?, which `if ! wait` would have replaced with the negation's own result. The mock needed a new knob rather than reusing IX_MOCK_FAIL: that one suppresses output as well as failing, and the whole point here is a failing exit code *with* a usable payload. Hence IX_MOCK_LOCATE_EXIT. Of the three new tests, one pins the change (the miss is logged as a miss); the other two are guards -- they pass today and would catch a refactor that stopped reading the body, or one that swallowed genuine failures. Refs ix-infrastructure/Ix#539
|
Heads-up on a latent bug in the mock change here — I hit the identical one writing #38 and only found it by accident.
[ -n "${IX_MOCK_LOCATE_EXIT:-}" ] && exit "${IX_MOCK_LOCATE_EXIT}"When It is invisible in this PR precisely because the fix makes the hooks tolerate a non-zero exit with a body. I found mine only when I reverted The fix is to use if [ -n "${IX_MOCK_LOCATE_EXIT:-}" ]; then exit "${IX_MOCK_LOCATE_EXIT}"; fi#38 does it that way and carries a comment explaining why, so if this lands first the two will disagree on style in the same file — worth aligning either way. Worth doing before merge: as it stands, this branch's mock cannot tell you whether the |
`IX_MOCK_LOCATE_EXIT` was written as
[ -n "${IX_MOCK_LOCATE_EXIT:-}" ] && exit "${IX_MOCK_LOCATE_EXIT}"
When the variable is unset, `[ -n "" ]` is false, so the `&&` list evaluates to
status 1. It is the last command in the `locate)` branch, so that becomes the
mock's own exit status: `ix locate` exited 1 on **every** call, in every test,
not only the ones setting the variable.
Demonstrated on this branch before the change:
$ bash tests/mock-ix.sh locate >/dev/null 2>&1; echo $? -> 1
$ bash tests/mock-ix.sh overview >/dev/null 2>&1; echo $? -> 0 (untouched)
The suite stayed green either way, which is the whole problem: this PR's fix
makes the hooks tolerate a non-zero exit with a body, so nothing complained
while every locate-driven test silently ran against a failing `ix locate`. The
cost was the *success* path -- no test exercised a locate that exits 0, so a
regression there would not have been caught.
An `if` with no else is status 0 when the condition is false.
Verified after the change:
unset -> 0, =1 -> 1, =3 -> 3, and the body is still printed before exiting
and the suite is 86 passed / 0 failed with the mock fixed.
Also re-ran this PR's own mutation check, which the bug had made impossible to
trust: with `hooks/ix-lib.sh` reverted to main and the tests kept, exactly one
test fails -- `intercept/non-zero locate with a body is a miss, not a failure`.
So the coverage here is genuine; only the harness was wrong.
|
Pushed the fix to this branch ( Confirmed the bug on this branch before touching it
What it actually cost, having measured rather than assumedThe suite is 86 passed / 0 failed both before and after the mock fix, so the bug was never causing a failure. What it removed was the success path: with Your coverage is genuine — I re-ran the check the bug had made untrustworthyMy first attempt at this was itself a no-op, worth flagging: Exactly one test fails, and it is the right one. The fix in this PR is properly pinned; only the harness was wrong. Note on #38I hit the identical bug writing #38 and fixed it there the same way, for |
Step 1 of ix-infrastructure/Ix#539, which names this plugin's
ix-bash.shas one of the consumers that must tolerate a non-zeroix locatebefore the CLI starts producing one.What actually breaks here
Not what I expected. The hook already survives a non-zero exit — it redirects stdout to a temp file and reads the file regardless of status, so the body is never lost.
The problem is the ledger.
wait "$_LOC_PID" || ix_capture_async ... "locate failed"files an error for every miss. Once #539 lands, every symbol a user asks about that does not happen to exist becomes a recordedlocate failed, burying the real failures.Now only an empty body is a genuine failure; a non-zero exit that still produced output is logged as a miss and used.
Two things worth flagging
waitcannot be bare here. These hooks run underset -euo pipefail, so an unguarded non-zerowaitkills the hook outright. My first attempt did exactly that and took 19 of the 83 tests with it. The suite caught it immediately — which is a good argument for #36, since nothing gates these tests in CI yet.The shape that works is
_loc_status=0; wait || _loc_status=$?— set -e-safe, and it keeps$?, whichif ! waitwould have replaced with the negation's own result.The mock needed a new knob.
IX_MOCK_FAILsuppresses output as well as failing, and the whole point of #539 is a failing exit code with a usable payload. HenceIX_MOCK_LOCATE_EXIT=N, which exits N after printing the fixture.Tests
86 passed, 0 failed (83 existing + 3 new).
Being precise about what they buy: reverting the fix fails exactly one —
non-zero locate with a body is a miss, not a failure. The other two pass on the old code too and are deliberate guards: one locks in that the body still reaches the user, the other that an empty body stays a real failure. They would catch a refactor to command substitution, which is the obvious way someone reintroduces this.Verified with
bash -nandshellcheck -S error.