launch: Fix launch provider drift (#16683)

This commit is contained in:
Parth Sareen
2026-06-11 17:21:46 -07:00
committed by GitHub
parent f8a48df24d
commit 12e04379cd
2 changed files with 145 additions and 7 deletions

View File

@@ -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

View File

@@ -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 {