package windsurf import ( "errors" "path/filepath" "strings" "testing" ) // stubStatFn replaces binaryStatFn for the duration of a test, restoring the // original on cleanup. func stubStatFn(t *testing.T, present map[string]bool) { t.Helper() orig := binaryStatFn binaryStatFn = func(path string) bool { return present[path] } t.Cleanup(func() { binaryStatFn = orig }) } func stubUserHome(t *testing.T, home string) { t.Helper() orig := userHomeFn userHomeFn = func() string { return home } t.Cleanup(func() { userHomeFn = orig }) } func TestDiscoverBinary_EnvOverrideWins(t *testing.T) { stubStatFn(t, map[string]bool{ "/tmp/my-ls": true, "/opt/windsurf/language_server_linux_x64": true, // should not be picked }) got, err := discoverBinaryFor(Platform{"linux", "amd64"}, "/tmp/my-ls", "/opt/windsurf/language_server_linux_x64") if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "/tmp/my-ls" { t.Errorf("env override should win, got %q", got) } } func TestDiscoverBinary_EnvMissingReturnsError(t *testing.T) { stubStatFn(t, map[string]bool{}) // nothing exists _, err := discoverBinaryFor(Platform{"linux", "amd64"}, "/does/not/exist", "") if err == nil { t.Fatal("expected error for missing env path") } if !errors.Is(err, ErrBinaryNotFound) { t.Errorf("expected ErrBinaryNotFound, got %v", err) } if !strings.Contains(err.Error(), "LS_BINARY_PATH") { t.Errorf("error should mention LS_BINARY_PATH, got %v", err) } if !strings.Contains(err.Error(), "docker") { t.Errorf("error should point at docker, got %v", err) } } func TestDiscoverBinary_CfgBinaryUsedWhenEnvEmpty(t *testing.T) { stubStatFn(t, map[string]bool{"/custom/ls": true}) got, err := discoverBinaryFor(Platform{"linux", "amd64"}, "", "/custom/ls") if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "/custom/ls" { t.Errorf("cfg path should be used, got %q", got) } } func TestDiscoverBinary_FallsBackToCandidates(t *testing.T) { stubStatFn(t, map[string]bool{ "/opt/windsurf/language_server_linux_x64": true, }) got, err := discoverBinaryFor(Platform{"linux", "amd64"}, "", "") if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "/opt/windsurf/language_server_linux_x64" { t.Errorf("expected /opt/windsurf/language_server_linux_x64, got %q", got) } } func TestDiscoverBinary_DarwinPicksBundleOverLegacy(t *testing.T) { bundle := "/Applications/Windsurf.app/Contents/Resources/app/extensions/windsurf/bin/language_server_macos_arm" legacy := "/opt/windsurf/language_server_macos_arm" stubStatFn(t, map[string]bool{ bundle: true, legacy: true, }) stubUserHome(t, "/Users/test") got, err := discoverBinaryFor(Platform{"darwin", "arm64"}, "", "") if err != nil { t.Fatalf("unexpected error: %v", err) } if got != bundle { t.Errorf("bundle path should win over legacy, got %q", got) } } func TestDiscoverBinary_WindowsPicksLocalAppData(t *testing.T) { t.Setenv("LOCALAPPDATA", `C:\Users\test\AppData\Local`) t.Setenv("PROGRAMFILES", `C:\Program Files`) // Construct the expected path the same way windowsCandidates() does so // the test is portable across builders (macOS/Linux Go use '/', Windows // uses '\\'). The Windows-native separator is verified by the Windows CI // job in backend-ci.yml. localPath := filepath.Join(`C:\Users\test\AppData\Local`, "Programs", "Windsurf", "resources", "app", "extensions", "windsurf", "bin", "language_server_windows_x64.exe") stubStatFn(t, map[string]bool{localPath: true}) got, err := discoverBinaryFor(Platform{"windows", "amd64"}, "", "") if err != nil { t.Fatalf("unexpected error: %v", err) } if got != localPath { t.Errorf("expected LOCALAPPDATA path, got %q", got) } } func TestDiscoverBinary_AllMissReturnsUsefulError(t *testing.T) { stubStatFn(t, map[string]bool{}) stubUserHome(t, "/Users/test") _, err := discoverBinaryFor(Platform{"darwin", "arm64"}, "", "") if err == nil { t.Fatal("expected error") } if !errors.Is(err, ErrBinaryNotFound) { t.Errorf("expected ErrBinaryNotFound, got %v", err) } for _, want := range []string{"darwin/arm64", "LS_BINARY_PATH", "docker", "/Applications/Windsurf.app"} { if !strings.Contains(err.Error(), want) { t.Errorf("error missing %q: %v", want, err) } } } func TestDiscoverBinary_UnsupportedPlatformPropagates(t *testing.T) { _, err := discoverBinaryFor(Platform{"freebsd", "amd64"}, "", "") if err == nil { t.Fatal("expected error") } if !errors.Is(err, ErrUnsupportedPlatform) { t.Errorf("expected ErrUnsupportedPlatform, got %v", err) } } func TestDiscoverBinary_WindowsNonExeRejected(t *testing.T) { // Even if env stat returns true, validateBinaryPath uses the real binaryStatFn, // which for Windows requires .exe suffix. Test via direct stat behavior. stubStatFn(t, map[string]bool{"C:\\custom\\ls": false}) // stat returns false _, err := discoverBinaryFor(Platform{"windows", "amd64"}, "C:\\custom\\ls", "") if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), ".exe") { t.Errorf("windows error should hint at .exe, got %v", err) } } func TestPlatformCandidates_Linux(t *testing.T) { stubUserHome(t, "/home/test") got, err := platformCandidates(Platform{"linux", "amd64"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(got) < 3 { t.Errorf("expected at least 3 linux candidates, got %d: %v", len(got), got) } if got[0] != "/opt/windsurf/language_server_linux_x64" { t.Errorf("legacy path should be first for backward-compat, got %q", got[0]) } } func TestPlatformCandidates_UnsupportedPropagates(t *testing.T) { _, err := platformCandidates(Platform{"plan9", "amd64"}) if err == nil || !errors.Is(err, ErrUnsupportedPlatform) { t.Errorf("expected ErrUnsupportedPlatform, got %v", err) } }