mirror of
https://github.com/ollama/ollama.git
synced 2026-01-29 07:12:03 +03:00
508 lines
14 KiB
Go
508 lines
14 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestOpenCodeIntegration(t *testing.T) {
|
|
o := &OpenCode{}
|
|
|
|
t.Run("String", func(t *testing.T) {
|
|
if got := o.String(); got != "OpenCode" {
|
|
t.Errorf("String() = %q, want %q", got, "OpenCode")
|
|
}
|
|
})
|
|
|
|
t.Run("implements Runner", func(t *testing.T) {
|
|
var _ Runner = o
|
|
})
|
|
|
|
t.Run("implements Editor", func(t *testing.T) {
|
|
var _ Editor = o
|
|
})
|
|
}
|
|
|
|
func TestOpenCodeEdit(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
configDir := filepath.Join(tmpDir, ".config", "opencode")
|
|
configPath := filepath.Join(configDir, "opencode.json")
|
|
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
|
|
statePath := filepath.Join(stateDir, "model.json")
|
|
|
|
cleanup := func() {
|
|
os.RemoveAll(configDir)
|
|
os.RemoveAll(stateDir)
|
|
}
|
|
|
|
t.Run("fresh install", func(t *testing.T) {
|
|
cleanup()
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenCodeModelExists(t, configPath, "llama3.2")
|
|
assertOpenCodeRecentModel(t, statePath, 0, "ollama", "llama3.2")
|
|
})
|
|
|
|
t.Run("preserve other providers", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"provider":{"anthropic":{"apiKey":"xxx"}}}`), 0o644)
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
provider := cfg["provider"].(map[string]any)
|
|
if provider["anthropic"] == nil {
|
|
t.Error("anthropic provider was removed")
|
|
}
|
|
assertOpenCodeModelExists(t, configPath, "llama3.2")
|
|
})
|
|
|
|
t.Run("preserve other models", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"mistral":{"name":"Mistral"}}}}}`), 0o644)
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenCodeModelExists(t, configPath, "mistral")
|
|
assertOpenCodeModelExists(t, configPath, "llama3.2")
|
|
})
|
|
|
|
t.Run("update existing model", func(t *testing.T) {
|
|
cleanup()
|
|
o.Edit([]string{"llama3.2"})
|
|
o.Edit([]string{"llama3.2"})
|
|
assertOpenCodeModelExists(t, configPath, "llama3.2")
|
|
})
|
|
|
|
t.Run("preserve top-level keys", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"theme":"dark","keybindings":{}}`), 0o644)
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
if cfg["theme"] != "dark" {
|
|
t.Error("theme was removed")
|
|
}
|
|
if cfg["keybindings"] == nil {
|
|
t.Error("keybindings was removed")
|
|
}
|
|
})
|
|
|
|
t.Run("model state - insert at index 0", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(stateDir, 0o755)
|
|
os.WriteFile(statePath, []byte(`{"recent":[{"providerID":"anthropic","modelID":"claude"}],"favorite":[],"variant":{}}`), 0o644)
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertOpenCodeRecentModel(t, statePath, 0, "ollama", "llama3.2")
|
|
assertOpenCodeRecentModel(t, statePath, 1, "anthropic", "claude")
|
|
})
|
|
|
|
t.Run("model state - preserve favorites and variants", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(stateDir, 0o755)
|
|
os.WriteFile(statePath, []byte(`{"recent":[],"favorite":[{"providerID":"x","modelID":"y"}],"variant":{"a":"b"}}`), 0o644)
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := os.ReadFile(statePath)
|
|
var state map[string]any
|
|
json.Unmarshal(data, &state)
|
|
if len(state["favorite"].([]any)) != 1 {
|
|
t.Error("favorite was modified")
|
|
}
|
|
if state["variant"].(map[string]any)["a"] != "b" {
|
|
t.Error("variant was modified")
|
|
}
|
|
})
|
|
|
|
t.Run("model state - deduplicate on re-add", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(stateDir, 0o755)
|
|
os.WriteFile(statePath, []byte(`{"recent":[{"providerID":"ollama","modelID":"llama3.2"},{"providerID":"anthropic","modelID":"claude"}],"favorite":[],"variant":{}}`), 0o644)
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := os.ReadFile(statePath)
|
|
var state map[string]any
|
|
json.Unmarshal(data, &state)
|
|
recent := state["recent"].([]any)
|
|
if len(recent) != 2 {
|
|
t.Errorf("expected 2 recent entries, got %d", len(recent))
|
|
}
|
|
assertOpenCodeRecentModel(t, statePath, 0, "ollama", "llama3.2")
|
|
})
|
|
|
|
t.Run("remove model", func(t *testing.T) {
|
|
cleanup()
|
|
// First add two models
|
|
o.Edit([]string{"llama3.2", "mistral"})
|
|
assertOpenCodeModelExists(t, configPath, "llama3.2")
|
|
assertOpenCodeModelExists(t, configPath, "mistral")
|
|
|
|
// Then remove one by only selecting the other
|
|
o.Edit([]string{"llama3.2"})
|
|
assertOpenCodeModelExists(t, configPath, "llama3.2")
|
|
assertOpenCodeModelNotExists(t, configPath, "mistral")
|
|
})
|
|
|
|
t.Run("preserve user customizations on managed models", func(t *testing.T) {
|
|
cleanup()
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add custom fields to the model entry (simulating user edits)
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
provider := cfg["provider"].(map[string]any)
|
|
ollama := provider["ollama"].(map[string]any)
|
|
models := ollama["models"].(map[string]any)
|
|
entry := models["llama3.2"].(map[string]any)
|
|
entry["_myPref"] = "custom-value"
|
|
entry["_myNum"] = 42
|
|
configData, _ := json.MarshalIndent(cfg, "", " ")
|
|
os.WriteFile(configPath, configData, 0o644)
|
|
|
|
// Re-run Edit — should preserve custom fields
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ = os.ReadFile(configPath)
|
|
json.Unmarshal(data, &cfg)
|
|
provider = cfg["provider"].(map[string]any)
|
|
ollama = provider["ollama"].(map[string]any)
|
|
models = ollama["models"].(map[string]any)
|
|
entry = models["llama3.2"].(map[string]any)
|
|
|
|
if entry["_myPref"] != "custom-value" {
|
|
t.Errorf("_myPref was lost: got %v", entry["_myPref"])
|
|
}
|
|
if entry["_myNum"] != float64(42) {
|
|
t.Errorf("_myNum was lost: got %v", entry["_myNum"])
|
|
}
|
|
if v, ok := entry["_launch"].(bool); !ok || !v {
|
|
t.Errorf("_launch marker missing or false: got %v", entry["_launch"])
|
|
}
|
|
})
|
|
|
|
t.Run("migrate legacy [Ollama] suffix entries", func(t *testing.T) {
|
|
cleanup()
|
|
// Write a config with a legacy entry (has [Ollama] suffix but no _launch marker)
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"llama3.2":{"name":"llama3.2 [Ollama]"}}}}}`), 0o644)
|
|
|
|
if err := o.Edit([]string{"llama3.2"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
provider := cfg["provider"].(map[string]any)
|
|
ollama := provider["ollama"].(map[string]any)
|
|
models := ollama["models"].(map[string]any)
|
|
entry := models["llama3.2"].(map[string]any)
|
|
|
|
// _launch marker should be added
|
|
if v, ok := entry["_launch"].(bool); !ok || !v {
|
|
t.Errorf("_launch marker not added during migration: got %v", entry["_launch"])
|
|
}
|
|
// [Ollama] suffix should be stripped
|
|
if name, ok := entry["name"].(string); !ok || name != "llama3.2" {
|
|
t.Errorf("name suffix not stripped: got %q", entry["name"])
|
|
}
|
|
})
|
|
|
|
t.Run("remove model preserves non-ollama models", func(t *testing.T) {
|
|
cleanup()
|
|
os.MkdirAll(configDir, 0o755)
|
|
// Add a non-Ollama model manually
|
|
os.WriteFile(configPath, []byte(`{"provider":{"ollama":{"models":{"external":{"name":"External Model"}}}}}`), 0o644)
|
|
|
|
o.Edit([]string{"llama3.2"})
|
|
assertOpenCodeModelExists(t, configPath, "llama3.2")
|
|
assertOpenCodeModelExists(t, configPath, "external") // Should be preserved
|
|
})
|
|
}
|
|
|
|
func assertOpenCodeModelExists(t *testing.T, path, model string) {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
provider, ok := cfg["provider"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("provider not found")
|
|
}
|
|
ollama, ok := provider["ollama"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("ollama provider not found")
|
|
}
|
|
models, ok := ollama["models"].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("models not found")
|
|
}
|
|
if models[model] == nil {
|
|
t.Errorf("model %s not found", model)
|
|
}
|
|
}
|
|
|
|
func assertOpenCodeModelNotExists(t *testing.T, path, model string) {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
provider, ok := cfg["provider"].(map[string]any)
|
|
if !ok {
|
|
return // No provider means no model
|
|
}
|
|
ollama, ok := provider["ollama"].(map[string]any)
|
|
if !ok {
|
|
return // No ollama means no model
|
|
}
|
|
models, ok := ollama["models"].(map[string]any)
|
|
if !ok {
|
|
return // No models means no model
|
|
}
|
|
if models[model] != nil {
|
|
t.Errorf("model %s should not exist but was found", model)
|
|
}
|
|
}
|
|
|
|
func assertOpenCodeRecentModel(t *testing.T, path string, index int, providerID, modelID string) {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var state map[string]any
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recent, ok := state["recent"].([]any)
|
|
if !ok {
|
|
t.Fatal("recent not found")
|
|
}
|
|
if index >= len(recent) {
|
|
t.Fatalf("index %d out of range (len=%d)", index, len(recent))
|
|
}
|
|
entry, ok := recent[index].(map[string]any)
|
|
if !ok {
|
|
t.Fatal("entry is not a map")
|
|
}
|
|
if entry["providerID"] != providerID {
|
|
t.Errorf("expected providerID %s, got %s", providerID, entry["providerID"])
|
|
}
|
|
if entry["modelID"] != modelID {
|
|
t.Errorf("expected modelID %s, got %s", modelID, entry["modelID"])
|
|
}
|
|
}
|
|
|
|
// Edge case tests for opencode.go
|
|
|
|
func TestOpenCodeEdit_CorruptedConfigJSON(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
configDir := filepath.Join(tmpDir, ".config", "opencode")
|
|
configPath := filepath.Join(configDir, "opencode.json")
|
|
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{corrupted json content`), 0o644)
|
|
|
|
// Should not panic - corrupted JSON should be treated as empty
|
|
err := o.Edit([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatalf("Edit failed with corrupted config: %v", err)
|
|
}
|
|
|
|
// Verify valid JSON was created
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
t.Errorf("resulting config is not valid JSON: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenCodeEdit_CorruptedStateJSON(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
|
|
statePath := filepath.Join(stateDir, "model.json")
|
|
|
|
os.MkdirAll(stateDir, 0o755)
|
|
os.WriteFile(statePath, []byte(`{corrupted state`), 0o644)
|
|
|
|
err := o.Edit([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatalf("Edit failed with corrupted state: %v", err)
|
|
}
|
|
|
|
// Verify valid state was created
|
|
data, _ := os.ReadFile(statePath)
|
|
var state map[string]any
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|
t.Errorf("resulting state is not valid JSON: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenCodeEdit_WrongTypeProvider(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
configDir := filepath.Join(tmpDir, ".config", "opencode")
|
|
configPath := filepath.Join(configDir, "opencode.json")
|
|
|
|
os.MkdirAll(configDir, 0o755)
|
|
os.WriteFile(configPath, []byte(`{"provider": "not a map"}`), 0o644)
|
|
|
|
err := o.Edit([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatalf("Edit with wrong type provider failed: %v", err)
|
|
}
|
|
|
|
// Verify provider is now correct type
|
|
data, _ := os.ReadFile(configPath)
|
|
var cfg map[string]any
|
|
json.Unmarshal(data, &cfg)
|
|
|
|
provider, ok := cfg["provider"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("provider should be map after setup, got %T", cfg["provider"])
|
|
}
|
|
if provider["ollama"] == nil {
|
|
t.Error("ollama provider should be created")
|
|
}
|
|
}
|
|
|
|
func TestOpenCodeEdit_WrongTypeRecent(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
|
|
statePath := filepath.Join(stateDir, "model.json")
|
|
|
|
os.MkdirAll(stateDir, 0o755)
|
|
os.WriteFile(statePath, []byte(`{"recent": "not an array", "favorite": [], "variant": {}}`), 0o644)
|
|
|
|
err := o.Edit([]string{"llama3.2"})
|
|
if err != nil {
|
|
t.Fatalf("Edit with wrong type recent failed: %v", err)
|
|
}
|
|
|
|
// The function should handle this gracefully
|
|
data, _ := os.ReadFile(statePath)
|
|
var state map[string]any
|
|
json.Unmarshal(data, &state)
|
|
|
|
// recent should be properly set after setup
|
|
recent, ok := state["recent"].([]any)
|
|
if !ok {
|
|
t.Logf("Note: recent type after setup is %T (documenting behavior)", state["recent"])
|
|
} else if len(recent) == 0 {
|
|
t.Logf("Note: recent is empty (documenting behavior)")
|
|
}
|
|
}
|
|
|
|
func TestOpenCodeEdit_EmptyModels(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
configDir := filepath.Join(tmpDir, ".config", "opencode")
|
|
configPath := filepath.Join(configDir, "opencode.json")
|
|
|
|
os.MkdirAll(configDir, 0o755)
|
|
originalContent := `{"provider":{"ollama":{"models":{"existing":{}}}}}`
|
|
os.WriteFile(configPath, []byte(originalContent), 0o644)
|
|
|
|
// Empty models should be no-op
|
|
err := o.Edit([]string{})
|
|
if err != nil {
|
|
t.Fatalf("Edit with empty models failed: %v", err)
|
|
}
|
|
|
|
// Original content should be preserved (file not modified)
|
|
data, _ := os.ReadFile(configPath)
|
|
if string(data) != originalContent {
|
|
t.Errorf("empty models should not modify file, but content changed")
|
|
}
|
|
}
|
|
|
|
func TestOpenCodeEdit_SpecialCharsInModelName(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
// Model name with special characters (though unusual)
|
|
specialModel := `model-with-"quotes"`
|
|
|
|
err := o.Edit([]string{specialModel})
|
|
if err != nil {
|
|
t.Fatalf("Edit with special chars failed: %v", err)
|
|
}
|
|
|
|
// Verify it was stored correctly
|
|
configDir := filepath.Join(tmpDir, ".config", "opencode")
|
|
configPath := filepath.Join(configDir, "opencode.json")
|
|
data, _ := os.ReadFile(configPath)
|
|
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatalf("resulting config is invalid JSON: %v", err)
|
|
}
|
|
|
|
// Model should be accessible
|
|
provider, _ := cfg["provider"].(map[string]any)
|
|
ollama, _ := provider["ollama"].(map[string]any)
|
|
models, _ := ollama["models"].(map[string]any)
|
|
|
|
if models[specialModel] == nil {
|
|
t.Errorf("model with special chars not found in config")
|
|
}
|
|
}
|
|
|
|
func TestOpenCodeModels_NoConfig(t *testing.T) {
|
|
o := &OpenCode{}
|
|
tmpDir := t.TempDir()
|
|
setTestHome(t, tmpDir)
|
|
|
|
models := o.Models()
|
|
if len(models) > 0 {
|
|
t.Errorf("expected nil/empty for missing config, got %v", models)
|
|
}
|
|
}
|