diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 71786f5e..e7a0ca73 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -247,8 +247,21 @@ def _validate_package_exists(package, verbose=False): # For virtual packages, use the downloader's validation method if dep_ref.is_virtual: - downloader = GitHubPackageDownloader() - return downloader.validate_virtual_package_exists(dep_ref) + ctx = auth_resolver.resolve_for_dep(dep_ref) + host = dep_ref.host or default_host() + org = dep_ref.repo_url.split('/')[0] if dep_ref.repo_url and '/' in dep_ref.repo_url else None + if verbose_log: + verbose_log(f"Auth resolved: host={host}, org={org}, source={ctx.source}, type={ctx.token_type}") + downloader = GitHubPackageDownloader(auth_resolver=auth_resolver) + result = downloader.validate_virtual_package_exists(dep_ref) + if not result and verbose_log: + try: + err_ctx = auth_resolver.build_error_context(host, f"accessing {package}", org=org) + for line in err_ctx.splitlines(): + verbose_log(line) + except Exception: + pass + return result # For Azure DevOps or GitHub Enterprise (non-github.com hosts), # use the downloader which handles authentication properly diff --git a/tests/unit/test_install_command.py b/tests/unit/test_install_command.py index 4d65946e..e5cb15d5 100644 --- a/tests/unit/test_install_command.py +++ b/tests/unit/test_install_command.py @@ -314,6 +314,47 @@ def test_verbose_validation_failure_calls_build_error_context(self, mock_urlopen assert call_args[0][0] == "github.com" # host assert call_args[0][1].endswith("owner/repo") # operation + def test_verbose_virtual_package_validation_shows_auth_diagnostics(self): + """When virtual package validation fails in verbose mode, auth diagnostics are shown.""" + from apm_cli.commands.install import _validate_package_exists + + with patch( + "apm_cli.deps.github_downloader.GitHubPackageDownloader.validate_virtual_package_exists", + return_value=False, + ), patch.object( + __import__("apm_cli.core.auth", fromlist=["AuthResolver"]).AuthResolver, + "resolve_for_dep", + return_value=MagicMock(source="none", token_type="none", token=None), + ) as mock_resolve, patch.object( + __import__("apm_cli.core.auth", fromlist=["AuthResolver"]).AuthResolver, + "build_error_context", + return_value="Authentication failed for accessing owner/repo/skills/my-skill on github.com.\nNo token available.", + ) as mock_build_ctx: + result = _validate_package_exists("owner/repo/skills/my-skill", verbose=True) + assert result is False + mock_resolve.assert_called_once() + mock_build_ctx.assert_called_once() + call_args = mock_build_ctx.call_args + assert call_args[0][0] == "github.com" # host + assert "owner/repo/skills/my-skill" in call_args[0][1] # operation + + def test_virtual_package_validation_reuses_auth_resolver(self): + """Virtual package validation should pass its AuthResolver to the downloader.""" + from apm_cli.commands.install import _validate_package_exists + + with patch( + "apm_cli.deps.github_downloader.GitHubPackageDownloader.__init__", + return_value=None, + ) as mock_init, patch( + "apm_cli.deps.github_downloader.GitHubPackageDownloader.validate_virtual_package_exists", + return_value=True, + ): + _validate_package_exists("owner/repo/skills/my-skill", verbose=False) + mock_init.assert_called_once() + # The auth_resolver kwarg should be passed (not creating a new one) + _, kwargs = mock_init.call_args + assert "auth_resolver" in kwargs + # --------------------------------------------------------------------------- # Transitive dep parent chain breadcrumb