Skip to content

fix(hooks): make apm install idempotent for hook entries#709

Open
srid wants to merge 3 commits intomicrosoft:mainfrom
juspay:fix/hook-idempotent-install
Open

fix(hooks): make apm install idempotent for hook entries#709
srid wants to merge 3 commits intomicrosoft:mainfrom
juspay:fix/hook-idempotent-install

Conversation

@srid
Copy link
Copy Markdown

@srid srid commented Apr 14, 2026

Description

Re-running apm install appended a duplicate hook entry per package on every run because _integrate_merged_hooks extended the per-event list unconditionally.

Upsert by _apm_source before appending — the cleanup code path already treats that marker as the ownership key; the install path now matches.

Fixes #708

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality (if applicable)

srid added 2 commits April 14, 2026 09:10
Re-running `apm install` appended a duplicate hook entry per package on
every run because `_integrate_merged_hooks` extended the per-event list
unconditionally.

Upsert by `_apm_source` before appending — the cleanup code path
already treats that marker as the ownership key; the install path now
matches.

Fixes microsoft#708.
Adds two regression tests for microsoft#708 under
TestClaudeIntegration:

* test_reinstall_is_idempotent — three consecutive
  integrate_package_hooks_claude() calls produce a byte-identical
  settings.json with a single Stop entry.
* test_reinstall_heals_preexisting_duplicates — a settings.json seeded
  with three duplicate entries for one _apm_source plus an unrelated
  user entry collapses to one APM entry and preserves the user entry.

Both tests fail against the pre-fix commit and pass against the
upsert-before-extend implementation in _integrate_merged_hooks, so
they lock in the behaviour for all merge-style hook targets (Claude,
Cursor, Codex) that share the same code path.
Copilot AI review requested due to automatic review settings April 14, 2026 13:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR fixes duplicate hook entries when re-running apm install by making merged-hook integration idempotent via _apm_source-based upsert semantics (regression fix for #708).

Changes:

  • Add unit tests covering reinstall idempotency and healing of preexisting duplicates in Claude settings.
  • Update _integrate_merged_hooks to remove previously integrated entries for the same package before appending new ones.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
tests/unit/integration/test_hook_integrator.py Adds regression tests ensuring reinstall doesn’t duplicate hooks and collapses legacy duplicates.
src/apm_cli/integration/hook_integrator.py Implements _apm_source-based “upsert” behavior during merged-hook integration to avoid duplicates.

srid added a commit to juspay/kolu that referenced this pull request Apr 14, 2026
**Bare `apm install` duplicates hook entries in `.claude/settings.json`
on every re-run**, so kolu's `apm` recipe papers over it by wiping
`.claude/` before each install and `apm-update` does `rm -f
settings.json` for the same reason. The real bug is
[microsoft/apm#708](microsoft/apm#708) — the
merge integrator `.extend()`s the per-event hook list unconditionally
instead of upserting by `_apm_source`.

Pin `apm_cmd` to microsoft/apm#709, which
filters existing entries owned by the package before appending fresh
ones. Fix is eight lines in `hook_integrator.py` plus two regression
tests covering idempotent re-integration and healing of pre-existing
duplicates. _Both workarounds in `ai.just` — the wholesale `.claude/`
wipe and the settings.json `rm -f` — are dropped; `apm install` is now
safe to run over a live tree._

The workflow instructions also pick up a small fix: `/do` now knows to
invoke the `/test` and `/ci` skills for the test and CI pipeline steps
(previously only `just check` / `just fmt` were documented, leaving
those steps without a verified execution path).

> Revert `apm_cmd` back to `microsoft/apm` once #708 lands upstream.
Branch comment in `ai.just` flags this.
Review feedback on microsoft#709: the per-hook-file loop was filtering
owned entries on every iteration, so a package contributing to the
same event from multiple hook files would lose earlier files'
freshly-appended entries when the next file's iteration re-ran the
filter.

Track cleared `(event_name)` in a set and skip filtering on
subsequent iterations. Add a regression test with two hook files
targeting `Stop`, verifying both entries survive install and
repeated re-installs.

Also add a defensive `parent.mkdir(parents=True, exist_ok=True)`
in the heal test before seeding settings.json, per review feedback.
@srid
Copy link
Copy Markdown
Author

srid commented Apr 14, 2026

Thanks for the review @copilot — all three addressed in ac7a840:

Multi-hook-file regression (hook_integrator.py:542) — good catch. The filter ran inside the per-hook-file loop, so a package contributing to the same event from two JSON files would lose the first file's entries on the second iteration. Fixed by tracking cleared event_names in a set and skipping subsequent filters.

Multi-file test (test_hook_integrator.py:597) — added test_reinstall_preserves_multiple_hook_files_same_event. It drops two hook files (hooks-a.json, hooks-b.json) both targeting Stop under the same package, asserts both entries survive install, and that the combined output is byte-identical across re-runs. Verified the test fails without the cleared_events gate (only the second file's entry survives) and passes with it.

Defensive parent.mkdir (test_hook_integrator.py:658) — added. The fixture already creates .claude/, so the test was passing, but the guard is cheap and makes the test robust to fixture changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-running apm install duplicates hook entries in settings.json

2 participants