You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix pre-merge-commit hook to stage fixes when diagnostics remain (#232)
* test: reproduce hook bug where fixes aren't staged when fix exits 1
The merge queue's pre-merge-commit hook silently drops files modified
by 'mdsmith fix .' whenever fix returns exit code 1 (unfixed
diagnostics remain). This was observed on bisect branch
merge-queue/batch-bisect-224-1777817057 (SHA b1ade01) where
mdsmith fix regenerated PLAN.md to reflect plan/120's new status
but the change never reached the merge commit.
Root cause: the canonical hook script uses
if ! '$exe' fix .; then
status=$?
if [ "$status" -ne 1 ]; then
exit "$status"
fi
fi
POSIX '! cmd' returns the logical NOT of cmd's exit status, so when
mdsmith fix exits 1, $? immediately after is 0. The script then
captures status=0, the [ 0 -ne 1 ] test is true, and the hook exits
0 BEFORE running the 'git add' staging loop.
The new test stands up a tiny git repo with a fake mdsmith that
modifies a tracked file and exits 1, runs the canonical hook, and
asserts the modified file ends up in the index. The test fails
today, demonstrating the exit-code clobbering bug.
https://claude.ai/code/session_01C4i4wEMhsfuoTDrq54QvaU
* fix: capture mdsmith fix exit status before negation in hook
POSIX 'if ! cmd; then status=$?; ...' clobbers cmd's exit code: $?
after '! cmd' is the logical NOT of cmd's exit status, so when
mdsmith fix exits 1 (unfixed diagnostics remain), status captures 0.
The follow-up '[ "$status" -ne 1 ]' is then true and the hook exits
0 before the 'git add' staging loop ever runs.
The visible symptom on the merge queue: PLAN.md (and other catalog
files) get regenerated by mdsmith fix in the working tree, but the
merge commit ships the stale pre-fix content because nothing
staged the fixes. CI on the resulting commit then fails on MDS019,
even though the same fix run locally succeeds.
The new template wraps the fix invocation in 'set +e' / 'set -e' and
captures $? directly, then propagates any exit status that is
neither 0 (clean) nor 1 (unfixed remain) so genuine config errors
or panics still abort the merge:
set +e
'$exe' fix .
status=$?
set -e
if [ "$status" -ne 0 ] && [ "$status" -ne 1 ]; then
exit "$status"
fi
HookMatchesCanonical and the cmd/mdsmith install tests are updated
to match the new fragments. The previously-failing reproduction
test in internal/githooks/githooks_unix_test.go now passes.
https://claude.ai/code/session_01C4i4wEMhsfuoTDrq54QvaU
* Replace script-content assertions with golden file + behavior tests
Content checks on BuildHookScript's output (Contains calls looking for
specific shell fragments) were brittle: any whitespace or wording change
broke them. Replace with:
- A golden-file test in internal/githooks that byte-compares
BuildHookScript("/usr/local/bin/mdsmith") against
testdata/pre-merge-commit.golden.sh; run with UPDATE_GOLDEN=1 to
regenerate the golden file when the template changes.
- Equality checks in the install/ensure tests that compare the written
hook content against githooks.BuildHookScript (the canonical output),
so they verify behavior (correct file written) without embedding
shell fragments inline.
- The e2e install test drops content checks entirely; exact rendering
is covered by the golden-file test; the e2e test confirms the file
exists and is executable.
https://claude.ai/code/session_01C4i4wEMhsfuoTDrq54QvaU
* Add e2e behavioral tests covering all pre-merge-commit hook branches
The hook has four observable branches:
1. fix exits 0, nothing modified → staging loop runs, nothing staged
2. fix exits 0, files modified → staging loop stages them
3. fix exits 1 (unfixed diagnostics remain) → staging loop still runs
and stages the fixable changes (regression for bisect-branch bug)
4. fix exits anything else → hook propagates the code, aborting merge
Each branch now has a dedicated e2e test in
TestE2E_PreMergeCommitHook_*. Tests 1–3 install the hook via
`mdsmith pre-merge-commit install` (real binary) and exercise it
in a real git repo with real markdown files. Test 4 installs a fake
mdsmith that exits 2 to verify the propagation path.
https://claude.ai/code/session_01C4i4wEMhsfuoTDrq54QvaU
* Require set +e in HookMatchesCanonical drift check
Without this, a hook that invokes `mdsmith fix .` under active set -e
(without the set +e guard) would pass the drift check even though it
would abort immediately when fix exits 1 — silently skipping the
staging loop that re-stages the fixed files.
Add "set +e" to the required fragments so any installed hook missing
the errexit-disable guard is flagged as drifted and prompts reinstall.
Add TestHookMatchesCanonical_RejectsMissingSetPlusE to pin the contract.
https://claude.ai/code/session_01C4i4wEMhsfuoTDrq54QvaU
* Replace fragment-based HookMatchesCanonical tests with golden files and behavioral test
Each bad-hook scenario now lives as a complete shell script under
testdata/hooks/bad/. A single table-driven test reads every file in
that directory and asserts HookMatchesCanonical returns false, replacing
nine inline string-concatenation tests.
A new behavioral test (TestHookScript_MissingSetPlusE_FailsToStageOnExitOne)
runs the missing-set-plus-e golden file in a real git repo and asserts the
file is NOT staged when fake mdsmith exits 1 — documenting the original
merge-queue bug and proving the drift detection is meaningful.
https://claude.ai/code/session_01C4i4wEMhsfuoTDrq54QvaU
* Assert unfixable diagnostics remain after ExitOneStagesFixed hook run
The test claimed mdsmith fix exits 1 due to an unfixable diagnostic but
never verified that the diagnostic actually remains after the hook. Add a
mdsmith check assertion after the hook to confirm a non-zero exit, ensuring
the test is not vacuous if the fixture's chosen rule ever becomes fixable.
https://claude.ai/code/session_01C4i4wEMhsfuoTDrq54QvaU
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments