mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* 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>
143 lines
4.1 KiB
Go
143 lines
4.1 KiB
Go
package ui
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/safedep/pmg/internal/shim"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// fakeChildExit satisfies the transparentExit interface, standing in for
|
|
// *runner.ChildExitError without a cross-package import.
|
|
type fakeChildExit struct {
|
|
code int
|
|
signaled bool
|
|
pmName string
|
|
scrubbed int
|
|
}
|
|
|
|
func (e *fakeChildExit) Error() string {
|
|
return fmt.Sprintf("%s exited with code %d", e.pmName, e.code)
|
|
}
|
|
func (e *fakeChildExit) ExitCode() int { return e.code }
|
|
func (e *fakeChildExit) Transparent() bool { return true }
|
|
func (e *fakeChildExit) IsSignaled() bool { return e.signaled }
|
|
func (e *fakeChildExit) ScrubbedEnvCount() int { return e.scrubbed }
|
|
|
|
func withVerbosity(t *testing.T, level VerbosityLevel) {
|
|
t.Helper()
|
|
prev := verbosityLevel
|
|
verbosityLevel = level
|
|
t.Cleanup(func() { verbosityLevel = prev })
|
|
}
|
|
|
|
func TestClassifyExit(t *testing.T) {
|
|
t.Run("genuine non-zero child exit prints a dim notice and mirrors the code", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelNormal)
|
|
|
|
d := classifyExit(&fakeChildExit{code: 1, pmName: "npm"})
|
|
|
|
assert.True(t, d.transparent)
|
|
assert.Equal(t, 1, d.code)
|
|
assert.True(t, d.notice)
|
|
assert.Equal(t, "↳ pmg: npm exited with code 1", d.message)
|
|
})
|
|
|
|
t.Run("transparent exit is detected even when wrapped", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelNormal)
|
|
|
|
wrapped := fmt.Errorf("failed to execute command: %w", &fakeChildExit{code: 2, pmName: "pnpm"})
|
|
d := classifyExit(wrapped)
|
|
|
|
assert.True(t, d.transparent)
|
|
assert.Equal(t, 2, d.code)
|
|
assert.True(t, d.notice)
|
|
})
|
|
|
|
t.Run("scrubbed env vars append a dim hint line", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelNormal)
|
|
|
|
d := classifyExit(&fakeChildExit{code: 1, pmName: "npm", scrubbed: 3})
|
|
|
|
assert.True(t, d.notice)
|
|
assert.Contains(t, d.message, "↳ pmg: npm exited with code 1\n")
|
|
assert.Contains(t, d.message, "sandbox scrubbed 3 env var(s)")
|
|
assert.Contains(t, d.message, "--sandbox-allow env=NAME")
|
|
})
|
|
|
|
t.Run("zero scrubbed env vars keep the notice to one line", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelNormal)
|
|
|
|
d := classifyExit(&fakeChildExit{code: 1, pmName: "npm"})
|
|
|
|
assert.NotContains(t, d.message, "scrubbed")
|
|
assert.NotContains(t, d.message, "\n")
|
|
})
|
|
|
|
t.Run("scrubbed hint is suppressed on signal exits", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelNormal)
|
|
|
|
d := classifyExit(&fakeChildExit{code: 130, signaled: true, pmName: "npm", scrubbed: 3})
|
|
|
|
assert.False(t, d.notice)
|
|
assert.Empty(t, d.message)
|
|
})
|
|
|
|
t.Run("scrubbed hint is suppressed in silent mode", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelSilent)
|
|
|
|
d := classifyExit(&fakeChildExit{code: 1, pmName: "npm", scrubbed: 3})
|
|
|
|
assert.False(t, d.notice)
|
|
assert.Empty(t, d.message)
|
|
})
|
|
|
|
t.Run("signal termination is silent but still mirrors the code", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelNormal)
|
|
|
|
d := classifyExit(&fakeChildExit{code: 130, signaled: true, pmName: "npm"})
|
|
|
|
assert.True(t, d.transparent)
|
|
assert.Equal(t, 130, d.code)
|
|
assert.False(t, d.notice)
|
|
assert.Empty(t, d.message)
|
|
})
|
|
|
|
t.Run("silent mode suppresses the notice", func(t *testing.T) {
|
|
withVerbosity(t, VerbosityLevelSilent)
|
|
|
|
d := classifyExit(&fakeChildExit{code: 1, pmName: "npm"})
|
|
|
|
assert.True(t, d.transparent)
|
|
assert.Equal(t, 1, d.code)
|
|
assert.False(t, d.notice)
|
|
})
|
|
|
|
t.Run("non-transparent error falls through to the loud path", func(t *testing.T) {
|
|
d := classifyExit(errors.New("some PMG failure"))
|
|
|
|
assert.False(t, d.transparent)
|
|
assert.False(t, d.notice)
|
|
})
|
|
}
|
|
|
|
func TestExitFromCommandErrorExitCode(t *testing.T) {
|
|
t.Run("missing package manager uses exit code 127", func(t *testing.T) {
|
|
err := &shim.BinaryNotFoundError{Name: "bun"}
|
|
|
|
var ec exitCoder
|
|
assert.True(t, errors.As(err, &ec))
|
|
assert.Equal(t, 127, ec.ExitCode())
|
|
})
|
|
|
|
t.Run("wrapped missing package manager preserves exit code 127", func(t *testing.T) {
|
|
err := fmt.Errorf("execute: %w", &shim.BinaryNotFoundError{Name: "npm"})
|
|
|
|
var ec exitCoder
|
|
assert.True(t, errors.As(err, &ec))
|
|
assert.Equal(t, 127, ec.ExitCode())
|
|
})
|
|
}
|