| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | func migratedSelectionConfig(t *testing.T) *Config { |
| 11 | t.Helper() |
| 12 | c := &Config{ConfigVersion: 9, Providers: []ProviderEntry{{ |
| 13 | Name: "go", Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go", |
| 14 | APIKeyEnv: "SELECTION_TEST_KEY", Models: []string{"deepseek-v4-flash", "deepseek-v4-pro"}, Default: "deepseek-v4-flash", |
| 15 | }}} |
| 16 | j, _ := planOpenCodeGoUpgrade(c) |
| 17 | c.openCodeGoJournal = &j |
| 18 | c.ConfigVersion = 10 |
| 19 | return c |
| 20 | } |
| 21 | |
| 22 | func TestOpenCodeGoCurrentSelectionAfterConnectionEdit(t *testing.T) { |
| 23 | for _, tc := range []struct { |
| 24 | name string |
| 25 | edit func(*ProviderEntry) |
| 26 | }{ |
| 27 | {"proxy", func(p *ProviderEntry) { p.NoProxy = true }}, |
| 28 | {"headers", func(p *ProviderEntry) { p.Headers = map[string]string{"X-User": "changed"} }}, |
| 29 | {"credential reference", func(p *ProviderEntry) { p.APIKeyEnv = "NEW_SELECTION_TEST_KEY" }}, |
| 30 | } { |
| 31 | t.Run(tc.name, func(t *testing.T) { |
| 32 | c := migratedSelectionConfig(t) |
| 33 | tc.edit(&c.Providers[0]) |
| 34 | for _, ref := range []string{"go/deepseek-v4-flash", "go", "deepseek-v4-flash"} { |
| 35 | if err := c.ModelReferenceError(ref); err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | if e, ok := c.ResolveModel(ref); !ok || e.Model != "deepseek-v4-flash" { |
| 39 | t.Fatalf("current selection %q failed: %+v", ref, e) |
| 40 | } |
| 41 | if _, err := c.ResolveHistoricalModel(ref); err == nil || !strings.Contains(err.Error(), "MIGRATED_MODEL_UNAVAILABLE") { |
| 42 | t.Fatalf("historical identity was not protected: %v", err) |
| 43 | } |
| 44 | } |
| 45 | }) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func TestOpenCodeGoCurrentDefaultAndHistoricalDefault(t *testing.T) { |
| 50 | c := migratedSelectionConfig(t) |
| 51 | c.Providers[0].Default = "deepseek-v4-pro" |
| 52 | if e, ok := c.ResolveModel("go"); !ok || e.Model != "deepseek-v4-pro" { |
| 53 | t.Fatalf("current default ignored: %+v", e) |
| 54 | } |
| 55 | if ref, err := c.ResolveHistoricalModel("go"); err != nil || ref != "go/deepseek-v4-flash" { |
| 56 | t.Fatalf("historical default changed: %q, %v", ref, err) |
| 57 | } |
| 58 | c.Providers[0].Models = []string{"deepseek-v4-pro"} |
| 59 | if e, ok := c.ResolveModel("go"); !ok || e.Model != "deepseek-v4-pro" { |
| 60 | t.Fatalf("deleted old default blocked new default: %+v", e) |
| 61 | } |
| 62 | if _, err := c.ResolveHistoricalModel("go"); err == nil { |
| 63 | t.Fatal("deleted historical model must fail closed") |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | func TestOpenCodeGoSelectionEditSaveReload(t *testing.T) { |
| 68 | path := filepath.Join(t.TempDir(), "config.toml") |
| 69 | if err := os.WriteFile(path, []byte(`config_version = 9 |
| 70 | [[providers]] |
| 71 | name = "go" |
| 72 | kind = "anthropic" |
| 73 | base_url = "https://opencode.ai/zen/go" |
| 74 | api_key_env = "SELECTION_TEST_KEY" |
| 75 | models = ["deepseek-v4-flash", "deepseek-v4-pro"] |
| 76 | default = "deepseek-v4-flash" |
| 77 | `), 0600); err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | if _, err := ApplyUserConfigUpgradesOnStartup(path); err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | c := LoadForEdit(path) |
| 84 | if c.openCodeGoJournal == nil { |
| 85 | t.Fatal("missing durable migration journal") |
| 86 | } |
| 87 | c.Providers[0].NoProxy = true |
| 88 | c.Providers[0].Default = "deepseek-v4-pro" |
| 89 | if err := c.SaveToScope(path, RenderScopeFull); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | c = LoadForEdit(path) |
| 93 | if e, ok := c.ResolveModel("go"); !ok || e.Model != "deepseek-v4-pro" || !e.NoProxy { |
| 94 | t.Fatalf("selection edit did not survive reload: %+v", e) |
| 95 | } |
| 96 | if _, err := c.ResolveHistoricalModel("go"); err == nil { |
| 97 | t.Fatal("save erased the historical identity guard") |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func TestOpenCodeGoExplicitSelectionIdentity(t *testing.T) { |
| 102 | c := migratedSelectionConfig(t) |
| 103 | c.Providers[0].NoProxy = true |
| 104 | ref := "go/deepseek-v4-flash" |
| 105 | identity := c.ModelSelectionIdentity(ref) |
| 106 | if len(identity) != 64 || strings.Contains(identity, "SELECTION_TEST_KEY") { |
| 107 | t.Fatal("selection must contain only an identity digest") |
| 108 | } |
| 109 | if _, err := c.ResolveSavedModel(ref, ""); err == nil { |
| 110 | t.Fatal("legacy selection adopted edited connection") |
| 111 | } |
| 112 | if got, err := c.ResolveSavedModel(ref, identity); err != nil || got != ref { |
| 113 | t.Fatalf("acknowledged selection failed: %q, %v", got, err) |
| 114 | } |
| 115 | c.Providers[0].Default = "deepseek-v4-pro" |
| 116 | if _, err := c.ResolveSavedModel(ref, identity); err != nil { |
| 117 | t.Fatalf("default edit changed explicit model identity: %v", err) |
| 118 | } |
| 119 | c.Providers[0].APIKeyEnv = "OTHER_KEY" |
| 120 | if _, err := c.ResolveSavedModel(ref, identity); err == nil { |
| 121 | t.Fatal("acknowledgement silently followed a later account edit") |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | func TestOpenCodeGoAutomaticSearchNeverUsesAnotherAccountAfterEdit(t *testing.T) { |
| 126 | c := migratedSelectionConfig(t) |
| 127 | c.Providers[0].NoProxy = true |
| 128 | c.Providers = append(c.Providers, ProviderEntry{Name: "other", Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go", Model: "deepseek-v4-flash", APIKeyEnv: "OTHER_KEY", WebSearch: boolPointer(true)}) |
| 129 | for i := range c.Providers { |
| 130 | c.Providers[i].resolvedAPIKey = "test-key" |
| 131 | } |
| 132 | chat, _ := c.ResolveModel("go/deepseek-v4-flash") |
| 133 | if got := c.ResolveWebSearch(chat); got.Entry != nil { |
| 134 | t.Fatalf("automatic search crossed accounts: %s", got.Entry.Name) |
| 135 | } |
| 136 | if got, err := c.ResolveWebSearchModel("other/deepseek-v4-flash"); err != nil || got.Name != "other" { |
| 137 | t.Fatalf("explicit search choice blocked: %v", err) |
| 138 | } |
| 139 | } |
| 140 |