Files
ollama/cmd/launch/opencode_test.go
2026-04-14 15:28:40 -07:00

908 lines
26 KiB
Go

package launch
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"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) {
t.Run("builds config content with provider", func(t *testing.T) {
setTestHome(t, t.TempDir())
o := &OpenCode{}
if err := o.Edit([]string{"llama3.2"}); err != nil {
t.Fatal(err)
}
var cfg map[string]any
if err := json.Unmarshal([]byte(o.configContent), &cfg); err != nil {
t.Fatalf("configContent is not valid JSON: %v", err)
}
// Verify provider structure
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
if ollama["name"] != "Ollama" {
t.Errorf("provider name = %v, want Ollama", ollama["name"])
}
if ollama["npm"] != "@ai-sdk/openai-compatible" {
t.Errorf("npm = %v, want @ai-sdk/openai-compatible", ollama["npm"])
}
// Verify model exists
models, _ := ollama["models"].(map[string]any)
if models["llama3.2"] == nil {
t.Error("model llama3.2 not found in config content")
}
// Verify default model
if cfg["model"] != "ollama/llama3.2" {
t.Errorf("model = %v, want ollama/llama3.2", cfg["model"])
}
})
t.Run("multiple models", func(t *testing.T) {
setTestHome(t, t.TempDir())
o := &OpenCode{}
if err := o.Edit([]string{"llama3.2", "qwen3:32b"}); err != nil {
t.Fatal(err)
}
var cfg map[string]any
json.Unmarshal([]byte(o.configContent), &cfg)
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
models, _ := ollama["models"].(map[string]any)
if models["llama3.2"] == nil {
t.Error("model llama3.2 not found")
}
if models["qwen3:32b"] == nil {
t.Error("model qwen3:32b not found")
}
// First model should be the default
if cfg["model"] != "ollama/llama3.2" {
t.Errorf("default model = %v, want ollama/llama3.2", cfg["model"])
}
})
t.Run("empty models is no-op", func(t *testing.T) {
setTestHome(t, t.TempDir())
o := &OpenCode{}
if err := o.Edit([]string{}); err != nil {
t.Fatal(err)
}
if o.configContent != "" {
t.Errorf("expected empty configContent for no models, got %s", o.configContent)
}
})
t.Run("does not write config files", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
o := &OpenCode{}
o.Edit([]string{"llama3.2"})
configDir := filepath.Join(tmpDir, ".config", "opencode")
if _, err := os.Stat(filepath.Join(configDir, "opencode.json")); !os.IsNotExist(err) {
t.Error("opencode.json should not be created")
}
if _, err := os.Stat(filepath.Join(configDir, "opencode.jsonc")); !os.IsNotExist(err) {
t.Error("opencode.jsonc should not be created")
}
})
t.Run("cloud model has limits", func(t *testing.T) {
setTestHome(t, t.TempDir())
o := &OpenCode{}
if err := o.Edit([]string{"glm-4.7:cloud"}); err != nil {
t.Fatal(err)
}
var cfg map[string]any
json.Unmarshal([]byte(o.configContent), &cfg)
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
models, _ := ollama["models"].(map[string]any)
entry, _ := models["glm-4.7:cloud"].(map[string]any)
limit, ok := entry["limit"].(map[string]any)
if !ok {
t.Fatal("cloud model should have limit set")
}
expected := cloudModelLimits["glm-4.7"]
if limit["context"] != float64(expected.Context) {
t.Errorf("context = %v, want %d", limit["context"], expected.Context)
}
if limit["output"] != float64(expected.Output) {
t.Errorf("output = %v, want %d", limit["output"], expected.Output)
}
})
t.Run("local model has no limits", func(t *testing.T) {
setTestHome(t, t.TempDir())
o := &OpenCode{}
o.Edit([]string{"llama3.2"})
var cfg map[string]any
json.Unmarshal([]byte(o.configContent), &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["limit"] != nil {
t.Errorf("local model should not have limit, got %v", entry["limit"])
}
})
}
func TestOpenCodeModels_ReturnsNil(t *testing.T) {
o := &OpenCode{}
if models := o.Models(); models != nil {
t.Errorf("Models() = %v, want nil", models)
}
}
func TestOpenCodePaths(t *testing.T) {
t.Run("returns nil when model.json does not exist", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
o := &OpenCode{}
if paths := o.Paths(); paths != nil {
t.Errorf("Paths() = %v, want nil", paths)
}
})
t.Run("returns model.json path when it exists", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
os.WriteFile(filepath.Join(stateDir, "model.json"), []byte(`{}`), 0o644)
o := &OpenCode{}
paths := o.Paths()
if len(paths) != 1 {
t.Fatalf("Paths() returned %d paths, want 1", len(paths))
}
if paths[0] != filepath.Join(stateDir, "model.json") {
t.Errorf("Paths() = %v, want %v", paths[0], filepath.Join(stateDir, "model.json"))
}
})
}
func TestLookupCloudModelLimit(t *testing.T) {
tests := []struct {
name string
wantOK bool
wantContext int
wantOutput int
}{
{"glm-4.7", false, 0, 0},
{"glm-4.7:cloud", true, 202_752, 131_072},
{"glm-5:cloud", true, 202_752, 131_072},
{"glm-5.1:cloud", true, 202_752, 131_072},
{"gemma4:31b-cloud", true, 262_144, 131_072},
{"gpt-oss:120b-cloud", true, 131_072, 131_072},
{"gpt-oss:20b-cloud", true, 131_072, 131_072},
{"kimi-k2.5", false, 0, 0},
{"kimi-k2.5:cloud", true, 262_144, 262_144},
{"deepseek-v3.2", false, 0, 0},
{"deepseek-v3.2:cloud", true, 163_840, 65_536},
{"qwen3.5", false, 0, 0},
{"qwen3.5:cloud", true, 262_144, 32_768},
{"qwen3-coder:480b", false, 0, 0},
{"qwen3-coder:480b:cloud", true, 262_144, 65_536},
{"qwen3-coder-next:cloud", true, 262_144, 32_768},
{"llama3.2", false, 0, 0},
{"unknown-model:cloud", false, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l, ok := lookupCloudModelLimit(tt.name)
if ok != tt.wantOK {
t.Errorf("lookupCloudModelLimit(%q) ok = %v, want %v", tt.name, ok, tt.wantOK)
}
if ok {
if l.Context != tt.wantContext {
t.Errorf("context = %d, want %d", l.Context, tt.wantContext)
}
if l.Output != tt.wantOutput {
t.Errorf("output = %d, want %d", l.Output, tt.wantOutput)
}
}
})
}
}
// inlineConfigModel extracts a model entry from the inline config content.
func inlineConfigModel(t *testing.T, content, model string) map[string]any {
t.Helper()
var cfg map[string]any
if err := json.Unmarshal([]byte(content), &cfg); err != nil {
t.Fatalf("configContent is not valid JSON: %v", err)
}
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
models, _ := ollama["models"].(map[string]any)
entry, ok := models[model].(map[string]any)
if !ok {
t.Fatalf("model %s not found in inline config", model)
}
return entry
}
func TestOpenCodeEdit_ReasoningOnThinkingModel(t *testing.T) {
setTestHome(t, t.TempDir())
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/show" {
fmt.Fprintf(w, `{"capabilities":["thinking"],"model_info":{}}`)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
t.Setenv("OLLAMA_HOST", srv.URL)
o := &OpenCode{}
if err := o.Edit([]string{"qwq"}); err != nil {
t.Fatal(err)
}
entry := inlineConfigModel(t, o.configContent, "qwq")
if entry["reasoning"] != true {
t.Error("expected reasoning = true for thinking model")
}
variants, ok := entry["variants"].(map[string]any)
if !ok {
t.Fatal("expected variants to be set")
}
none, ok := variants["none"].(map[string]any)
if !ok {
t.Fatal("expected none variant to be set")
}
if none["reasoningEffort"] != "none" {
t.Errorf("none variant reasoningEffort = %v, want none", none["reasoningEffort"])
}
// Built-in low/medium/high should be disabled
for _, level := range []string{"low", "medium", "high"} {
v, ok := variants[level].(map[string]any)
if !ok {
t.Errorf("expected %s variant to exist", level)
continue
}
if v["disabled"] != true {
t.Errorf("expected %s variant to be disabled", level)
}
}
}
func TestOpenCodeEdit_ReasoningLevelsOnGptOss(t *testing.T) {
setTestHome(t, t.TempDir())
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/show" {
fmt.Fprintf(w, `{"capabilities":["thinking"],"model_info":{}}`)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
t.Setenv("OLLAMA_HOST", srv.URL)
o := &OpenCode{}
if err := o.Edit([]string{"gpt-oss:120b-cloud"}); err != nil {
t.Fatal(err)
}
entry := inlineConfigModel(t, o.configContent, "gpt-oss:120b-cloud")
if entry["reasoning"] != true {
t.Error("expected reasoning = true")
}
// GPT-OSS cannot turn thinking off and supports levels,
// so no custom variants should be written.
if entry["variants"] != nil {
t.Errorf("expected no variants for gpt-oss, got %v", entry["variants"])
}
// Should default to medium reasoning effort
opts, ok := entry["options"].(map[string]any)
if !ok {
t.Fatal("expected options to be set for gpt-oss")
}
if opts["reasoningEffort"] != "medium" {
t.Errorf("reasoningEffort = %v, want medium", opts["reasoningEffort"])
}
}
func TestOpenCodeEdit_NoReasoningOnNonThinkingModel(t *testing.T) {
setTestHome(t, t.TempDir())
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/show" {
fmt.Fprintf(w, `{"capabilities":[],"model_info":{}}`)
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
t.Setenv("OLLAMA_HOST", srv.URL)
o := &OpenCode{}
if err := o.Edit([]string{"llama3.2"}); err != nil {
t.Fatal(err)
}
entry := inlineConfigModel(t, o.configContent, "llama3.2")
if entry["reasoning"] != nil {
t.Errorf("expected no reasoning for non-thinking model, got %v", entry["reasoning"])
}
if entry["variants"] != nil {
t.Errorf("expected no variants for non-thinking model, got %v", entry["variants"])
}
}
func TestFindOpenCode(t *testing.T) {
t.Run("fallback to ~/.opencode/bin", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
// Ensure opencode is not on PATH
t.Setenv("PATH", tmpDir)
// Without the fallback binary, findOpenCode should fail
if _, ok := findOpenCode(); ok {
t.Fatal("findOpenCode should fail when binary is not on PATH or in fallback location")
}
// Create a fake binary at the curl install fallback location
binDir := filepath.Join(tmpDir, ".opencode", "bin")
os.MkdirAll(binDir, 0o755)
name := "opencode"
if runtime.GOOS == "windows" {
name = "opencode.exe"
}
fakeBin := filepath.Join(binDir, name)
os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755)
// Now findOpenCode should succeed via fallback
path, ok := findOpenCode()
if !ok {
t.Fatal("findOpenCode should succeed with fallback binary")
}
if path != fakeBin {
t.Errorf("findOpenCode = %q, want %q", path, fakeBin)
}
})
}
// Verify that the BackfillsCloudModelLimitOnExistingEntry test from the old
// file-based approach is covered by the new inline config approach.
func TestOpenCodeEdit_CloudModelLimitStructure(t *testing.T) {
o := &OpenCode{}
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
expected := cloudModelLimits["glm-4.7"]
if err := o.Edit([]string{"glm-4.7:cloud"}); err != nil {
t.Fatal(err)
}
var cfg map[string]any
json.Unmarshal([]byte(o.configContent), &cfg)
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
models, _ := ollama["models"].(map[string]any)
entry, _ := models["glm-4.7:cloud"].(map[string]any)
limit, ok := entry["limit"].(map[string]any)
if !ok {
t.Fatal("cloud model limit was not set")
}
if limit["context"] != float64(expected.Context) {
t.Errorf("context = %v, want %d", limit["context"], expected.Context)
}
if limit["output"] != float64(expected.Output) {
t.Errorf("output = %v, want %d", limit["output"], expected.Output)
}
}
func TestOpenCodeEdit_SpecialCharsInModelName(t *testing.T) {
o := &OpenCode{}
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
specialModel := `model-with-"quotes"`
err := o.Edit([]string{specialModel})
if err != nil {
t.Fatalf("Edit with special chars failed: %v", err)
}
var cfg map[string]any
if err := json.Unmarshal([]byte(o.configContent), &cfg); err != nil {
t.Fatalf("resulting config is invalid JSON: %v", err)
}
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 TestReadModelJSONModels(t *testing.T) {
t.Run("reads ollama models from model.json", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
state := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
map[string]any{"providerID": "ollama", "modelID": "qwen3:32b"},
},
}
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
models := readModelJSONModels()
if len(models) != 2 {
t.Fatalf("got %d models, want 2", len(models))
}
if models[0] != "llama3.2" || models[1] != "qwen3:32b" {
t.Errorf("got %v, want [llama3.2 qwen3:32b]", models)
}
})
t.Run("skips non-ollama providers", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
state := map[string]any{
"recent": []any{
map[string]any{"providerID": "openai", "modelID": "gpt-4"},
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
},
}
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
models := readModelJSONModels()
if len(models) != 1 || models[0] != "llama3.2" {
t.Errorf("got %v, want [llama3.2]", models)
}
})
t.Run("returns nil when file does not exist", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
if models := readModelJSONModels(); models != nil {
t.Errorf("got %v, want nil", models)
}
})
t.Run("returns nil for corrupt JSON", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
os.WriteFile(filepath.Join(stateDir, "model.json"), []byte(`{corrupt`), 0o644)
if models := readModelJSONModels(); models != nil {
t.Errorf("got %v, want nil", models)
}
})
}
func TestOpenCodeResolveContent(t *testing.T) {
t.Run("returns Edit's content when set", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
o := &OpenCode{}
if err := o.Edit([]string{"gemma4"}); err != nil {
t.Fatal(err)
}
editContent := o.configContent
// Write a different model.json — should be ignored
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
state := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "different-model"},
},
}
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
got := o.resolveContent("gemma4")
if got != editContent {
t.Errorf("resolveContent returned different content than Edit set\ngot: %s\nwant: %s", got, editContent)
}
})
t.Run("falls back to model.json when Edit was not called", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
state := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
map[string]any{"providerID": "ollama", "modelID": "qwen3:32b"},
},
}
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
content := o.resolveContent("llama3.2")
if content == "" {
t.Fatal("resolveContent returned empty")
}
var cfg map[string]any
json.Unmarshal([]byte(content), &cfg)
if cfg["model"] != "ollama/llama3.2" {
t.Errorf("primary = %v, want ollama/llama3.2", cfg["model"])
}
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
cfgModels, _ := ollama["models"].(map[string]any)
if cfgModels["llama3.2"] == nil || cfgModels["qwen3:32b"] == nil {
t.Errorf("expected both models in config, got %v", cfgModels)
}
})
t.Run("uses requested model as primary even when not first in model.json", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
state := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
map[string]any{"providerID": "ollama", "modelID": "qwen3:32b"},
},
}
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
content := o.resolveContent("qwen3:32b")
var cfg map[string]any
json.Unmarshal([]byte(content), &cfg)
if cfg["model"] != "ollama/qwen3:32b" {
t.Errorf("primary = %v, want ollama/qwen3:32b", cfg["model"])
}
})
t.Run("injects requested model when missing from model.json", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
state := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
},
}
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
content := o.resolveContent("gemma4")
var cfg map[string]any
json.Unmarshal([]byte(content), &cfg)
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
cfgModels, _ := ollama["models"].(map[string]any)
if cfgModels["gemma4"] == nil {
t.Error("requested model gemma4 not injected into config")
}
if cfg["model"] != "ollama/gemma4" {
t.Errorf("primary = %v, want ollama/gemma4", cfg["model"])
}
})
t.Run("returns empty when no model.json and no model param", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
o := &OpenCode{}
if got := o.resolveContent(""); got != "" {
t.Errorf("resolveContent(\"\") = %q, want empty", got)
}
})
t.Run("does not mutate configContent on fallback", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
state := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
},
}
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
_ = o.resolveContent("llama3.2")
if o.configContent != "" {
t.Errorf("resolveContent should not mutate configContent, got %q", o.configContent)
}
})
}
func TestBuildInlineConfig(t *testing.T) {
t.Run("returns error for empty primary", func(t *testing.T) {
if _, err := buildInlineConfig("", []string{"llama3.2"}); err == nil {
t.Error("expected error for empty primary")
}
})
t.Run("returns error for empty models", func(t *testing.T) {
if _, err := buildInlineConfig("llama3.2", nil); err == nil {
t.Error("expected error for empty models")
}
})
t.Run("primary differs from first model in list", func(t *testing.T) {
content, err := buildInlineConfig("qwen3:32b", []string{"llama3.2", "qwen3:32b"})
if err != nil {
t.Fatal(err)
}
var cfg map[string]any
json.Unmarshal([]byte(content), &cfg)
if cfg["model"] != "ollama/qwen3:32b" {
t.Errorf("primary = %v, want ollama/qwen3:32b", cfg["model"])
}
})
}
func TestOpenCodeEdit_PreservesRecentEntries(t *testing.T) {
t.Run("prepends new models to existing recent", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
initial := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "old-A"},
map[string]any{"providerID": "ollama", "modelID": "old-B"},
},
}
data, _ := json.MarshalIndent(initial, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
if err := o.Edit([]string{"new-X"}); err != nil {
t.Fatal(err)
}
stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
var state map[string]any
json.Unmarshal(stored, &state)
recent, _ := state["recent"].([]any)
if len(recent) != 3 {
t.Fatalf("expected 3 entries, got %d", len(recent))
}
first, _ := recent[0].(map[string]any)
if first["modelID"] != "new-X" {
t.Errorf("first entry = %v, want new-X", first["modelID"])
}
})
t.Run("prepends multiple new models in order", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
initial := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "old-A"},
map[string]any{"providerID": "ollama", "modelID": "old-B"},
},
}
data, _ := json.MarshalIndent(initial, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
if err := o.Edit([]string{"X", "Y", "Z"}); err != nil {
t.Fatal(err)
}
stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
var state map[string]any
json.Unmarshal(stored, &state)
recent, _ := state["recent"].([]any)
want := []string{"X", "Y", "Z", "old-A", "old-B"}
if len(recent) != len(want) {
t.Fatalf("expected %d entries, got %d", len(want), len(recent))
}
for i, w := range want {
e, _ := recent[i].(map[string]any)
if e["modelID"] != w {
t.Errorf("recent[%d] = %v, want %v", i, e["modelID"], w)
}
}
})
t.Run("preserves non-ollama entries", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
initial := map[string]any{
"recent": []any{
map[string]any{"providerID": "openai", "modelID": "gpt-4"},
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
},
}
data, _ := json.MarshalIndent(initial, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
if err := o.Edit([]string{"qwen3:32b"}); err != nil {
t.Fatal(err)
}
stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
var state map[string]any
json.Unmarshal(stored, &state)
recent, _ := state["recent"].([]any)
// Should have: qwen3:32b (new), gpt-4 (preserved openai), llama3.2 (preserved ollama)
var foundOpenAI bool
for _, entry := range recent {
e, _ := entry.(map[string]any)
if e["providerID"] == "openai" && e["modelID"] == "gpt-4" {
foundOpenAI = true
}
}
if !foundOpenAI {
t.Errorf("non-ollama gpt-4 entry was not preserved, got %v", recent)
}
})
t.Run("deduplicates ollama models being re-added", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
initial := map[string]any{
"recent": []any{
map[string]any{"providerID": "ollama", "modelID": "llama3.2"},
},
}
data, _ := json.MarshalIndent(initial, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
o := &OpenCode{}
if err := o.Edit([]string{"llama3.2"}); err != nil {
t.Fatal(err)
}
stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
var state map[string]any
json.Unmarshal(stored, &state)
recent, _ := state["recent"].([]any)
count := 0
for _, entry := range recent {
e, _ := entry.(map[string]any)
if e["modelID"] == "llama3.2" {
count++
}
}
if count != 1 {
t.Errorf("expected 1 llama3.2 entry, got %d", count)
}
})
t.Run("caps recent list at 10", func(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
stateDir := filepath.Join(tmpDir, ".local", "state", "opencode")
os.MkdirAll(stateDir, 0o755)
// Pre-populate with 9 distinct ollama models
recentEntries := make([]any, 0, 9)
for i := range 9 {
recentEntries = append(recentEntries, map[string]any{
"providerID": "ollama",
"modelID": fmt.Sprintf("old-%d", i),
})
}
initial := map[string]any{"recent": recentEntries}
data, _ := json.MarshalIndent(initial, "", " ")
os.WriteFile(filepath.Join(stateDir, "model.json"), data, 0o644)
// Add 5 new models — should cap at 10 total
o := &OpenCode{}
if err := o.Edit([]string{"new-0", "new-1", "new-2", "new-3", "new-4"}); err != nil {
t.Fatal(err)
}
stored, _ := os.ReadFile(filepath.Join(stateDir, "model.json"))
var state map[string]any
json.Unmarshal(stored, &state)
recent, _ := state["recent"].([]any)
if len(recent) != 10 {
t.Errorf("expected 10 entries (capped), got %d", len(recent))
}
})
}
func TestOpenCodeEdit_BaseURL(t *testing.T) {
o := &OpenCode{}
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
// Default OLLAMA_HOST
o.Edit([]string{"llama3.2"})
var cfg map[string]any
json.Unmarshal([]byte(o.configContent), &cfg)
provider, _ := cfg["provider"].(map[string]any)
ollama, _ := provider["ollama"].(map[string]any)
options, _ := ollama["options"].(map[string]any)
baseURL, _ := options["baseURL"].(string)
if baseURL == "" {
t.Error("baseURL should be set")
}
}