package toolingplan import ( "context" "os" "path/filepath" "strings" "testing" ) func TestBuildDetectsGoVersionFromGoMod(t *testing.T) { repoRoot := t.TempDir() writePlanFile(t, repoRoot, "go.mod", "module example.com/demo\n\ngo 1.25.0\n") plan := Build(context.Background(), repoRoot) if len(plan.Steps) != 1 { t.Fatalf("steps = %#v, want one step", plan.Steps) } step := plan.Steps[0] if step.Tool != "go" || step.Version != "1.25.0" || step.Source != "go.mod" { t.Fatalf("step = %#v, want go@1.25.0 from go.mod", step) } } func TestBuildSkipsGoWhenRepoMiseAlreadyDeclaresIt(t *testing.T) { repoRoot := t.TempDir() writePlanFile(t, repoRoot, ".mise.toml", "[tools]\ngo = '1.25.0'\n") writePlanFile(t, repoRoot, "go.mod", "module example.com/demo\n\ngo 1.25.0\n") plan := Build(context.Background(), repoRoot) if len(plan.Steps) != 0 { t.Fatalf("steps = %#v, want no deterministic go install", plan.Steps) } if !containsSkip(plan.Skips, "go", "already managed by repo mise declarations") { t.Fatalf("skips = %#v, want managed go skip", plan.Skips) } if len(plan.RepoManagedTools) != 1 || plan.RepoManagedTools[0] != "go" { t.Fatalf("repo managed tools = %#v, want [go]", plan.RepoManagedTools) } } func TestBuildDetectsNodeAndPackageManager(t *testing.T) { repoRoot := t.TempDir() writePlanFile(t, repoRoot, ".node-version", "v22.14.0\n") writePlanFile(t, repoRoot, "package.json", `{"packageManager":"pnpm@9.15.2"}`) plan := Build(context.Background(), repoRoot) if !containsStep(plan.Steps, "node", "22.14.0", ".node-version") { t.Fatalf("steps = %#v, want node step", plan.Steps) } if !containsStep(plan.Steps, "pnpm", "9.15.2", "package.json#packageManager") { t.Fatalf("steps = %#v, want pnpm step", plan.Steps) } } func TestBuildSkipsPackageManagerWhenNodeIsNotPinned(t *testing.T) { repoRoot := t.TempDir() writePlanFile(t, repoRoot, "package.json", `{"packageManager":"pnpm@9.15.2"}`) plan := Build(context.Background(), repoRoot) if containsStep(plan.Steps, "pnpm", "9.15.2", "package.json#packageManager") { t.Fatalf("steps = %#v, want no package manager install", plan.Steps) } if !containsSkip(plan.Skips, "node package manager", "packageManager is pinned but node is not pinned") { t.Fatalf("skips = %#v, want node package manager skip", plan.Skips) } } func TestBuildDetectsPythonAndRust(t *testing.T) { repoRoot := t.TempDir() writePlanFile(t, repoRoot, ".python-version", "3.12.9\n") writePlanFile(t, repoRoot, "rust-toolchain.toml", "[toolchain]\nchannel = '1.86.0'\n") plan := Build(context.Background(), repoRoot) if !containsStep(plan.Steps, "python", "3.12.9", ".python-version") { t.Fatalf("steps = %#v, want python step", plan.Steps) } if !containsStep(plan.Steps, "rust", "1.86.0", "rust-toolchain.toml") { t.Fatalf("steps = %#v, want rust step", plan.Steps) } } func TestBuildSkipsRustChannelNames(t *testing.T) { repoRoot := t.TempDir() writePlanFile(t, repoRoot, "rust-toolchain.toml", "[toolchain]\nchannel = 'stable'\n") plan := Build(context.Background(), repoRoot) if !containsSkip(plan.Skips, "rust", "rust-toolchain.toml channel is not an exact version") { t.Fatalf("skips = %#v, want rust exact-version skip", plan.Skips) } } func TestBuildReportsMalformedMiseTomlAsSkip(t *testing.T) { repoRoot := t.TempDir() writePlanFile(t, repoRoot, ".mise.toml", "[tools\nbroken") plan := Build(context.Background(), repoRoot) if !containsSkip(plan.Skips, "repo mise declarations", "could not parse .mise.toml") { t.Fatalf("skips = %#v, want malformed .mise.toml skip", plan.Skips) } } func writePlanFile(t *testing.T, repoRoot, relativePath, contents string) { t.Helper() path := filepath.Join(repoRoot, relativePath) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll(%s): %v", filepath.Dir(path), err) } if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { t.Fatalf("WriteFile(%s): %v", relativePath, err) } } func containsStep(steps []InstallStep, tool, version, source string) bool { for _, step := range steps { if step.Tool == tool && step.Version == version && step.Source == source { return true } } return false } func containsSkip(skips []SkipNote, target, reasonContains string) bool { for _, skip := range skips { if skip.Target == target && strings.Contains(skip.Reason, reasonContains) { return true } } return false }