mirror of
https://github.com/ollama/ollama.git
synced 2026-07-10 09:44:18 +00:00
462 lines
14 KiB
Go
462 lines
14 KiB
Go
package launch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestClineIntegration(t *testing.T) {
|
|
c := &Cline{}
|
|
|
|
t.Run("String", func(t *testing.T) {
|
|
if got := c.String(); got != "Cline" {
|
|
t.Errorf("String() = %q, want %q", got, "Cline")
|
|
}
|
|
})
|
|
|
|
t.Run("implements Runner", func(t *testing.T) {
|
|
var _ Runner = c
|
|
})
|
|
|
|
t.Run("implements Editor", func(t *testing.T) {
|
|
var _ Editor = c
|
|
})
|
|
}
|
|
|
|
func TestEnsureClineInstalled(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("uses a POSIX shell test binary")
|
|
}
|
|
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
t.Setenv("PATH", tmpDir)
|
|
|
|
clinePath := filepath.Join(tmpDir, "cline")
|
|
npmScript := fmt.Sprintf(`#!/bin/sh
|
|
printf '%%s\n' "$*" > "$HOME/npm-calls.log"
|
|
/bin/cat > %q <<'EOF'
|
|
#!/bin/sh
|
|
exit 0
|
|
EOF
|
|
/bin/chmod +x %q
|
|
exit 0
|
|
`, clinePath, clinePath)
|
|
if err := os.WriteFile(filepath.Join(tmpDir, "npm"), []byte(npmScript), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldConfirmPrompt := DefaultConfirmPrompt
|
|
DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) {
|
|
if prompt != "Cline is not installed. Install with npm?" {
|
|
t.Fatalf("unexpected prompt: %q", prompt)
|
|
}
|
|
return true, nil
|
|
}
|
|
defer func() { DefaultConfirmPrompt = oldConfirmPrompt }()
|
|
|
|
bin, err := ensureClineInstalled()
|
|
if err != nil {
|
|
t.Fatalf("ensureClineInstalled() error = %v", err)
|
|
}
|
|
if bin != "cline" {
|
|
t.Fatalf("ensureClineInstalled() bin = %q, want %q", bin, "cline")
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(tmpDir, "npm-calls.log"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := strings.TrimSpace(string(data)); got != "install -g cline@latest" {
|
|
t.Fatalf("npm args = %q, want %q", got, "install -g cline@latest")
|
|
}
|
|
}
|
|
|
|
func TestClineEdit(t *testing.T) {
|
|
c := &Cline{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
configDir := filepath.Join(tmpDir, ".cline", "data")
|
|
configPath := filepath.Join(configDir, "globalState.json")
|
|
providersPath := filepath.Join(tmpDir, ".cline", "data", "settings", "providers.json")
|
|
|
|
readConfig := func() map[string]any {
|
|
data, _ := os.ReadFile(configPath)
|
|
var config map[string]any
|
|
json.Unmarshal(data, &config)
|
|
return config
|
|
}
|
|
|
|
readProvidersConfig := func() map[string]any {
|
|
data, _ := os.ReadFile(providersPath)
|
|
var config map[string]any
|
|
json.Unmarshal(data, &config)
|
|
return config
|
|
}
|
|
|
|
t.Run("creates config from scratch", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
|
|
if err := c.Edit(testLaunchModels("kimi-k2.5:cloud")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := readConfig()
|
|
if config["actModeApiProvider"] != clineLaunchProvider {
|
|
t.Errorf("actModeApiProvider = %v, want %s", config["actModeApiProvider"], clineLaunchProvider)
|
|
}
|
|
if config["actModeOllamaModelId"] != "kimi-k2.5:cloud" {
|
|
t.Errorf("actModeOllamaModelId = %v, want kimi-k2.5:cloud", config["actModeOllamaModelId"])
|
|
}
|
|
if config["actModeOllamaBaseUrl"] != "http://127.0.0.1:11434" {
|
|
t.Errorf("actModeOllamaBaseUrl = %v, want http://127.0.0.1:11434", config["actModeOllamaBaseUrl"])
|
|
}
|
|
if config["planModeApiProvider"] != clineLaunchProvider {
|
|
t.Errorf("planModeApiProvider = %v, want %s", config["planModeApiProvider"], clineLaunchProvider)
|
|
}
|
|
if config["planModeOllamaModelId"] != "kimi-k2.5:cloud" {
|
|
t.Errorf("planModeOllamaModelId = %v, want kimi-k2.5:cloud", config["planModeOllamaModelId"])
|
|
}
|
|
if config["planModeOllamaBaseUrl"] != "http://127.0.0.1:11434" {
|
|
t.Errorf("planModeOllamaBaseUrl = %v, want http://127.0.0.1:11434", config["planModeOllamaBaseUrl"])
|
|
}
|
|
if config["ollamaBaseUrl"] != "http://127.0.0.1:11434" {
|
|
t.Errorf("ollamaBaseUrl = %v, want http://127.0.0.1:11434", config["ollamaBaseUrl"])
|
|
}
|
|
if config["welcomeViewCompleted"] != true {
|
|
t.Errorf("welcomeViewCompleted = %v, want true", config["welcomeViewCompleted"])
|
|
}
|
|
|
|
providersConfig := readProvidersConfig()
|
|
if providersConfig["lastUsedProvider"] != clineLaunchProvider {
|
|
t.Errorf("lastUsedProvider = %v, want %s", providersConfig["lastUsedProvider"], clineLaunchProvider)
|
|
}
|
|
providers, _ := providersConfig["providers"].(map[string]any)
|
|
provider, _ := providers[clineLaunchProvider].(map[string]any)
|
|
if provider["updatedAt"] == "" {
|
|
t.Errorf("updatedAt = %v, want timestamp", provider["updatedAt"])
|
|
}
|
|
settings, _ := provider["settings"].(map[string]any)
|
|
if settings["model"] != "kimi-k2.5:cloud" {
|
|
t.Errorf("settings.model = %v, want kimi-k2.5:cloud", settings["model"])
|
|
}
|
|
if _, ok := settings["apiKey"]; ok {
|
|
t.Errorf("settings.apiKey = %v, want omitted for local Ollama", settings["apiKey"])
|
|
}
|
|
if settings["baseUrl"] != "http://127.0.0.1:11434/v1" {
|
|
t.Errorf("settings.baseUrl = %v, want http://127.0.0.1:11434/v1", settings["baseUrl"])
|
|
}
|
|
})
|
|
|
|
t.Run("preserves existing fields", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.MkdirAll(filepath.Dir(providersPath), 0o755)
|
|
|
|
existing := map[string]any{
|
|
"remoteRulesToggles": map[string]any{},
|
|
"remoteWorkflowToggles": map[string]any{},
|
|
"customSetting": "keep-me",
|
|
}
|
|
data, _ := json.Marshal(existing)
|
|
os.WriteFile(configPath, data, 0o644)
|
|
|
|
existingProviders := map[string]any{
|
|
"customRoot": "keep-me-too",
|
|
"providers": map[string]any{
|
|
clineLaunchProvider: map[string]any{
|
|
"updatedAt": "2026-05-29T16:56:46.111Z",
|
|
"settings": map[string]any{
|
|
"apiKey": "bad-migrated-key",
|
|
"timeout": float64(30000),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
data, _ = json.Marshal(existingProviders)
|
|
os.WriteFile(providersPath, data, 0o644)
|
|
|
|
if err := c.Edit(testLaunchModels("glm-5:cloud")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := readConfig()
|
|
if config["customSetting"] != "keep-me" {
|
|
t.Errorf("customSetting was not preserved")
|
|
}
|
|
if config["actModeOllamaModelId"] != "glm-5:cloud" {
|
|
t.Errorf("actModeOllamaModelId = %v, want glm-5:cloud", config["actModeOllamaModelId"])
|
|
}
|
|
|
|
providersConfig := readProvidersConfig()
|
|
if providersConfig["customRoot"] != "keep-me-too" {
|
|
t.Errorf("customRoot was not preserved")
|
|
}
|
|
providers, _ := providersConfig["providers"].(map[string]any)
|
|
provider, _ := providers[clineLaunchProvider].(map[string]any)
|
|
if provider["updatedAt"] == "2026-05-29T16:56:46.111Z" {
|
|
t.Errorf("updatedAt = %v, want refreshed timestamp after provider change", provider["updatedAt"])
|
|
}
|
|
settings, _ := provider["settings"].(map[string]any)
|
|
if settings["timeout"] != float64(30000) {
|
|
t.Errorf("settings.timeout = %v, want 30000", settings["timeout"])
|
|
}
|
|
if _, ok := settings["apiKey"]; ok {
|
|
t.Errorf("settings.apiKey = %v, want omitted for local Ollama", settings["apiKey"])
|
|
}
|
|
if settings["model"] != "glm-5:cloud" {
|
|
t.Errorf("settings.model = %v, want glm-5:cloud", settings["model"])
|
|
}
|
|
if settings["baseUrl"] != "http://127.0.0.1:11434/v1" {
|
|
t.Errorf("settings.baseUrl = %v, want http://127.0.0.1:11434/v1", settings["baseUrl"])
|
|
}
|
|
})
|
|
|
|
t.Run("validates both configs before writing providers config", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte("{not json"), 0o644)
|
|
|
|
err := c.Edit(testLaunchModels("kimi-k2.5:cloud"))
|
|
if err == nil {
|
|
t.Fatal("expected invalid legacy config error")
|
|
}
|
|
if _, statErr := os.Stat(providersPath); !os.IsNotExist(statErr) {
|
|
t.Fatalf("providers config should not be written when legacy config is invalid, stat err = %v", statErr)
|
|
}
|
|
})
|
|
|
|
t.Run("preserves updatedAt when provider settings are unchanged", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
os.MkdirAll(filepath.Dir(providersPath), 0o755)
|
|
|
|
existingProviders := map[string]any{
|
|
"providers": map[string]any{
|
|
clineLaunchProvider: map[string]any{
|
|
"updatedAt": "2026-05-29T16:56:46.111Z",
|
|
"tokenSource": "manual",
|
|
"settings": map[string]any{
|
|
"provider": clineLaunchProvider,
|
|
"model": "kimi-k2.5:cloud",
|
|
"baseUrl": "http://127.0.0.1:11434/v1",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
data, _ := json.Marshal(existingProviders)
|
|
os.WriteFile(providersPath, data, 0o644)
|
|
|
|
if err := c.Edit(testLaunchModels("kimi-k2.5:cloud")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
providersConfig := readProvidersConfig()
|
|
providers, _ := providersConfig["providers"].(map[string]any)
|
|
provider, _ := providers[clineLaunchProvider].(map[string]any)
|
|
if provider["updatedAt"] != "2026-05-29T16:56:46.111Z" {
|
|
t.Errorf("updatedAt = %v, want preserved timestamp", provider["updatedAt"])
|
|
}
|
|
})
|
|
|
|
t.Run("updates model on re-edit", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
|
|
if err := c.Edit(testLaunchModels("kimi-k2.5:cloud")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := c.Edit(testLaunchModels("glm-5:cloud")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := readConfig()
|
|
if config["actModeOllamaModelId"] != "glm-5:cloud" {
|
|
t.Errorf("actModeOllamaModelId = %v, want glm-5:cloud", config["actModeOllamaModelId"])
|
|
}
|
|
if config["planModeOllamaModelId"] != "glm-5:cloud" {
|
|
t.Errorf("planModeOllamaModelId = %v, want glm-5:cloud", config["planModeOllamaModelId"])
|
|
}
|
|
})
|
|
|
|
t.Run("empty models is no-op", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
|
|
if err := c.Edit(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
|
t.Error("expected no config file to be created for empty models")
|
|
}
|
|
})
|
|
|
|
t.Run("uses first model as primary", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
|
|
if err := c.Edit(testLaunchModels("kimi-k2.5:cloud", "glm-5:cloud")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := readConfig()
|
|
if config["actModeOllamaModelId"] != "kimi-k2.5:cloud" {
|
|
t.Errorf("actModeOllamaModelId = %v, want kimi-k2.5:cloud (first model)", config["actModeOllamaModelId"])
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClineModels(t *testing.T) {
|
|
c := &Cline{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
configDir := filepath.Join(tmpDir, ".cline", "data")
|
|
configPath := filepath.Join(configDir, "globalState.json")
|
|
providersPath := filepath.Join(tmpDir, ".cline", "data", "settings", "providers.json")
|
|
|
|
t.Run("returns nil when no config", func(t *testing.T) {
|
|
if models := c.Models(); models != nil {
|
|
t.Errorf("Models() = %v, want nil", models)
|
|
}
|
|
})
|
|
|
|
t.Run("returns nil when provider is not ollama", func(t *testing.T) {
|
|
os.MkdirAll(configDir, 0o755)
|
|
config := map[string]any{
|
|
"actModeApiProvider": "anthropic",
|
|
"actModeOllamaModelId": "some-model",
|
|
}
|
|
data, _ := json.Marshal(config)
|
|
os.WriteFile(configPath, data, 0o644)
|
|
|
|
if models := c.Models(); models != nil {
|
|
t.Errorf("Models() = %v, want nil", models)
|
|
}
|
|
})
|
|
|
|
t.Run("returns model when ollama is configured", func(t *testing.T) {
|
|
os.MkdirAll(configDir, 0o755)
|
|
config := map[string]any{
|
|
"actModeApiProvider": "ollama",
|
|
"actModeOllamaModelId": "kimi-k2.5:cloud",
|
|
}
|
|
data, _ := json.Marshal(config)
|
|
os.WriteFile(configPath, data, 0o644)
|
|
|
|
models := c.Models()
|
|
if len(models) != 1 || models[0] != "kimi-k2.5:cloud" {
|
|
t.Errorf("Models() = %v, want [kimi-k2.5:cloud]", models)
|
|
}
|
|
})
|
|
|
|
t.Run("prefers CLI provider config", func(t *testing.T) {
|
|
os.MkdirAll(filepath.Dir(providersPath), 0o755)
|
|
config := map[string]any{
|
|
"lastUsedProvider": clineLaunchProvider,
|
|
"providers": map[string]any{
|
|
clineLaunchProvider: map[string]any{
|
|
"settings": map[string]any{
|
|
"model": "glm-5:cloud",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
data, _ := json.Marshal(config)
|
|
os.WriteFile(providersPath, data, 0o644)
|
|
|
|
models := c.Models()
|
|
if len(models) != 1 || models[0] != "glm-5:cloud" {
|
|
t.Errorf("Models() = %v, want [glm-5:cloud]", models)
|
|
}
|
|
})
|
|
|
|
t.Run("ignores stale CLI provider config when ollama is not active", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.MkdirAll(filepath.Dir(providersPath), 0o755)
|
|
legacyConfig := map[string]any{
|
|
"actModeApiProvider": "anthropic",
|
|
"actModeOllamaModelId": "legacy-ollama-model",
|
|
}
|
|
data, _ := json.Marshal(legacyConfig)
|
|
os.WriteFile(configPath, data, 0o644)
|
|
providerConfig := map[string]any{
|
|
"lastUsedProvider": "openai",
|
|
"providers": map[string]any{
|
|
clineLaunchProvider: map[string]any{
|
|
"settings": map[string]any{
|
|
"model": "stale-ollama-model",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
data, _ = json.Marshal(providerConfig)
|
|
os.WriteFile(providersPath, data, 0o644)
|
|
|
|
if models := c.Models(); models != nil {
|
|
t.Errorf("Models() = %v, want nil", models)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClinePaths(t *testing.T) {
|
|
c := &Cline{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
t.Run("returns nil when no config exists", func(t *testing.T) {
|
|
if paths := c.Paths(); paths != nil {
|
|
t.Errorf("Paths() = %v, want nil", paths)
|
|
}
|
|
})
|
|
|
|
t.Run("returns path when config exists", func(t *testing.T) {
|
|
configDir := filepath.Join(tmpDir, ".cline", "data")
|
|
os.MkdirAll(configDir, 0o755)
|
|
configPath := filepath.Join(configDir, "globalState.json")
|
|
os.WriteFile(configPath, []byte("{}"), 0o644)
|
|
|
|
paths := c.Paths()
|
|
if len(paths) != 1 || paths[0] != configPath {
|
|
t.Errorf("Paths() = %v, want [%s]", paths, configPath)
|
|
}
|
|
})
|
|
|
|
t.Run("returns both paths when both configs exist", func(t *testing.T) {
|
|
os.RemoveAll(filepath.Join(tmpDir, ".cline"))
|
|
legacyPath := clineLegacyGlobalStatePath(tmpDir)
|
|
providersPath := clineProvidersPath(tmpDir)
|
|
os.MkdirAll(filepath.Dir(legacyPath), 0o755)
|
|
os.MkdirAll(filepath.Dir(providersPath), 0o755)
|
|
os.WriteFile(legacyPath, []byte("{}"), 0o644)
|
|
os.WriteFile(providersPath, []byte("{}"), 0o644)
|
|
|
|
paths := c.Paths()
|
|
want := []string{providersPath, legacyPath}
|
|
if len(paths) != len(want) {
|
|
t.Fatalf("Paths() = %v, want %v", paths, want)
|
|
}
|
|
for i := range want {
|
|
if paths[i] != want[i] {
|
|
t.Fatalf("Paths() = %v, want %v", paths, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClineLaunchArgs(t *testing.T) {
|
|
got := clineLaunchArgs("kimi-k2.5:cloud", []string{"--json", "hello"})
|
|
want := []string{"--json", "hello"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("args length = %d, want %d: %v", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("args[%d] = %q, want %q; got %v", i, got[i], want[i], got)
|
|
}
|
|
}
|
|
}
|