Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

458 lines
13 KiB
Go

package runtimecmd
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func TestResolverFindsKnownNodeGlobalBin(t *testing.T) {
home := t.TempDir()
binDir := filepath.Join(home, ".nvm", "versions", "node", "v24.12.0", "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
binaryPath := filepath.Join(binDir, "codex-acp")
writeExecutable(t, binaryPath)
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=" + strings.Join([]string{filepath.FromSlash("/usr/bin"), filepath.FromSlash("/bin")}, string(os.PathListSeparator))}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("codex-acp", env); got != binaryPath {
t.Fatalf("Resolve() = %q, want %q", got, binaryPath)
}
if got := resolver.ResolveBinary([]string{"codex-acp"}, nil); got != binaryPath {
t.Fatalf("ResolveBinary() = %q, want %q", got, binaryPath)
}
}
func TestResolverPrefersNewestNVMNodeBin(t *testing.T) {
home := t.TempDir()
oldBinDir := filepath.Join(home, ".nvm", "versions", "node", "v20.19.4", "bin")
newBinDir := filepath.Join(home, ".nvm", "versions", "node", "v24.16.0", "bin")
for _, dir := range []string{oldBinDir, newBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
writeExecutable(t, filepath.Join(dir, "codex"))
}
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
want := filepath.Join(newBinDir, "codex")
if got := resolver.Resolve("codex", env); got != want {
t.Fatalf("Resolve() = %q, want newest NVM codex %q", got, want)
}
}
func TestResolverPrefersExistingPathOverScannedNVMFallback(t *testing.T) {
home := t.TempDir()
activeBinDir := filepath.Join(home, ".nvm", "versions", "node", "v20.19.4", "bin")
newerBinDir := filepath.Join(home, ".nvm", "versions", "node", "v24.16.0", "bin")
for _, dir := range []string{activeBinDir, newerBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
writeExecutable(t, filepath.Join(dir, "codex"))
}
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=" + activeBinDir + string(os.PathListSeparator) + "/usr/bin:/bin",
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
want := filepath.Join(activeBinDir, "codex")
if got := resolver.Resolve("codex", env); got != want {
t.Fatalf("Resolve() = %q, want active PATH codex %q", got, want)
}
}
func TestResolverResolveAllReturnsEveryExecutableMatchInPathOrder(t *testing.T) {
home := t.TempDir()
firstDir := filepath.Join(home, "first")
secondDir := filepath.Join(home, "second")
for _, dir := range []string{firstDir, secondDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
writeExecutable(t, filepath.Join(dir, "codex"))
}
resolver := Resolver{IsExecutableFile: func(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}}
env := []string{"PATH=" + strings.Join([]string{firstDir, secondDir, firstDir}, string(os.PathListSeparator))}
got := resolver.ResolveAll("codex", env)
want := []string{filepath.Join(firstDir, "codex"), filepath.Join(secondDir, "codex")}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ResolveAll() = %#v, want %#v", got, want)
}
if selected := resolver.Resolve("codex", env); selected != want[0] {
t.Fatalf("Resolve() = %q, want first candidate %q", selected, want[0])
}
}
func TestResolverResolveAllNamesKeepsDirectoryOrderBeforeLauncherVariant(t *testing.T) {
home := t.TempDir()
firstDir := filepath.Join(home, "first")
secondDir := filepath.Join(home, "second")
for _, path := range []string{
filepath.Join(firstDir, "codex.cmd"),
filepath.Join(secondDir, "codex.exe"),
} {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
writeExecutable(t, path)
}
resolver := Resolver{IsExecutableFile: func(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}}
env := []string{"PATH=" + strings.Join([]string{firstDir, secondDir}, string(os.PathListSeparator))}
got := resolver.ResolveAllNames([]string{"codex.exe", "codex.cmd"}, env)
want := []string{filepath.Join(firstDir, "codex.cmd"), filepath.Join(secondDir, "codex.exe")}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ResolveAllNames() = %#v, want %#v", got, want)
}
}
func TestResolverFindsTuttiBinFallback(t *testing.T) {
home := t.TempDir()
tuttiBinDir := filepath.Join(home, ".tutti", "bin")
if err := os.MkdirAll(tuttiBinDir, 0o755); err != nil {
t.Fatalf("mkdir tutti bin dir: %v", err)
}
tuttiPath := filepath.Join(tuttiBinDir, "tutti")
writeExecutable(t, tuttiPath)
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("tutti", env); got != tuttiPath {
t.Fatalf("Resolve() = %q, want %q", got, tuttiPath)
}
if got := resolver.ResolveBinary([]string{"tutti"}, nil); got != tuttiPath {
t.Fatalf("ResolveBinary() = %q, want %q", got, tuttiPath)
}
}
func TestResolverFindsOpenCodeBinFallback(t *testing.T) {
home := t.TempDir()
opencodeBinDir := filepath.Join(home, ".opencode", "bin")
if err := os.MkdirAll(opencodeBinDir, 0o755); err != nil {
t.Fatalf("mkdir opencode bin dir: %v", err)
}
opencodePath := filepath.Join(opencodeBinDir, "opencode")
writeExecutable(t, opencodePath)
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("opencode", env); got != opencodePath {
t.Fatalf("Resolve() = %q, want %q", got, opencodePath)
}
if got := resolver.ResolveBinary([]string{"opencode"}, nil); got != opencodePath {
t.Fatalf("ResolveBinary() = %q, want %q", got, opencodePath)
}
}
func TestResolverFindsFnmNodeBin(t *testing.T) {
home := t.TempDir()
fnmDir := filepath.Join(home, "custom-fnm")
binDir := filepath.Join(fnmDir, "node-versions", "v24.12.0", "installation", "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
nodePath := filepath.Join(binDir, "node")
writeExecutable(t, nodePath)
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=/usr/bin:/bin",
"FNM_DIR=" + fnmDir,
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("node", env); got != nodePath {
t.Fatalf("Resolve() = %q, want %q", got, nodePath)
}
}
func TestResolverPrefersFnmNodeBinOverExistingPathNode(t *testing.T) {
home := t.TempDir()
fnmDir := filepath.Join(home, "custom-fnm")
fnmBinDir := filepath.Join(fnmDir, "node-versions", "v24.12.0", "installation", "bin")
existingBinDir := filepath.Join(home, "existing", "bin")
for _, dir := range []string{fnmBinDir, existingBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
}
fnmNodePath := filepath.Join(fnmBinDir, "node")
writeExecutable(t, fnmNodePath)
writeExecutable(t, filepath.Join(existingBinDir, "node"))
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=" + existingBinDir + string(os.PathListSeparator) + "/usr/bin:/bin",
"FNM_DIR=" + fnmDir,
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("node", env); got != fnmNodePath {
t.Fatalf("Resolve() = %q, want %q", got, fnmNodePath)
}
}
func TestResolverPrefersInheritedPathOverBunInstallBin(t *testing.T) {
home := t.TempDir()
bunInstall := filepath.Join(home, "custom-bun")
bunBinDir := filepath.Join(bunInstall, "bin")
pathBinDir := filepath.Join(home, "path-bin")
for _, dir := range []string{bunBinDir, pathBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
}
writeExecutable(t, filepath.Join(bunBinDir, "claude"))
pathClaude := filepath.Join(pathBinDir, "claude")
writeExecutable(t, pathClaude)
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=" + pathBinDir + string(os.PathListSeparator) + "/usr/bin:/bin",
"BUN_INSTALL=" + bunInstall,
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("claude", env); got != pathClaude {
t.Fatalf("Resolve() = %q, want inherited PATH claude %q", got, pathClaude)
}
}
func TestResolverReplacesPathEnv(t *testing.T) {
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/first", "OTHER=value"}
},
HomeDir: func() (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env([]string{"PATH=/override:/first"})
pathCount := 0
pathValue := ""
for _, item := range env {
key, value, ok := strings.Cut(item, "=")
if ok && key == "PATH" {
pathCount++
pathValue = value
}
}
if pathCount != 1 {
t.Fatalf("PATH entry count = %d, want 1 in %#v", pathCount, env)
}
if !strings.HasPrefix(pathValue, "/override") {
t.Fatalf("PATH = %q, want override prefix", pathValue)
}
}
func TestResolverMergesMultiplePathOverrides(t *testing.T) {
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return "", os.ErrNotExist
},
}
pathValueForTest := func(paths ...string) string {
converted := make([]string, 0, len(paths))
for _, path := range paths {
converted = append(converted, filepath.FromSlash(path))
}
return strings.Join(converted, string(os.PathListSeparator))
}
env := resolver.Env([]string{
"PATH=" + pathValueForTest("/state/bin", "/usr/bin", "/bin"),
"PATH=" + pathValueForTest("/managed/node/bin", "/usr/bin", "/bin"),
})
pathCount := 0
pathValue := ""
for _, item := range env {
key, value, ok := strings.Cut(item, "=")
if ok && key == "PATH" {
pathCount++
pathValue = value
}
}
if pathCount != 1 {
t.Fatalf("PATH entry count = %d, want 1 in %#v", pathCount, env)
}
pathDirs := filepath.SplitList(pathValue)
if len(pathDirs) < 4 ||
pathDirs[0] != filepath.FromSlash("/managed/node/bin") ||
pathDirs[1] != filepath.FromSlash("/state/bin") ||
pathDirs[2] != filepath.FromSlash("/usr/bin") ||
pathDirs[3] != filepath.FromSlash("/bin") {
t.Fatalf("PATH = %q, want override prefixes before inherited base path", pathValue)
}
}
func TestResolverEnvStripsClaudeCodeNestingGuards(t *testing.T) {
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=/usr/bin",
"CLAUDECODE=1",
"CLAUDE_CODE_ENTRYPOINT=claude-desktop",
"CLAUDE_CODE_SESSION_ID=abc",
"CLAUDE_CODE_CHILD_SESSION=1",
"CLAUDE_CODE_OAUTH_SCOPES=keep-me",
"OTHER=value",
}
},
HomeDir: func() (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
for _, item := range env {
key, _, _ := strings.Cut(item, "=")
switch key {
case "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION":
t.Fatalf("nesting guard %q should be stripped, got %#v", key, env)
}
}
if value, ok := envValueFrom(env, "CLAUDE_CODE_OAUTH_SCOPES"); !ok || value != "keep-me" {
t.Fatalf("unrelated CLAUDE_CODE_* var was dropped: %#v", env)
}
if value, ok := envValueFrom(env, "OTHER"); !ok || value != "value" {
t.Fatalf("unrelated var was dropped: %#v", env)
}
}
func TestResolverUserBinInstallDirsPrefersPathEntriesThenFallbacks(t *testing.T) {
home := t.TempDir()
pathDir := filepath.Join(home, "custom-bin")
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=" + pathDir + string(os.PathListSeparator) + "/usr/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
}
dirs := resolver.UserBinInstallDirs(nil)
if len(dirs) < 4 {
t.Fatalf("len(dirs) = %d, want at least 4; dirs=%#v", len(dirs), dirs)
}
if dirs[0] != pathDir {
t.Fatalf("dirs[0] = %q, want %q", dirs[0], pathDir)
}
if dirs[1] != "/usr/bin" {
t.Fatalf("dirs[1] = %q, want /usr/bin", dirs[1])
}
if dirs[2] != filepath.Join(home, ".local", "bin") {
t.Fatalf("dirs[2] = %q, want fallback ~/.local/bin", dirs[2])
}
if dirs[3] != filepath.Join(home, "bin") {
t.Fatalf("dirs[3] = %q, want fallback ~/bin", dirs[3])
}
}
func writeExecutable(t *testing.T, path string) {
t.Helper()
if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write executable %s: %v", path, err)
}
}