Files
2026-05-27 17:43:00 -05:00

534 lines
15 KiB
Go

package main
import (
"os"
"strings"
"sync"
"testing"
"time"
)
// TestPkgInstallManyBatchesDnf verifies a single batched dnf call rather
// than one per package.
func TestPkgInstallManyBatchesDnf(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
var calls [][]string
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls = append(calls, append([]string(nil), argv...))
return CmdResult{ExitCode: 0}
}
failed := pkgInstallMany([]string{"git", "curl", "vim"})
if len(failed) != 0 {
t.Errorf("expected no failures, got %v", failed)
}
if len(calls) != 1 {
t.Fatalf("expected exactly 1 batched call, got %d: %v", len(calls), calls)
}
got := strings.Join(calls[0], " ")
if !strings.HasPrefix(got, "dnf install -y") {
t.Errorf("expected 'dnf install -y …' prefix, got: %q", got)
}
for _, pkg := range []string{"git", "curl", "vim"} {
if !strings.Contains(got, pkg) {
t.Errorf("expected %s in batched call, got: %q", pkg, got)
}
}
}
// TestPkgInstallManyBatchesApt verifies the same for apt-get.
func TestPkgInstallManyBatchesApt(t *testing.T) {
defer resetMocks()
pkgMgr = "apt-get"
var calls [][]string
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls = append(calls, append([]string(nil), argv...))
return CmdResult{ExitCode: 0}
}
pkgInstallMany([]string{"a", "b", "c"})
if len(calls) != 1 {
t.Fatalf("expected 1 batched call, got %d", len(calls))
}
if calls[0][0] != "apt-get" || calls[0][1] != "install" || calls[0][2] != "-y" {
t.Errorf("expected 'apt-get install -y' prefix, got: %v", calls[0])
}
}
// TestPkgInstallManyBatchesPacman verifies pacman flags.
func TestPkgInstallManyBatchesPacman(t *testing.T) {
defer resetMocks()
pkgMgr = "pacman"
var calls [][]string
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls = append(calls, append([]string(nil), argv...))
return CmdResult{ExitCode: 0}
}
pkgInstallMany([]string{"a", "b"})
if len(calls) != 1 || calls[0][0] != "pacman" {
t.Fatalf("expected single pacman call, got %v", calls)
}
joined := strings.Join(calls[0], " ")
if !strings.Contains(joined, "--noconfirm") || !strings.Contains(joined, "--needed") {
t.Errorf("expected --noconfirm --needed in pacman call, got: %q", joined)
}
}
// TestPkgInstallManyFallback verifies that a failed batch retries per-package
// and returns the failures it identifies on the per-package retry.
func TestPkgInstallManyFallback(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
calls := 0
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls++
// Fail the first (batched) call, succeed individual retries except for "bad".
if calls == 1 {
return CmdResult{ExitCode: 1}
}
for _, a := range argv {
if a == "bad" {
return CmdResult{ExitCode: 1}
}
}
return CmdResult{ExitCode: 0}
}
failed := pkgInstallMany([]string{"good1", "good2", "bad"})
if len(failed) != 1 || failed[0] != "bad" {
t.Errorf("expected only 'bad' to fail, got %v", failed)
}
// 1 batch + 3 per-package retries = 4 calls.
if calls != 4 {
t.Errorf("expected 4 total calls (1 batch + 3 retries), got %d", calls)
}
}
// TestPkgInstallManyEmpty: no-op on empty input, no calls.
func TestPkgInstallManyEmpty(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
called := false
runCmd = func(argv []string, _ CmdOpts) CmdResult {
called = true
return CmdResult{ExitCode: 0}
}
failed := pkgInstallMany(nil)
if len(failed) != 0 {
t.Errorf("expected no failures, got %v", failed)
}
if called {
t.Error("expected no runCmd call for empty input")
}
}
// TestPkgInstallManyBrew verifies that brew is batched into a single
// `brew install f1 f2 …` call (formulas only — no casks in this test).
// Parallel brew calls would deadlock on shared transitive-dep locks
// (cmake, ninja, libsodium, …), so we deliberately batch and serialize.
func TestPkgInstallManyBrew(t *testing.T) {
defer resetMocks()
pkgMgr = "brew"
var mu sync.Mutex
var calls [][]string
runCmd = func(argv []string, _ CmdOpts) CmdResult {
mu.Lock()
calls = append(calls, append([]string(nil), argv...))
mu.Unlock()
return CmdResult{ExitCode: 0}
}
if failed := pkgInstallMany([]string{"git", "vim", "curl"}); len(failed) != 0 {
t.Errorf("expected no failures, got %v", failed)
}
if len(calls) != 1 {
t.Fatalf("expected exactly 1 batched brew call, got %d: %v", len(calls), calls)
}
got := strings.Join(calls[0], " ")
if got != "brew install git vim curl" {
t.Errorf("expected 'brew install git vim curl', got %q", got)
}
}
// TestPkgInstallManyBrewSplitCasks verifies that casks and formulas are
// emitted in separate calls (because --cask is mutually exclusive with
// formula installs in one invocation).
func TestPkgInstallManyBrewSplitCasks(t *testing.T) {
defer resetMocks()
pkgMgr = "brew"
var calls [][]string
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls = append(calls, append([]string(nil), argv...))
return CmdResult{ExitCode: 0}
}
// "docker" is in brewCasks; the rest are formulas.
pkgInstallMany([]string{"git", "docker", "vim"})
if len(calls) != 2 {
t.Fatalf("expected 2 calls (1 formula batch + 1 cask batch), got %d: %v", len(calls), calls)
}
formula := strings.Join(calls[0], " ")
cask := strings.Join(calls[1], " ")
if formula != "brew install git vim" {
t.Errorf("expected 'brew install git vim', got %q", formula)
}
if cask != "brew install --cask docker" {
t.Errorf("expected 'brew install --cask docker', got %q", cask)
}
}
// TestPkgInstallManyBrewFallback: batched formula install fails; we retry
// per-package and identify the broken one.
func TestPkgInstallManyBrewFallback(t *testing.T) {
defer resetMocks()
pkgMgr = "brew"
calls := 0
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls++
// First call is the batch — fail it.
if calls == 1 {
return CmdResult{ExitCode: 1}
}
// Per-package retries: only "broken" fails.
if argv[len(argv)-1] == "broken" {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
failed := pkgInstallMany([]string{"git", "broken", "curl"})
if len(failed) != 1 || failed[0] != "broken" {
t.Errorf("expected only 'broken' to fail, got %v", failed)
}
// 1 batch + 3 per-package retries = 4 calls.
if calls != 4 {
t.Errorf("expected 4 total calls, got %d", calls)
}
}
// TestInstallFlatpakBatched: a single batched flatpak install for the
// happy path.
func TestInstallFlatpakBatched(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return name == "flatpak" }
var calls [][]string
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls = append(calls, append([]string(nil), argv...))
return CmdResult{ExitCode: 0}
}
installFlatpakPackages([]string{"a.app", "b.app", "c.app"})
// Expect: remote-add (1) + single batched install (1) = 2 calls.
if len(calls) != 2 {
t.Fatalf("expected 2 calls (remote-add + batched install), got %d: %v", len(calls), calls)
}
if calls[1][1] != "install" {
t.Errorf("expected install as second call, got %v", calls[1])
}
for _, app := range []string{"a.app", "b.app", "c.app"} {
found := false
for _, a := range calls[1] {
if a == app {
found = true
break
}
}
if !found {
t.Errorf("expected %s in batched call, got %v", app, calls[1])
}
}
}
// TestInstallFlatpakBatchFallback: failed batch retries per-package.
func TestInstallFlatpakBatchFallback(t *testing.T) {
defer resetMocks()
hasCmd = func(name string) bool { return name == "flatpak" }
calls := 0
runCmd = func(argv []string, _ CmdOpts) CmdResult {
calls++
// First call: remote-add (always OK)
// Second call: batched install (fail)
// Following calls: per-package retries (OK)
if calls == 2 {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
installFlatpakPackages([]string{"a.app", "b.app"})
// 1 (remote-add) + 1 (failed batch) + 2 (per-package retries) = 4 calls.
if calls != 4 {
t.Errorf("expected 4 calls (remote-add + batch + 2 retries), got %d", calls)
}
}
// TestCheckSystemPackagesParallelOrdering: ordering preserved despite
// concurrent probes.
func TestCheckSystemPackagesParallelOrdering(t *testing.T) {
defer resetMocks()
pkgMgr = "dnf"
// odd-indexed packages "installed", even-indexed "not installed"
probe = func(argv []string, _ time.Duration) (CmdResult, bool) {
pkg := argv[len(argv)-1]
// pkg-0..pkg-7
idx := pkg[len(pkg)-1] - '0'
if idx%2 == 1 {
return CmdResult{ExitCode: 0}, true // installed
}
return CmdResult{ExitCode: 1}, true // not installed
}
names := []string{"pkg-0", "pkg-1", "pkg-2", "pkg-3", "pkg-4", "pkg-5", "pkg-6", "pkg-7"}
res := checkSystemPackages(names)
wantToInstall := []string{"pkg-0", "pkg-2", "pkg-4", "pkg-6"}
wantAlready := []string{"pkg-1", "pkg-3", "pkg-5", "pkg-7"}
if !equalStringSlices(res.toInstallRegular, wantToInstall) {
t.Errorf("toInstall: want %v, got %v", wantToInstall, res.toInstallRegular)
}
if !equalStringSlices(res.alreadyInstalled, wantAlready) {
t.Errorf("alreadyInstalled: want %v, got %v", wantAlready, res.alreadyInstalled)
}
}
func TestCheckCustomPackagesParallel(t *testing.T) {
defer resetMocks()
// pkg with InstallPath /tmp/foo-N; "installed" iff N is odd.
osStat = func(name string) (os.FileInfo, error) {
// Map: name like /tmp/foo-1 → installed; /tmp/foo-0 → not.
idx := name[len(name)-1] - '0'
if idx%2 == 1 {
return nil, nil
}
return nil, os.ErrNotExist
}
pkgs := []*CustomPackage{
{Name: "p0", InstallPath: "/tmp/foo-0"},
{Name: "p1", InstallPath: "/tmp/foo-1"},
{Name: "p2", InstallPath: "/tmp/foo-2"},
{Name: "p3", InstallPath: "/tmp/foo-3"},
}
res := checkCustomPackages(pkgs)
if len(res.toInstall) != 2 || res.toInstall[0].Name != "p0" || res.toInstall[1].Name != "p2" {
t.Errorf("toInstall: want p0,p2 in order, got %v", names(res.toInstall))
}
if len(res.alreadyInstalled) != 2 || res.alreadyInstalled[0].pkg.Name != "p1" || res.alreadyInstalled[1].pkg.Name != "p3" {
t.Errorf("already: want p1,p3 in order, got %v", customNames(res.alreadyInstalled))
}
}
func TestInstallNpmToolsBatchSinglePnpmCall(t *testing.T) {
defer resetMocks()
// Pretend ~/.nvm exists so NVM check passes.
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, ".nvm") {
return nil, nil
}
return nil, os.ErrNotExist
}
var shellCalls []string
runShell = func(cmd string, _ CmdOpts) CmdResult {
shellCalls = append(shellCalls, cmd)
return CmdResult{ExitCode: 0}
}
pkgs := []*CustomPackage{
{Name: "claude"},
{Name: "codex"},
{Name: "copilot"},
}
installNpmToolsBatch(pkgs)
// Expect exactly one pnpm add -g call containing all three packages.
addCalls := 0
for _, c := range shellCalls {
if strings.Contains(c, "pnpm add -g") {
addCalls++
if !strings.Contains(c, "@anthropic-ai/claude-code") ||
!strings.Contains(c, "@openai/codex") ||
!strings.Contains(c, "@github/copilot") {
t.Errorf("expected all three npm names in batched call, got: %q", c)
}
}
}
if addCalls != 1 {
t.Errorf("expected exactly 1 batched pnpm add call, got %d (all calls: %v)", addCalls, shellCalls)
}
}
func TestInstallNpmToolsBatchFallback(t *testing.T) {
defer resetMocks()
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, ".nvm") {
return nil, nil
}
return nil, os.ErrNotExist
}
calls := 0
runShell = func(cmd string, _ CmdOpts) CmdResult {
calls++
// Fail the first (batched) pnpm add call; succeed thereafter.
if calls == 1 && strings.Contains(cmd, "pnpm add -g") {
return CmdResult{ExitCode: 1}
}
return CmdResult{ExitCode: 0}
}
pkgs := []*CustomPackage{
{Name: "claude"},
{Name: "codex"},
}
installNpmToolsBatch(pkgs)
// 1 ensureNodeLTS + 1 batch + 2 per-package retries (each may emit
// 2 shell calls: ensureNodeLTS again + add). Just sanity-check that
// retries happened.
if calls < 3 {
t.Errorf("expected at least 3 shell calls after batch failure, got %d", calls)
}
}
func TestInstallCustomPackagesWavesIndependentFirst(t *testing.T) {
defer resetMocks()
// Make hasCmd / osStat permissive. ~/.nvm must "exist" so the
// npm-batch path doesn't bail out at its precondition check.
hasCmd = func(name string) bool { return true }
osStat = func(name string) (os.FileInfo, error) {
if strings.HasSuffix(name, ".nvm") {
return nil, nil
}
return nil, os.ErrNotExist
}
osReadFile = func(name string) ([]byte, error) {
return []byte{}, os.ErrNotExist
}
var orderMu sync.Mutex
var order []string
runCmd = func(argv []string, _ CmdOpts) CmdResult {
orderMu.Lock()
order = append(order, strings.Join(argv, " "))
orderMu.Unlock()
return CmdResult{ExitCode: 0}
}
runShell = func(cmd string, _ CmdOpts) CmdResult {
orderMu.Lock()
order = append(order, cmd)
orderMu.Unlock()
return CmdResult{ExitCode: 0}
}
download = func(_, _ string) bool { return true }
fetchJSON = func(_ string, _ any) bool { return false }
fetchText = func(_ string) string { return "" }
// Mix of independent + node-dependent. We just verify dispatch order:
// the npm-batched call must appear after some Wave A activity.
pkgs := []*CustomPackage{
{Name: "agy"},
{Name: "oh-my-zsh"},
{Name: "claude"},
{Name: "codex"},
}
installCustomPackages(pkgs)
// The pnpm add -g call must exist and appear after agy/oh-my-zsh
// install attempts.
var firstBatchIdx, firstWaveAIdx int = -1, -1
for i, c := range order {
if strings.Contains(c, "pnpm add -g @anthropic-ai/claude-code") {
firstBatchIdx = i
}
if (strings.Contains(c, "antigravity.google") || strings.Contains(c, "ohmyzsh")) && firstWaveAIdx == -1 {
firstWaveAIdx = i
}
}
if firstWaveAIdx == -1 {
t.Errorf("expected to see Wave A activity (agy/oh-my-zsh), got order: %v", order)
}
if firstBatchIdx == -1 {
t.Errorf("expected to see batched pnpm add call, got order: %v", order)
}
if firstWaveAIdx > firstBatchIdx {
t.Errorf("expected Wave A activity to begin before Wave B batch, got waveA@%d batch@%d", firstWaveAIdx, firstBatchIdx)
}
}
func TestResolveLatestAllRunsInParallel(t *testing.T) {
defer resetMocks()
// Register a custom resolver that records start order.
var mu sync.Mutex
var starts []string
latestResolvers["test-fast"] = func(p *CustomPackage) (string, string, bool) {
mu.Lock()
starts = append(starts, p.Name)
mu.Unlock()
return p.Version, p.SHA256, true
}
defer delete(latestResolvers, "test-fast")
pkgs := []*CustomPackage{
{Name: "x", Version: "1", SHA256: "a", FetchLatest: "test-fast"},
{Name: "y", Version: "2", SHA256: "b", FetchLatest: "test-fast"},
{Name: "z", Version: "3", SHA256: "c", FetchLatest: "test-fast"},
}
resolveLatestAll(pkgs)
if len(starts) != 3 {
t.Errorf("expected all 3 resolvers invoked, got %d: %v", len(starts), starts)
}
}
// ── small helpers/fakes ────────────────────────────────────────────────
func equalStringSlices(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func names(pkgs []*CustomPackage) []string {
out := make([]string, len(pkgs))
for i, p := range pkgs {
out[i] = p.Name
}
return out
}
func customNames(s []customStatus) []string {
out := make([]string, len(s))
for i, st := range s {
out[i] = st.pkg.Name
}
return out
}