Skip to content

fix(install): allow local packages at --global scope; fix broken tests#937

Merged
danielmeppiel merged 7 commits intomicrosoft:mainfrom
stbenjam:fix/global-scope-test
Apr 30, 2026
Merged

fix(install): allow local packages at --global scope; fix broken tests#937
danielmeppiel merged 7 commits intomicrosoft:mainfrom
stbenjam:fix/global-scope-test

Conversation

@stbenjam
Copy link
Copy Markdown
Contributor

@stbenjam stbenjam commented Apr 25, 2026

Summary

I spotted some failing tests that don't run in CI.

  • Remove the validation that rejected local filesystem packages when using --global. There is no technical reason local paths cannot be installed at user scope -- primitives are copied to ~/.claude/, ~/.gemini/, etc. the same way as at project scope.
  • Add test_global_scope_e2e.py to scripts/test-integration.sh so CI catches regressions in --global scope tests going forward. This file was never included in CI, so the broken test went undetected.
  • Fix test_user_scope_skips_workspace_runtimes which was missing the MCPServerOperations mock. The test passed in CI by accident: no runtimes are installed in CI, so MCPIntegrator.install() returns early at the scope-filtering stage before reaching the registry validation. Locally, if codex is installed, target_runtimes survives scope filtering and hits the real registry, which fails on the fake test/server name. The sibling test (test_project_scope_includes_all_runtimes) already had the correct mock.
  • Fix test_init_integration.py — PR feat(marketplace): fold marketplace.yml into apm.yml's 'marketplace:' block (closes #722, implements #1036) #1038 changed apm marketplace init to write apm.yml instead of marketplace.yml, but the tests still looked for the old filename and used legacy loaders. Updated to use apm.yml, load_marketplace_from_apm_yml(), and render_marketplace_block().
  • Fix test_outdated_integration.py — PR refactor: split marketplace commands into package modules #1024 changed apm marketplace outdated to exit 1 when upgradable packages exist, but the test still asserted exit code 0. Updated to match the new behavior.
  • Add tests/integration/marketplace/ to scripts/test-integration.sh so CI catches marketplace authoring test regressions going forward.

Test plan

  • pytest tests/unit tests/test_console.py tests/integration/test_global_scope_e2e.py -- 5459 passed, 0 failed
  • test_auto_bootstrap_creates_user_manifest now passes (previously broken since local-package rejection was added)
  • test_user_scope_skips_workspace_runtimes now passes deterministically regardless of locally installed runtimes
  • pytest tests/ -- 7452 passed, 181 skipped (excluding env-dependent e2e/live tests)
  • test_creates_apm_yml and all marketplace init tests pass against current init command
  • test_exit_code_one_when_upgradable passes against current outdated command

🤖 Generated with Claude Code

stbenjam and others added 2 commits April 25, 2026 10:30
Remove the validation that rejected local filesystem packages when
using --global. There is no technical reason local paths cannot be
installed at user scope -- the primitives are copied to ~/.claude/,
~/.gemini/, etc. the same way as at project scope.

The rejection was added as a precaution against relative-path
confusion, but absolute paths work fine and relative paths are
resolved during parsing anyway.

Also add test_global_scope_e2e.py to scripts/test-integration.sh
so CI catches regressions in --global scope tests going forward.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
test_user_scope_skips_workspace_runtimes was missing the
MCPServerOperations mock that the sibling test already had,
causing it to hit the real registry and fail on the fake
"test/server" name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 25, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict

Disposition: REQUEST_CHANGES (incomplete implementation -- three guards, one removed)


Per-persona findings

Python Architect:

The PR removes one guard in _validate_and_add_packages_to_apm_yml (commands/install.py) but leaves two downstream guards intact. The pipeline has three independent enforcement points for the same policy, and only one was addressed.

OO / class diagram

classDiagram
    direction LR
    class _validate_and_add_packages_to_apm_yml {
        <<IOBoundary>>
        +packages list
        +scope InstallScope
        +logger InstallLogger
    }
    class LocalPackageSource {
        <<Strategy>>
        +acquire() Materialization
    }
    class download_callback {
        <<Pure>>
        +dep_ref DependencyReference
        +modules_dir Path
    }
    class DependencyReference {
        <<ValueObject>>
        +is_local bool
        +local_path str
        +parse(dep_str) DependencyReference
    }
    class InstallScope {
        <<Enum>>
        PROJECT
        USER
    }
    class _copy_local_package {
        <<IOBoundary>>
        +dep_ref DependencyReference
        +install_path Path
        +project_root Path
    }
    _validate_and_add_packages_to_apm_yml ..> DependencyReference : parses
    _validate_and_add_packages_to_apm_yml ..> InstallScope : reads (guard REMOVED here)
    LocalPackageSource ..> InstallScope : reads (guard STILL PRESENT)
    download_callback ..> InstallScope : reads (guard STILL PRESENT)
    LocalPackageSource ..> _copy_local_package : delegates
    download_callback ..> _copy_local_package : delegates
    class _validate_and_add_packages_to_apm_yml:::touched
    class LocalPackageSource:::untouched
    class download_callback:::untouched
    classDef touched fill:#fff3b0,stroke:#d47600
    classDef untouched fill:#ffcccc,stroke:#cc0000
    note for LocalPackageSource "sources.py:141-153: still\nrejects USER scope with\n'not supported' diagnostic"
    note for download_callback "resolve.py:136-142: still\nadds to callback_failures\nfor USER scope"
Loading

Execution flow diagram

flowchart TD
    A["apm install --global ./local-pkg"] --> B["_validate_and_add_packages_to_apm_yml\ncommands/install.py"]
    B --> C["DependencyReference.parse(package)"]
    C --> D{is_insecure?}
    D -- No --> E["[BEFORE PR] is_local AND USER scope?\n-> validation_fail + reject"]
    E -- "REMOVED by PR #937" --> F["[FS] Add to apm.yml\napm.yml updated with dep"]
    D -- No --> F
    F --> G["LocalPackageSource.acquire()\nsources.py:118"]
    G --> H{scope is USER?}
    H -- YES --> I["[STILL PRESENT] diagnostics.warn\n'local paths are not supported at user scope'\nreturn None -- package NOT deployed"]
    H -- NO --> J["_copy_local_package()\nlocal_content.py:78"]
    J --> K["[FS] shutil.copytree to apm_modules/\nIntegration pipeline runs"]
    I --> L["apm.yml has entry,\nprimitives NOT deployed,\nexit code may be 0"]
    style E fill:#ffcccc,stroke:#cc0000
    style I fill:#ffcccc,stroke:#cc0000
    style L fill:#ffeecc,stroke:#cc7700
```

**Design patterns**

- Used in this PR: none -- straight-line guard removal in a procedural validation function, appropriate for the scope.
- Pragmatic suggestion: none -- the current shape (sequential validation guards + collected outcomes) is the simplest correct design. The fix is to remove all three guards consistently, not to introduce a new pattern.

**Verdict**: The change is architecturally incomplete. `sources.py:141-153` and `resolve.py:136-142` enforce the same restriction that was removed from `commands/install.py`. The result is a broken half-state: the manifest records the dependency, the package is never deployed, and the user sees a contradictory diagnostic that still says "not supported at user scope."

---

**CLI Logging Expert**:

The removed validation correctly used `logger.validation_fail(package, reason)` -- the right pattern. No logging anti-patterns were introduced.

The live concern is `sources.py:141-153`, which still emits:

```
"Skipped local package '{dep_ref.local_path}' -- local paths are not supported at user scope (--global). Use a remote reference (owner/repo) instead."
Loading

This message will fire even after this PR merges, directly contradicting the PR's intent. If local packages are truly allowed, that diagnostic must be removed (or reworded) in the same PR.

The E2E tests check result.stdout + result.stderr with substring assertions like "user scope" in combined.lower(), which would pass even when the diagnostic is a warning rather than a success message -- they do not distinguish success from failure output.


DevX UX Expert:

The intent -- aligning with npm install -g ./my-pkg / pip install -e . ergonomics -- is correct. The original restriction was genuinely surprising to package-manager-savvy developers.

However, the current implementation is UX-negative compared to the before state:

  • Before: apm install --global ./my-pkg fails immediately with a clear error: "not supported at user scope -- use owner/repo". The user knows exactly what to do.
  • After (as submitted): Validation passes, apm.yml is updated, then a buried diagnostic warning fires ("not supported at user scope"). The user's apm.yml now lists a dependency that was never deployed. This is the worst of both worlds: no upfront guidance, and a corrupted manifest state.

The fix must be complete (remove all three guards and verify _copy_local_package path resolution at user scope) before this improves the UX.

One path-resolution note for when the fix is complete: _copy_local_package resolves relative paths against project_root. At user scope, ensure project_root is Path.cwd() (the directory where the user ran apm install), not Path.home(). If project_root is ~ at user scope, ./my-pkg resolves to ~/.my-pkg which likely does not exist.


Supply Chain Security Expert:

The original restriction comment cited a real correctness risk: "relative paths resolve against cwd during validation but against $HOME during copy." Inspecting _copy_local_package (local_content.py:92-96):

local = Path(dep_ref.local_path).expanduser()
if not local.is_absolute():
    local = (project_root / local).resolve()

At project scope, project_root = Path.cwd() -- correct. At user scope, project_root needs to be checked: if it resolves to Path.home(), then ./my-pkg would expand to ~/.my-pkg and silently fail the is_dir() check. This is a correctness hazard, not a traversal hazard (absolute paths are safe; relative paths may silently miss the source).

No new path traversal or token leakage surface is introduced. The path_security guards in the integrator layer remain intact and are not bypassed by this change. The risk is bounded: the user is explicitly specifying a local path they control, and _copy_local_package applies resolve() which prevents ..-style traversal.

Verdict: Low security risk, but the project_root resolution at user scope must be verified before the downstream guards are removed.


Auth Expert: Not activated -- no auth-relevant files changed; the PR removes a scope restriction (is_local and scope is USER) in install.py that has no bearing on AuthResolver, token precedence, or credential resolution.


OSS Growth Hacker:

The goal -- apm install --global ./my-pkg just working -- is a friction reducer that would resonate with developers familiar with npm link, pip install -e ., or cargo install --path .. It reinforces the "package manager for AI-native development" frame.

But a half-baked relaxation that produces a manifest with an undeployed entry is a silent failure mode that would generate confused bug reports and erode trust. Fix it completely, then it deserves a Fixed CHANGELOG entry and potentially a mention in the release notes as a quality-of-life improvement.

Side-channel to CEO: once complete, this is a good "APM behaves like mature package managers" signal for the CHANGELOG. Not a standalone launch beat, but worth a clean release note line.


CEO arbitration

The PR's core intent is right -- apm install --global ./my-pkg should work, and the original restriction was unnecessarily conservative. All specialists agree on the direction. The problem is implementation completeness: the early validation guard was the least important of the three enforcement points, and removing only it produces a worse user experience than the original restriction. The sources.py:141-153 guard is the one that actually prevents deployment, and it still fires with stale "not supported" language. Merging as-is would add a confusing manifest-corruption failure mode to the product. Request the author complete the change -- remove all three guards, verify project_root resolution at user scope in _copy_local_package, update the stale diagnostic message, and add a CHANGELOG entry. The E2E test suite in test_global_scope_e2e.py is well-structured and will provide good regression coverage once the pipeline is actually unblocked end-to-end.


Required actions before merge

  1. Remove the guard in src/apm_cli/install/sources.py:141-153 (the LocalPackageSource.acquire() block that emits "local paths are not supported at user scope" and returns None). This is the guard that actually prevents deployment; leaving it makes the PR a no-op for real installs.

  2. Remove the guard in src/apm_cli/install/phases/resolve.py:136-142 (the download_callback block that adds to callback_failures for transitive local deps at user scope). Without this, transitive local deps would silently fail even if direct deps work.

  3. Verify project_root value at user scope in _copy_local_package (local_content.py:92-96): confirm that relative paths like ./my-pkg resolve against Path.cwd() (the directory where apm install was invoked), not Path.home(). Add a test case for a relative path with an explicit cwd different from fake_home to prevent this regression.

  4. Add a CHANGELOG.md entry under [Unreleased] -> Fixed, e.g.: Local packages (./path, /abs/path) now install correctly at user scope (--global); the earlier restriction was overly conservative. (#937)


Optional follow-ups

  • Add a unit test that directly calls LocalPackageSource.acquire() at InstallScope.USER with a local package, asserting it succeeds (not just that the manifest is written). This would have caught the incomplete guard removal.
  • Consider a [i] info message when a local package is installed at user scope clarifying where the primitives were deployed (e.g., [i] Local package deployed from /abs/path to ~/.claude/commands/).
  • The test_warns_about_unsupported_targets E2E test asserts "cursor" in combined.lower() -- this is a substring check against output that may contain URLs. Verify it is not flagging a URL fragment (CodeQL py/incomplete-url-substring-sanitization); if the word "cursor" only appears in prose, it is fine as-is.

Generated by PR Review Panel for issue #937 · ● 1.9M ·

Copy link
Copy Markdown
Collaborator

@danielmeppiel danielmeppiel left a comment

Choose a reason for hiding this comment

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

Great one but need adjustment per panel findings

@danielmeppiel danielmeppiel removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 25, 2026
@danielmeppiel
Copy link
Copy Markdown
Collaborator

Maintainer panel verdict (per #1064 FIX-NOW orchestration)

Ran the apm-review-panel locally with real-asset reproduction: apm install --global ./local-pkg against a scratch HOME.

Verdict: NEEDS-AUTHOR (the 4 prior reviewer asks remain UNADDRESSED on this branch)

Reproduction (PR is currently a no-op for its stated use case)

With your branch checked out:

HOME=<scratch> apm install --global ./local-pkg
=> exit 0
=> apm.yml lists `./local-pkg`
=> apm_modules/ is EMPTY

The manifest-vs-deployment desync is exactly what the prior bot panel called out -- the CLI-level guard is removed but the downstream guards still reject USER scope silently, so the install succeeds in the manifest sense but never deploys files.

Panel agrees with prior reviewer (CONFLICT RULE prevented silent override)

Local 7-persona panel reaches the SAME conclusion as the prior apm-review-panel bot panel that produced danielmeppiel's CHANGES_REQUESTED on 64cee38d. No conflict between panels; the 4 asks below stand.

Asks for @stbenjam (re-stating the 4 outstanding items)

  1. Remove the second guard at src/apm_cli/install/sources.py:141-153 that rejects USER scope for local sources -- the CLI-level guard removal is incomplete without this.
  2. Remove the third guard at src/apm_cli/install/phases/resolve.py:136-142 that blocks transitive local dependencies at USER scope.
  3. Verify _copy_local_package resolves project_root correctly at user scope -- when there is no project root (apm install --global from arbitrary cwd), the relative-source resolution needs an explicit base.
  4. Add a CHANGELOG.md [Unreleased] / Fixed entry for this fix.

Test evidence (your current HEAD)

unit (touched files): 75/75 pass
integration: 21 pass / 15 skip (token/binary-gated)
real-world: confirmed PR is a no-op for local --global install

Once the 4 items are addressed, ping and I'll re-run the panel + merge.

Copy link
Copy Markdown
Collaborator

@danielmeppiel danielmeppiel left a comment

Choose a reason for hiding this comment

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

Per maintainer panel verdict in this thread: 4 prior asks remain unaddressed; PR is verified no-op for local --global install. Re-requesting changes formally.

stbenjam and others added 2 commits April 30, 2026 12:07
# Conflicts:
#	src/apm_cli/commands/install.py
#	tests/unit/test_install_command.py
…tdated exit code

The init command now writes apm.yml instead of marketplace.yml, and
the outdated command exits 1 when upgradable packages exist. Update
tests to match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 30, 2026 12:23
@stbenjam
Copy link
Copy Markdown
Contributor Author

@danielmeppiel Ok so I fixed the comments, but new tests fail after rebasing (that I've also fixed here). Can you help me understand how tests in scripts/test-integration.sh are decided? The broken tests aren't triggered in CI, so they seem to get broken often.

Is it an oversight? Should we just have pytest run all integration tests instead of per-file list in the script?

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

This PR aims to make apm install --global accept local filesystem packages at user scope, and to tighten CI coverage by running the global-scope E2E test that previously was not executed in CI. It also updates marketplace integration tests to match current command behavior.

Changes:

  • Remove install-time validation that rejected local path packages when --global is used.
  • Add tests/integration/test_global_scope_e2e.py to scripts/test-integration.sh so CI runs global-scope E2E coverage.
  • Update marketplace integration tests to reflect current marketplace init (apm.yml marketplace block) and marketplace outdated exit code behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/apm_cli/commands/install.py Removes validation that previously rejected local deps at user scope.
tests/unit/test_install_command.py Removes unit tests that asserted local deps are rejected at --global scope.
scripts/test-integration.sh Runs the global-scope E2E test suite as part of integration test script.
tests/integration/marketplace/test_outdated_integration.py Updates expected exit code to 1 when upgradable packages exist.
tests/integration/marketplace/test_init_integration.py Updates tests to expect apm.yml scaffolding and marketplace block behavior.

Comment on lines +101 to 112
"""--force must overwrite an existing marketplace block."""
runner = CliRunner()
with runner.isolated_filesystem(temp_dir=str(tmp_path)) as cwd:
# First run
runner.invoke(init, [], catch_exceptions=False)
yml_path = Path(cwd) / "marketplace.yml"
yml_path.write_text("corrupted: true\n", encoding="utf-8")
yml_path = Path(cwd) / "apm.yml"
# Force overwrite
result = runner.invoke(init, ["--force"], catch_exceptions=False)
content = yml_path.read_text(encoding="utf-8")
assert result.exit_code == 0
# The scaffold must have replaced the corrupted content
assert "my-marketplace" in content
assert "marketplace:" in content

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

test_force_overwrites_existing no longer mutates apm.yml before invoking init --force, so it doesn't actually verify that --force overwrites an existing marketplace block (it may pass even if overwrite is broken). Consider writing an apm.yml with an existing marketplace: block containing different values, then assert those values change after --force.

Copilot uses AI. Check for mistakes.
Comment on lines 401 to 406
dep_ref.allow_insecure = True
_apm_yml_entries[canonical] = dep_ref.to_apm_yml_entry()

# Reject local packages at user scope -- relative paths resolve
# against cwd during validation but against $HOME during copy,
# causing silent failures.
if dep_ref.is_local and scope is not None:
from ..core.scope import InstallScope

if scope is InstallScope.USER:
reason = (
"local packages are not supported at user scope (--global). "
"Use a remote reference (owner/repo) instead"
)
invalid_outcomes.append((package, reason))
if logger:
logger.validation_fail(package, reason)
continue

# Check if package is already in dependencies (by identity)
already_in_deps = identity in existing_identities

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Removing the local-at-user-scope rejection here makes validation accept local filesystem deps for --global, but the install pipeline still skips/blocks them later (e.g. LocalDependencySource.acquire() and the resolve-phase download_callback both treat local deps at USER scope as unsupported). This can leave users with a ~/.apm/apm.yml that records a local path while nothing is actually installed/integrated. Either fully support local deps at USER scope (ensure relative paths resolve consistently, likely by canonicalizing to an absolute path before writing the user manifest and allowing the local copy path in resolve/integration), or keep rejecting them consistently with a single clear error path.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to 126
def test_exit_code_one_when_upgradable(self, tmp_path: Path):
"""outdated must exit 1 when upgradable packages exist."""
runner = CliRunner()
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

This test now asserts exit code 1 when upgrades exist, but the module-level docstring at the top of the file still states that the exit code is always 0. Update the docstring to match the new contract (exit 1 when upgradable packages exist).

Copilot uses AI. Check for mistakes.
@stbenjam
Copy link
Copy Markdown
Contributor Author

@danielmeppiel Ok so I fixed the comments, but new tests fail after rebasing (that I've also fixed here). Can you help me understand how tests in scripts/test-integration.sh are decided? The broken tests aren't triggered in CI, so they seem to get broken often.

Is it an oversight? Should we just have pytest run all integration tests instead of per-file list in the script?

Also I'd hold off on additional reviews until I sort out what my goal is in this PR. I'd like to solve the general integration test problem that I keep running into

@danielmeppiel
Copy link
Copy Markdown
Collaborator

Maintainer follow-up: lint + deeper finding + test-integration answer

Thanks for the rebase Stephen! Three things:

1. Lint failure (trivial, 1-line)

Lint is red on c9159802 because of a single missing blank line in tests/unit/test_install_command.py near line 838. Run:

uv run --extra dev ruff format tests/unit/test_install_command.py

That is the entire fix.

2. Real-asset reproduction: the user-scope local-pkg path is still broken

I reran the repro from my prior comment against your rebased branch, with an absolute path (so the relative-resolution concern in the sources.py guard doesn't apply):

HOME=$TMP/fakehome  apm install --global --verbose $TMP/local-pkg

Output:

[+] $TMP/local-pkg
[*] Updated apm.yml with 1 new package(s)
...
  Skipping $TMP/local-pkg (already failed during resolution)

  [+] <project root> (local)
[*] Installed 1 APM dependency.

Result: package is recorded in ~/.apm/apm.yml but no content is deployed under ~/.apm/. The user gets a green [+] and [*] Installed 1 APM dependency. while nothing actually shipped. This is worse than the pre-PR state -- now the manifest is poisoned.

The cause is the second of the three guards the panel called out:

src/apm_cli/install/sources.py:140-153
        if ctx.scope is InstallScope.USER:
            diagnostics.warn(
                f"Skipped local package ... -- local paths are not supported at user scope (--global). "
                "Use a remote reference (owner/repo) instead.",
                ...
            )
            return None

This guard fires unconditionally for InstallScope.USER -- it does not distinguish relative from absolute paths. Absolute paths are unambiguous (no $HOME-vs-cwd ambiguity), so the guard's stated rationale only applies to relative paths. The minimum fix is to scope the skip to relative paths only:

if ctx.scope is InstallScope.USER and not Path(dep_ref.local_path).is_absolute():
    # Skip relative-path local packages at user scope ...
    return None

Or to remove the guard entirely (if you trust upstream validation, which already absolutizes Click --global path args). Either is a small change but it is the change the panel was asking for -- the commands/install.py removal you did is necessary but not sufficient, because resolution happens later.

3. Your scripts/test-integration.sh question (good catch)

Today the script enumerates a hand-curated list of tests/integration/*_e2e.py files (search for pytest tests/integration/ in the file). New e2e modules are not picked up automatically. There is no policy reason for this -- it is an oversight; we just have not had a forcing function to fix it.

A pytest tests/integration -m e2e (or directory-glob with explicit excludes for token-gated suites like ADO) would absolutely be cleaner. That is out of scope for #937 but worth a separate one-line PR -- I would merge it.

What I need from you

  • Lint format fix (1 line).
  • One of the two sources.py guard adjustments above (or argue against it with a counter-repro showing my test is wrong).

Re-push and I will run the full panel + real-asset repro again. We are close.

The guard in sources.py:140 unconditionally blocked InstallScope.USER
local refs, so even `apm install --global /abs/path` recorded the
manifest entry but skipped deployment, leaving the user with a
poisoned manifest and no installed content. A parallel guard in
phases/resolve.py:131 dropped the same refs at resolution time.

Repro before fix:

  TMP=$(mktemp -d); HOME=$TMP/fakehome
  apm install --global $TMP/local-pkg --verbose
  # Verbose: 'Skipping ... already failed during resolution'
  # User: '[+] Installed 1 APM dependency'
  # find ~/.apm: manifest only, no deployed content

After fix (real repro stdout):

  [+] /tmp/.../local-pkg (local)
  [*] Installed 2 APM dependencies.
  # find ~/.apm: apm_modules/_local/local-pkg/.apm/instructions/
  #              test.instructions.md  (deployed)

Now only relative paths are rejected at user scope (project-relative
references are ambiguous outside a project; cwd is arbitrary and
$HOME is not a project root). Absolute paths are unambiguous and
flow through the same _copy_local_package path used at project scope.

Strengthens tests/integration/test_global_scope_e2e.py to assert
actual file deployment under ~/.apm/apm_modules/_local/, closing the
test gap that let the bug ship (the prior test only checked manifest
contents).

Co-authored-by: stbenjam <stbenjam@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator

Pushed lint fix + the missing sources.py guard adjustment (plus the parallel guard in phases/resolve.py) + E2E deployment assertion. Real-asset repro now shows content actually deploying under ~/.apm/apm_modules/_local/<pkg>/.apm/instructions/.

Heuristic chosen: only relative local paths are rejected at user scope (project-relative refs have no meaningful root when cwd is arbitrary and $HOME is not a project). Absolute paths flow through the same _copy_local_package path used at project scope.

Strengthened test_auto_bootstrap_creates_user_manifest to assert deployed file existence -- this would have caught the original bug (manifest-only check passed against broken behaviour).

Co-authored your name on the commit; happy to discuss the guard heuristic if you'd prefer a different shape.

@danielmeppiel danielmeppiel merged commit ab98a81 into microsoft:main Apr 30, 2026
10 checks passed
@stbenjam
Copy link
Copy Markdown
Contributor Author

I had imagined just resolving relative paths, but I don't have a use case for it, I was just trying to fix failing tests. Thanks!

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.

3 participants