From 12e04379cd5a9a2f150104a02c5761213f4be777 Mon Sep 17 00:00:00 2001 From: Parth Sareen Date: Thu, 11 Jun 2026 17:21:46 -0700 Subject: [PATCH] launch: Fix launch provider drift (#16683) --- cmd/launch/launch.go | 3 +- cmd/launch/launch_test.go | 149 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/cmd/launch/launch.go b/cmd/launch/launch.go index ef5b7f92..42e92826 100644 --- a/cmd/launch/launch.go +++ b/cmd/launch/launch.go @@ -764,7 +764,8 @@ func (c *launcherClient) launchEditorIntegration(ctx context.Context, name strin } var launchModels []LaunchModel - if (needsConfigure || req.ModelOverride != "") && !savedMatchesModels(saved, models) { + liveConfigMatches := slices.Equal(editor.Models(), models) + if needsConfigure || req.ModelOverride != "" || !savedMatchesModels(saved, models) || !liveConfigMatches { launchModels = c.modelInventory().Resolve(ctx, models) if err := prepareEditorIntegration(name, editor, launchModels); err != nil { return err diff --git a/cmd/launch/launch_test.go b/cmd/launch/launch_test.go index 7f4e00f9..ca1cd508 100644 --- a/cmd/launch/launch_test.go +++ b/cmd/launch/launch_test.go @@ -17,10 +17,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/ollama/ollama/cmd/config" + "github.com/ollama/ollama/cmd/internal/fileutil" ) type launcherEditorRunner struct { paths []string + models []string edited [][]string ranModel string } @@ -35,11 +37,14 @@ func (r *launcherEditorRunner) String() string { return "LauncherEditor" } func (r *launcherEditorRunner) Paths() []string { return r.paths } func (r *launcherEditorRunner) Edit(models []LaunchModel) error { - r.edited = append(r.edited, launchModelNames(models)) + names := launchModelNames(models) + r.edited = append(r.edited, names) return nil } -func (r *launcherEditorRunner) Models() []string { return nil } +func (r *launcherEditorRunner) Models() []string { + return append([]string(nil), r.models...) +} type launcherSingleRunner struct { ranModel string @@ -2208,6 +2213,86 @@ func TestLaunchIntegration_EditorForceConfigure(t *testing.T) { } } +func TestLaunchIntegration_ClineRewritesWhenLiveProviderDrifted(t *testing.T) { + tmpDir := t.TempDir() + setLaunchTestHome(t, tmpDir) + withLauncherHooks(t) + + binDir := t.TempDir() + writeFakeBinary(t, binDir, "cline") + t.Setenv("PATH", binDir) + + if err := config.SaveIntegration("cline", []string{"llama3.2"}); err != nil { + t.Fatalf("failed to seed saved config: %v", err) + } + + providersPath := clineProvidersPath(tmpDir) + if err := os.MkdirAll(filepath.Dir(providersPath), 0o755); err != nil { + t.Fatalf("failed to create providers dir: %v", err) + } + existingProviders := map[string]any{ + "version": float64(1), + "lastUsedProvider": "openai-codex-cli", + "providers": map[string]any{ + "openai-codex-cli": map[string]any{ + "settings": map[string]any{ + "provider": "openai-codex-cli", + "model": "gpt-5.5", + "reasoning": "medium", + }, + "updatedAt": "2026-06-01T12:00:00Z", + "tokenSource": "manual", + }, + }, + } + data, _ := json.Marshal(existingProviders) + if err := os.WriteFile(providersPath, data, 0o644); err != nil { + t.Fatalf("failed to seed providers config: %v", err) + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/show": + var req apiShowRequest + _ = json.NewDecoder(r.Body).Decode(&req) + fmt.Fprintf(w, `{"model":%q}`, req.Model) + default: + http.NotFound(w, r) + } + })) + defer srv.Close() + t.Setenv("OLLAMA_HOST", srv.URL) + + if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{ + Name: "cline", + }); err != nil { + t.Fatalf("LaunchIntegration returned error: %v", err) + } + + providersConfig, err := fileutil.ReadJSON(providersPath) + if err != nil { + t.Fatalf("failed to read providers config: %v", err) + } + if providersConfig["lastUsedProvider"] != clineLaunchProvider { + t.Fatalf("lastUsedProvider = %v, want %s", providersConfig["lastUsedProvider"], clineLaunchProvider) + } + providers, _ := providersConfig["providers"].(map[string]any) + if _, ok := providers["openai-codex-cli"]; !ok { + t.Fatal("expected existing openai-codex-cli provider to be preserved") + } + ollamaProvider, _ := providers[clineLaunchProvider].(map[string]any) + settings, _ := ollamaProvider["settings"].(map[string]any) + if settings["provider"] != clineLaunchProvider { + t.Fatalf("ollama settings.provider = %v, want %s", settings["provider"], clineLaunchProvider) + } + if settings["model"] != "llama3.2" { + t.Fatalf("ollama settings.model = %v, want llama3.2", settings["model"]) + } + if settings["baseUrl"] != srv.URL+"/v1" { + t.Fatalf("ollama settings.baseUrl = %v, want %s/v1", settings["baseUrl"], srv.URL) + } +} + func TestLaunchIntegration_EditorForceConfigure_FloatsCheckedModelsInPicker(t *testing.T) { tmpDir := t.TempDir() setLaunchTestHome(t, tmpDir) @@ -2217,7 +2302,7 @@ func TestLaunchIntegration_EditorForceConfigure_FloatsCheckedModelsInPicker(t *t writeFakeBinary(t, binDir, "droid") t.Setenv("PATH", binDir) - editor := &launcherEditorRunner{} + editor := &launcherEditorRunner{models: []string{"llama3.2", "missing-local"}} withIntegrationOverride(t, "droid", editor) if err := config.SaveIntegration("droid", []string{"qwen3.5:cloud", "qwen3.5"}); err != nil { @@ -2792,7 +2877,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchValidatesPrimaryOnly(t *testing writeFakeBinary(t, binDir, "droid") t.Setenv("PATH", binDir) - editor := &launcherEditorRunner{} + editor := &launcherEditorRunner{models: []string{"llama3.2", "missing-local"}} withIntegrationOverride(t, "droid", editor) if err := config.SaveIntegration("droid", []string{"llama3.2", "missing-local"}); err != nil { @@ -2861,7 +2946,7 @@ func TestLaunchIntegration_ConfiguredEditorLaunchSkipsReconfigure(t *testing.T) if err := os.WriteFile(settingsPath, []byte("{}"), 0o644); err != nil { t.Fatalf("failed to seed editor settings: %v", err) } - editor := &launcherEditorRunner{paths: []string{settingsPath}} + editor := &launcherEditorRunner{paths: []string{settingsPath}, models: []string{"llama3.2", "qwen3:8b"}} withIntegrationOverride(t, "droid", editor) if err := config.SaveIntegration("droid", []string{"llama3.2", "qwen3:8b"}); err != nil { @@ -2904,6 +2989,58 @@ func TestLaunchIntegration_ConfiguredEditorLaunchSkipsReconfigure(t *testing.T) } } +func TestLaunchIntegration_ConfiguredEditorLaunchRewritesDriftedLiveConfig(t *testing.T) { + tmpDir := t.TempDir() + setLaunchTestHome(t, tmpDir) + withLauncherHooks(t) + + binDir := t.TempDir() + writeFakeBinary(t, binDir, "droid") + t.Setenv("PATH", binDir) + + editor := &launcherEditorRunner{models: []string{"qwen3:8b"}} + withIntegrationOverride(t, "droid", editor) + + if err := config.SaveIntegration("droid", []string{"llama3.2", "mistral"}); err != nil { + t.Fatalf("failed to seed config: %v", err) + } + + DefaultConfirmPrompt = func(prompt string, options ConfirmOptions) (bool, error) { + t.Fatalf("did not expect prompt during a normal editor launch: %s", prompt) + return false, nil + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/show" { + var req apiShowRequest + _ = json.NewDecoder(r.Body).Decode(&req) + fmt.Fprintf(w, `{"model":%q}`, req.Model) + return + } + http.NotFound(w, r) + })) + defer srv.Close() + t.Setenv("OLLAMA_HOST", srv.URL) + + if err := LaunchIntegration(context.Background(), IntegrationLaunchRequest{Name: "droid"}); err != nil { + t.Fatalf("LaunchIntegration returned error: %v", err) + } + if diff := cmp.Diff([][]string{{"llama3.2", "mistral"}}, editor.edited); diff != "" { + t.Fatalf("expected editor config rewrite when live config drifts (-want +got):\n%s", diff) + } + if editor.ranModel != "llama3.2" { + t.Fatalf("expected launch to use saved primary model, got %q", editor.ranModel) + } + + saved, err := config.LoadIntegration("droid") + if err != nil { + t.Fatalf("failed to reload saved config: %v", err) + } + if diff := compareStrings(saved.Models, []string{"llama3.2", "mistral"}); diff != "" { + t.Fatalf("unexpected saved models (-want +got):\n%s", diff) + } +} + func TestLaunchIntegration_OpenclawPreservesExistingModelList(t *testing.T) { tmpDir := t.TempDir() setLaunchTestHome(t, tmpDir) @@ -2913,7 +3050,7 @@ func TestLaunchIntegration_OpenclawPreservesExistingModelList(t *testing.T) { writeFakeBinary(t, binDir, "openclaw") t.Setenv("PATH", binDir) - editor := &launcherEditorRunner{} + editor := &launcherEditorRunner{models: []string{"llama3.2", "mistral"}} withIntegrationOverride(t, "openclaw", editor) if err := config.SaveIntegration("openclaw", []string{"llama3.2", "mistral"}); err != nil {