Fix phantom drift in apm audit after clean install (#1978)#1986
Open
sergio-sisternes-epam wants to merge 2 commits into
Open
Fix phantom drift in apm audit after clean install (#1978)#1986sergio-sisternes-epam wants to merge 2 commits into
sergio-sisternes-epam wants to merge 2 commits into
Conversation
During drift replay, run_replay() passed the scratch tmpdir as project_root to integrate_package_primitives. The hook integrator's _is_root_local_package compared install_path (real project dir) against project_root (scratch tmpdir) -- they never match, so the source marker was computed as bare "name" instead of "_local/name". The JSON on disk (written during real install) had _local/name; scratch had name -- the diff fired and reported phantom drift. Fix: thread real_project_root: Path | None = None through the hook integration call chain so ownership identity checks evaluate against the actual project directory while all file I/O still targets the scratch dir. Changed files: - src/apm_cli/install/drift.py: pass real_project_root=project_root (the real project dir, not scratch) to integrate_package_primitives - src/apm_cli/install/services.py: accept real_project_root param; inject into _call_kwargs for the hooks primitive only - src/apm_cli/integration/hook_integrator.py: add real_project_root to integrate_hooks_for_target, integrate_package_hooks, _integrate_merged_hooks; use _eff_root = real_project_root or project_root for _get_package_name, _get_hook_source_marker, _is_root_local_package, _dependency_hook_sources - src/apm_cli/integration/kiro_hook_integrator.py: same pattern for integrate_kiro_hooks Tests: - unit: test_get_hook_source_marker_uses_real_project_root_not_scratch and parametrized test_real_project_root_fixes_source_marker_for_merge_targets covering all merge-hook targets (claude/codex/cursor/gemini/windsurf) - e2e: test_audit_no_drift_after_clean_install -- install then apm audit --ci must exit 0, parametrized across all merge-hook targets Fixes #1978 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a false-positive drift report in apm audit --ci immediately after a clean apm install by ensuring hook ownership identity checks compare against the real project root during drift replay (while replay file I/O still targets the scratch directory).
Changes:
- Thread
real_project_root: Path | Nonethrough the install integration pipeline and drift replay so hook integrators compute_apm_sourceconsistently with install-time behavior. - Update hook integration paths (merged-hook targets + Copilot + Kiro) to use an effective root (
real_project_root or project_root) for ownership/name decisions. - Add unit + integration regression tests covering the phantom drift scenario across merge-hook targets.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/integration/test_hook_integrator_defect_regression.py | Adds regression tests for #1978, covering source marker behavior during replay-like conditions. |
| tests/integration/test_hook_root_source_drift_e2e.py | Adds end-to-end test ensuring audit --ci reports no drift right after install. |
| src/apm_cli/integration/kiro_hook_integrator.py | Accepts real_project_root and uses it for package naming decisions during replay. |
| src/apm_cli/integration/hook_integrator.py | Threads real_project_root through hook integration and uses it for ownership/source marker decisions. |
| src/apm_cli/install/services.py | Passes real_project_root through to hook integration only (as a hooks-only kwarg). |
| src/apm_cli/install/drift.py | Supplies real_project_root=project_root when replaying integrations into a scratch root. |
- Drop tautological OR clause from pkg_name_scratch assertion; assert directly against pkg_info.install_path.name so the check can actually fail if fallback behaviour changes. - Fix docstring in test_audit_no_drift_after_clean_install: the test runs apm audit --ci, not apm audit --check drift. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
apm audit --cireported false drift on every merge-hook target (claude, codex, cursor, gemini, windsurf) immediately after a cleanapm install. Copilot was immune (per-file hook layout -- no_apm_sourcein JSON).Problem (WHY)
During drift replay,
run_replay()passed the scratch tmpdir asproject_roottointegrate_package_primitives. The hook integrator's_is_root_local_packagecomparedpackage_info.install_path(real project dir) againstproject_root(scratch tmpdir) -- they never match, so_get_hook_source_markerreturned the bare package name ("myapp") instead of the root-local marker ("_local/myapp").The JSON written to disk during a real install contains
"_apm_source": "_local/myapp". The scratch replay wrote"_apm_source": "myapp". The diff fired -- phantom drift on every target whose hooks live in a merged JSON config.Approach (WHAT)
Thread
real_project_root: Path | None = Nonethrough the hook integration call chain so ownership identity checks evaluate against the actual project directory while all file I/O still targets the scratch dir.Implementation (HOW)
All other integrators (prompt, agent, instruction, command, skill) are unaffected --
real_project_rootis injected into_call_kwargsonly when_prim_name == "hooks".sequenceDiagram participant R as run_replay participant S as services.py participant H as HookIntegrator R->>S: integrate_package_primitives(scratch_root, real_project_root=project_root) S->>H: integrate_hooks_for_target(project_root=scratch, real_project_root=real) H->>H: _eff_root = real_project_root or project_root H->>H: _is_root_local_package(install_path, _eff_root) -> True H->>H: _get_hook_source_marker(...) -> "_local/myapp" H->>H: write to scratch/...Trade-offs
real_project_rootisNonein all normal (non-replay) callers -- no behavioural change outside replay.C901complexity noqa onintegrate_package_primitiveswas extended (was PLR0913, now PLR0913,C901) since theif real_project_root is not Nonebranch crossed the threshold. The function's complexity was already accepted; this is the minimal acceptable extension.Validation evidence
How to test
Closes #1978