Skip to content

Fix the two pre-existing test failures on develop (stack 6/6) - #47

Closed
martient wants to merge 2 commits into
claude/ai-agents-capabilities-report-fgpp3p-ci-cleanupfrom
claude/ai-agents-capabilities-report-fgpp3p-test-fixes
Closed

martient wants to merge 2 commits into
claude/ai-agents-capabilities-report-fgpp3p-ci-cleanupfrom
claude/ai-agents-capabilities-report-fgpp3p-test-fixes

Conversation

@martient

@martient martient commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #46. Fixes the two failures flagged throughout this series as pre-existing, and adds the behaviour matrix for the guardrail introduced in #46.

Result: 421 passed, 0 failed — parallel and --test-threads=1.


1. tag_tests::test_tag_respects_repo_git_overrides_and_cli_can_override_them — root-caused and fixed

The test points gpg.program at a script that always fails and expects tag creation to fail with it, proving Committy passes repo git config through.

It was not a Committy bug. Committy passed the overrides correctly. The test depends on ambient host config:

  • gpg.program is only consulted when gpg.format is openpgp.
  • This machine's /root/.gitconfig sets gpg.format=ssh and gpg.ssh.program.
  • So git routed signing to the SSH signer, the failing stub never ran, and git tag -a succeeded.

Confirmed directly — the tag git produced carried a valid -----END SSH SIGNATURE-----, and committy config show proved both overrides were loaded and forwarded. The test then reported "Committy ignored the override" when Committy had done exactly the right thing.

The test now pins gpg.format=openpgp alongside the other overrides, so it exercises the path it means to exercise regardless of host.

This one is demonstrated: failure reproduced, cause identified, fix verified.

2. config::hierarchy::tests::test_repository_config_overrides_user — hardened, but causality unproven

All three config::hierarchy tests call MergedConfig::load, which reads the user config from $HOME. The tests in src/config.rs mutate HOME and are #[serial]; these three were not. serial_test only serialises tests that opt in, so a hierarchy test could read a relocated or half-written user config while one of those held it — and MergedConfig::load(...).unwrap() panics on the resulting Err. That matches the observed symptom.

They are now #[serial] too.

⚠️ I could not reproduce the original failure. It happened once. Across thirteen subsequent runs — eight lib-only, five full-suite — it did not recur, including with this fix reverted. So the mechanism is reasoned from the code, not demonstrated by a red-to-green transition.

I'm keeping the change because it's correct and costs nothing: a test reading global state that other tests mutate belongs in the same serial group. But it should be read as hardening against a plausible race, not as a verified fix. If it ever recurs, the .unwrap() at hierarchy.rs:230 is where to look.

3. Behaviour matrix for the guardrail — and three defects it caught

.claude/hooks/block-dangerous-git.sh (rewritten in #46) shipped unvalidated: nothing executed it, so its deny rules were assumed. That matters more than for ordinary code, because a guardrail that silently stops refusing looks exactly like one that works.

tests/guardrail_hook_tests.rs feeds the hook the same JSON the harness does and asserts the exit code, across 12 cases. Writing it found three defects, all fixed here:

  • git push -f origin main was not refused. The force check was one ordered regex whose -f alternative required a preceding space that the push match had already consumed. Force detection is now two independent conditions — a push, and a force flag anywhere — so flag order and adjacency stop mattering.
  • A heredoc feeding an interpreter was dropped along with every other heredoc body, so bash <<EOF / git reset --hard / EOF sailed through. Bodies are still discarded when the opener writes to a file, but scanned when it names a shell or interpreter.
  • Quoted prose naming a dangerous command is still refused. Deliberate, and now recorded as a decision: stripping quoted segments would fix it and would also let bash -c "git reset --hard" through. For a guardrail a false refusal costs far less than a false pass.

Coverage includes the variants the previous substring hook missed entirely — doubled spaces, a leading env assignment, git -C ., and a git call after &&.

4. CI was not running on this stack at all

lint.yml filtered pull_request: branches: ["*"], and in Actions branch filters * does not match /. Every stacked PR targets a claude/… base, so #43–#47 had zero check runs — silently, since a workflow that does not trigger looks identical to one with nothing to report. Only #42 (base develop) ever ran.

Fixed to ["**"], which is what the filter meant. It would equally have hidden CI on anything under a feat/ or fix/ prefix. The fix is merged down the stack so every PR in the series now gets checks.

@martient
martient added this pull request to stack #48 September 20, 2026 17:40
@martient martient self-assigned this Sep 20, 2026
@martient martient added the bug Something isn't working label Sep 20, 2026
test_tag_respects_repo_git_overrides_and_cli_can_override_them asserts
that a repo-configured gpg.program is consulted, by pointing it at a
script that always fails and expecting the tag creation to fail with it.

That only holds when gpg.format is openpgp. On a machine whose global
config sets gpg.format=ssh, git routes signing to gpg.ssh.program, the
stub never runs, the tag is signed successfully by the ssh signer, and
the assertion evaporates -- the test fails claiming Committy ignored the
override when Committy had passed it through correctly. Verified: the tag
git produced in that environment carried a valid SSH signature.

The test now pins gpg.format=openpgp alongside the other overrides, so it
exercises the path it means to exercise regardless of the host.

Separately, the three config::hierarchy tests call MergedConfig::load,
which reads the user config from $HOME. The tests in config.rs mutate
HOME and are marked #[serial]; these were not, so they could read a
half-written or relocated user config while one of those held it, and
MergedConfig::load(...).unwrap() would panic. They are now serial too.

Caveat on that second change: it closes a real race but is unproven. The
failure was observed once, and did not recur in thirteen subsequent runs
(eight lib-only, five full) even with the fix reverted, so causality is
reasoned rather than demonstrated. It is cheap and correct regardless --
a test reading global state that other tests mutate belongs in the same
serial group.
…t caught

The rewritten guardrail shipped unvalidated: nothing executed it, so its
deny rules were assumed rather than checked. That matters more than for
ordinary code, because a guardrail that silently stops refusing looks
exactly like one that works.

Twelve tests feed the hook the same JSON the harness does and assert the
exit code. Writing them found three defects in the hook, all now fixed:

- `git push -f origin main` was not refused. The force check was one
  ordered regex whose `-f` alternative required a preceding space, which
  the `push ` match had already consumed. Force detection is now two
  independent conditions -- a push, and a force flag anywhere -- so flag
  order and adjacency stop mattering.
- A heredoc feeding an interpreter was dropped along with every other
  heredoc body, so `bash <<EOF` / `git reset --hard` / `EOF` sailed
  through. Heredoc bodies are still discarded when the opener writes to a
  file, but scanned when it names a shell or interpreter.
- Quoted prose naming a dangerous command is still refused. This one is
  deliberate and now recorded as a decision: stripping quoted segments
  would fix it and would also let `bash -c "git reset --hard"` through,
  and for a guardrail a false refusal costs far less than a false pass.

Coverage includes the variants the previous substring hook missed
entirely -- doubled spaces, a leading env assignment, `git -C .`, and a
git call after `&&`.

Copy link
Copy Markdown
Owner Author

Superseded by #49, which carries these commits unchanged. Closing — see #42 for why the stack was collapsed.


Generated by Claude Code

@martient martient closed this Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants