Files
pmg/internal/runner/execute_test.go
T
02965143d0 fix: fail fast when package manager is not installed (#360)
* fix: fail fast with clear error when package manager is not installed

When PMG shims intercept a package manager that isn't installed (e.g. a
clean JAMF-provisioned laptop where PMG is set up before dev tooling),
real-binary resolution failed with a generic "unknown" error and a
bug-report link, making it look like PMG itself had crashed.

Introduce a typed BinaryNotFoundError that exits with code 127 (standard
"command not found") and maps to a new PackageManagerNotFound error code
with an actionable message instead of the Unknown classification.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: improve missing package manager help message

Use a concise, dynamic install hint instead of explaining PMG's
PATH forwarding internals.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: mention PATH in missing package manager help text

Covers the common case where a package manager is installed but its bin
directory is not on PATH yet (e.g. after curl | bash install).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 18:31:18 +05:30

121 lines
2.9 KiB
Go

package runner
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/shim"
"github.com/safedep/pmg/packagemanager"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMergeEnvOverridesExistingValues(t *testing.T) {
env := mergeEnv(
[]string{
"PATH=/usr/bin",
"HTTP_PROXY=http://old-proxy",
"NO_PROXY=localhost",
},
[]string{
"HTTP_PROXY=http://pmg-proxy",
"HTTPS_PROXY=http://pmg-proxy",
"NO_PROXY=localhost,127.0.0.1",
},
)
assert.Equal(t, []string{
"PATH=/usr/bin",
"HTTP_PROXY=http://pmg-proxy",
"NO_PROXY=localhost,127.0.0.1",
"HTTPS_PROXY=http://pmg-proxy",
}, env)
}
func TestModeEnvOverrides(t *testing.T) {
opts := ExecuteOptions{
EnvOverrides: []string{"HTTP_PROXY=http://pmg-proxy"},
DirectEnvOverrides: []string{"CI=true"},
PTYEnvOverrides: []string{"TERM=xterm-256color"},
}
assert.Equal(t,
[]string{"HTTP_PROXY=http://pmg-proxy", "CI=true"},
modeEnvOverrides(opts, ExecutionModeDirect),
)
assert.Equal(t,
[]string{"HTTP_PROXY=http://pmg-proxy", "TERM=xterm-256color"},
modeEnvOverrides(opts, ExecutionModePTY),
)
}
func TestExecutionModeAuto(t *testing.T) {
assert.Equal(t, ExecutionModePTY, executionMode(ExecuteOptions{
Mode: ExecutionModeAuto,
IsInteractive: func() bool { return true },
}))
assert.Equal(t, ExecutionModeDirect, executionMode(ExecuteOptions{
Mode: ExecutionModeAuto,
IsInteractive: func() bool { return false },
}))
}
func TestExecuteWithOptionsRunsDirectHookBeforeSandbox(t *testing.T) {
cfg := config.Get()
previous := *cfg
t.Cleanup(func() {
*cfg = previous
})
cfg.Config.Sandbox.Enabled = true
cfg.Config.Sandbox.Policies = map[string]config.SandboxPolicyRef{}
exe, err := os.Executable()
require.NoError(t, err)
hookCalled := false
err = ExecuteWithOptions(context.Background(), &packagemanager.ParsedCommand{
Command: packagemanager.Command{
Exe: exe,
},
}, ExecuteOptions{
PackageManagerName: "npm",
Mode: ExecutionModeDirect,
BeforeDirectRun: func() error {
hookCalled = true
return nil
},
})
require.Error(t, err)
assert.True(t, hookCalled)
}
func TestExecuteWithOptionsMissingPackageManager(t *testing.T) {
tmpDir := t.TempDir()
pmgBin := filepath.Join(tmpDir, ".pmg", "bin")
require.NoError(t, os.MkdirAll(pmgBin, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(pmgBin, "bun"), []byte("#!/bin/sh\necho shim"), 0o755))
t.Setenv("PATH", pmgBin)
err := ExecuteWithOptions(context.Background(), &packagemanager.ParsedCommand{
Command: packagemanager.Command{
Exe: "bun",
Args: []string{"--version"},
},
}, ExecuteOptions{
PackageManagerName: "bun",
})
var notFound *shim.BinaryNotFoundError
require.ErrorAs(t, err, &notFound)
assert.Equal(t, "bun", notFound.Name)
assert.Equal(t, 127, notFound.ExitCode())
}