From e573b784ee4ef8ad14e3736372a0475c8148b012 Mon Sep 17 00:00:00 2001 From: catcatcat23 <670977359@qq.com> Date: Thu, 2 Apr 2026 22:27:47 +0800 Subject: [PATCH] fix(tui): sync selected model to correct config path and model_name --- cmd/picoclaw-launcher-tui/config/config.go | 16 +- .../config/config_test.go | 174 ++++++++++++++++++ cmd/picoclaw-launcher-tui/main.go | 4 +- 3 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 cmd/picoclaw-launcher-tui/config/config_test.go diff --git a/cmd/picoclaw-launcher-tui/config/config.go b/cmd/picoclaw-launcher-tui/config/config.go index 227b9fa3d..2b82459cd 100644 --- a/cmd/picoclaw-launcher-tui/config/config.go +++ b/cmd/picoclaw-launcher-tui/config/config.go @@ -151,15 +151,17 @@ func (p *Provider) UsersForScheme(schemeName string) []User { return out } -// SyncSelectedModelToMainConfig syncs the currently selected model to ~/.picoclaw/config.json +// SyncSelectedModelToMainConfig syncs the currently selected model to the main config file. // Adds/replaces a "tui-prefer" model entry and sets it as the default model. // Preserves all other existing fields in the config file unchanged. -func SyncSelectedModelToMainConfig(scheme Scheme, user User, modelID string) error { - home, err := os.UserHomeDir() - if err != nil { - home = "." +func SyncSelectedModelToMainConfig(mainConfigPath string, scheme Scheme, user User, modelID string) error { + if mainConfigPath == "" { + home, err := os.UserHomeDir() + if err != nil { + home = "." + } + mainConfigPath = filepath.Join(home, ".picoclaw", "config.json") } - mainConfigPath := filepath.Join(home, ".picoclaw", "config.json") var cfg map[string]any if data, readErr := os.ReadFile(mainConfigPath); readErr == nil { @@ -180,7 +182,7 @@ func SyncSelectedModelToMainConfig(scheme Scheme, user User, modelID string) err } defaults, ok := agents["defaults"].(map[string]any) if ok { - defaults["model"] = "tui-prefer" + defaults["model_name"] = "tui-prefer" } } diff --git a/cmd/picoclaw-launcher-tui/config/config_test.go b/cmd/picoclaw-launcher-tui/config/config_test.go new file mode 100644 index 000000000..9041c8871 --- /dev/null +++ b/cmd/picoclaw-launcher-tui/config/config_test.go @@ -0,0 +1,174 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestSyncSelectedModelToMainConfig_WritesModelNameAndModelList(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + + initial := map[string]any{ + "agents": map[string]any{ + "defaults": map[string]any{}, + }, + "model_list": []any{ + map[string]any{ + "model_name": "existing", + "model": "openai/gpt-4o-mini", + }, + }, + } + data, err := json.Marshal(initial) + if err != nil { + t.Fatalf("marshal initial config: %v", err) + } + if err = os.WriteFile(configPath, data, 0o600); err != nil { + t.Fatalf("write initial config: %v", err) + } + + scheme := Scheme{Name: "openai", BaseURL: "https://api.openai.com/v1"} + user := User{Name: "u1", Key: "sk-test"} + if err = SyncSelectedModelToMainConfig(configPath, scheme, user, "openai/gpt-5.4"); err != nil { + t.Fatalf("SyncSelectedModelToMainConfig() error = %v", err) + } + + updatedData, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("read updated config: %v", err) + } + + var updated map[string]any + if err = json.Unmarshal(updatedData, &updated); err != nil { + t.Fatalf("unmarshal updated config: %v", err) + } + + agents := updated["agents"].(map[string]any) + defaults := agents["defaults"].(map[string]any) + if got, ok := defaults["model_name"].(string); !ok || got != "tui-prefer" { + t.Fatalf("agents.defaults.model_name = %v, want %q", defaults["model_name"], "tui-prefer") + } + if _, exists := defaults["model"]; exists { + t.Fatalf("unexpected legacy field agents.defaults.model present: %v", defaults["model"]) + } + + modelList := updated["model_list"].([]any) + var tuiPrefer map[string]any + for _, item := range modelList { + entry, ok := item.(map[string]any) + if !ok { + continue + } + if name, _ := entry["model_name"].(string); name == "tui-prefer" { + tuiPrefer = entry + break + } + } + if tuiPrefer == nil { + t.Fatalf("tui-prefer model entry not found") + } + if got, _ := tuiPrefer["model"].(string); got != "openai/gpt-5.4" { + t.Fatalf("tui-prefer model = %q, want %q", got, "openai/gpt-5.4") + } + if got, _ := tuiPrefer["api_key"].(string); got != "sk-test" { + t.Fatalf("tui-prefer api_key = %q, want %q", got, "sk-test") + } + if got, _ := tuiPrefer["api_base"].(string); got != "https://api.openai.com/v1" { + t.Fatalf("tui-prefer api_base = %q, want %q", got, "https://api.openai.com/v1") + } +} + +func TestSyncSelectedModelToMainConfig_ReplacesExistingTuiPrefer(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + + initial := map[string]any{ + "agents": map[string]any{ + "defaults": map[string]any{ + "model_name": "old", + }, + }, + "model_list": []any{ + map[string]any{ + "model_name": "tui-prefer", + "model": "openai/old", + "api_key": "sk-old", + "api_base": "https://old.example.com/v1", + }, + }, + } + data, err := json.Marshal(initial) + if err != nil { + t.Fatalf("marshal initial config: %v", err) + } + if err = os.WriteFile(configPath, data, 0o600); err != nil { + t.Fatalf("write initial config: %v", err) + } + + scheme := Scheme{Name: "new", BaseURL: "https://new.example.com/v1"} + user := User{Name: "u2", Key: "sk-new"} + if err = SyncSelectedModelToMainConfig(configPath, scheme, user, "openai/new"); err != nil { + t.Fatalf("SyncSelectedModelToMainConfig() error = %v", err) + } + + updatedData, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("read updated config: %v", err) + } + + var updated map[string]any + if err = json.Unmarshal(updatedData, &updated); err != nil { + t.Fatalf("unmarshal updated config: %v", err) + } + + modelList := updated["model_list"].([]any) + count := 0 + for _, item := range modelList { + entry, ok := item.(map[string]any) + if !ok { + continue + } + name, _ := entry["model_name"].(string) + if name != "tui-prefer" { + continue + } + count++ + if got, _ := entry["model"].(string); got != "openai/new" { + t.Fatalf("tui-prefer model = %q, want %q", got, "openai/new") + } + if got, _ := entry["api_key"].(string); got != "sk-new" { + t.Fatalf("tui-prefer api_key = %q, want %q", got, "sk-new") + } + if got, _ := entry["api_base"].(string); got != "https://new.example.com/v1" { + t.Fatalf("tui-prefer api_base = %q, want %q", got, "https://new.example.com/v1") + } + } + if count != 1 { + t.Fatalf("tui-prefer entry count = %d, want 1", count) + } +} + +func TestSyncSelectedModelToMainConfig_ReturnsErrorOnInvalidPath(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + parentFile := filepath.Join(tmpDir, "not-a-dir") + if err := os.WriteFile(parentFile, []byte("x"), 0o600); err != nil { + t.Fatalf("write temp file: %v", err) + } + + configPath := filepath.Join(parentFile, "config.json") + scheme := Scheme{Name: "s1", BaseURL: "https://api.example.com/v1"} + user := User{Name: "u1", Key: "sk-test"} + err := SyncSelectedModelToMainConfig(configPath, scheme, user, "openai/gpt-5.4") + if err == nil { + t.Fatalf("expected error, got nil") + } +} diff --git a/cmd/picoclaw-launcher-tui/main.go b/cmd/picoclaw-launcher-tui/main.go index 3cb7110c1..dc035fd1f 100644 --- a/cmd/picoclaw-launcher-tui/main.go +++ b/cmd/picoclaw-launcher-tui/main.go @@ -39,7 +39,9 @@ func main() { app := ui.New(cfg, configPath) // Bind model selection hook to sync to main config app.OnModelSelected = func(scheme tuicfg.Scheme, user tuicfg.User, modelID string) { - _ = tuicfg.SyncSelectedModelToMainConfig(scheme, user, modelID) + if err := tuicfg.SyncSelectedModelToMainConfig(configPath, scheme, user, modelID); err != nil { + fmt.Fprintf(os.Stderr, "picoclaw-launcher-tui: failed to sync selected model: %v\n", err) + } } if err := app.Run(); err != nil { fmt.Fprintf(os.Stderr, "picoclaw-launcher-tui: %v\n", err)