返回 DeepSeek-Reasonix
provider_access_group_removal_test.go
根目录 / desktop / provider_access_group_removal_test.go
1 package main
2
3 import (
4 "strings"
5 "testing"
6
7 "reasonix/internal/config"
8 )
9
10 func TestRemoveProviderAccessesRemovesGroupedOpenCodeGoRoutesAtomically(t *testing.T) {
11 isolateDesktopUserDirs(t)
12 setDesktopTestCredential(t, "REASONIX_TEST_KEY", "sk-test")
13
14 cfg := config.Default()
15 fallback := config.ProviderEntry{
16 Name: "mimo-pro", Kind: "openai", BaseURL: "https://mimo.example/v1",
17 Model: "mimo-v2.5-pro", APIKeyEnv: "REASONIX_TEST_KEY",
18 }
19 cfg.DefaultModel = "opencode-go/glm-5.3"
20 cfg.Agent.PlannerModel = "opencode-go-anthropic/qwen3.7-plus"
21 cfg.Agent.VisionModel = "opencode-go/kimi-k3"
22 cfg.Agent.RecoveryModel = "opencode-go-anthropic/qwen3.7-plus"
23 cfg.Agent.SubagentModel = "opencode-go-responses/grok-4.5"
24 cfg.Agent.SubagentModels = map[string]string{"review": "opencode-go/glm-5.3"}
25 cfg.Bot.Model = "opencode-go-anthropic/qwen3.7-plus"
26 cfg.Bot.QQ.Model = "opencode-go/glm-5.3"
27 cfg.Bot.Dingtalk.Model = "opencode-go-responses/grok-4.5"
28 cfg.Bot.Routes = []config.BotRouteConfig{{ConnectionID: "route", Model: "opencode-go-anthropic/qwen3.7-plus"}}
29 cfg.Bot.Connections = []config.BotConnectionConfig{
30 {ID: "opencode", Provider: "feishu", Model: "opencode-go-responses/grok-4.5"},
31 {ID: "fallback", Provider: "weixin", Model: "mimo-pro/mimo-v2.5-pro"},
32 }
33 cfg.Desktop.ProviderAccess = []string{
34 "opencode-go", "opencode-go-anthropic", "opencode-go-responses",
35 "opencode-go-deepseek-responses", "mimo-pro",
36 }
37 cfg.Providers = []config.ProviderEntry{
38 {
39 Name: "opencode-go", Kind: "openai", BaseURL: "https://opencode.ai/zen/go/v1",
40 Model: "glm-5.3", APIKeyEnv: "REASONIX_TEST_KEY", PresetID: "opencode-go",
41 },
42 {
43 Name: "opencode-go-anthropic", Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go",
44 Model: "qwen3.7-plus", APIKeyEnv: "REASONIX_TEST_KEY", PresetID: "opencode-go-anthropic",
45 },
46 {
47 Name: "opencode-go-responses", Kind: "responses", BaseURL: "https://opencode.ai/zen/go/v1",
48 Model: "grok-4.5", APIKeyEnv: "REASONIX_TEST_KEY", PresetID: "opencode-go-responses",
49 },
50 {
51 Name: "opencode-go-deepseek-responses", Kind: "responses", BaseURL: "https://opencode.ai/zen/go/v1",
52 Model: "deepseek-v4-flash", APIKeyEnv: "REASONIX_TEST_KEY", PresetID: "opencode-go-deepseek-responses",
53 },
54 fallback,
55 }
56 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
57 t.Fatalf("save config: %v", err)
58 }
59
60 app := NewApp()
61 tabs := []*WorkspaceTab{
62 {ID: "chat", Scope: "global", model: cfg.DefaultModel},
63 {ID: "planner", Scope: "global", model: cfg.Agent.PlannerModel},
64 {ID: "subagent", Scope: "global", model: cfg.Agent.SubagentModel},
65 }
66 app.tabs = map[string]*WorkspaceTab{}
67 for _, tab := range tabs {
68 app.tabs[tab.ID] = tab
69 }
70 app.tabOrder = []string{"chat", "planner", "subagent"}
71 app.activeTabID = "chat"
72
73 if err := app.RemoveProviderAccesses([]string{"opencode-go", "opencode-go-anthropic", "opencode-go-responses", "opencode-go-deepseek-responses"}); err != nil {
74 t.Fatalf("RemoveProviderAccesses: %v", err)
75 }
76
77 got := config.LoadForEdit(config.UserConfigPath())
78 access := providerAccessSet(got.Desktop.ProviderAccess)
79 if access["opencode-go"] || access["opencode-go-anthropic"] || access["opencode-go-responses"] || access["opencode-go-deepseek-responses"] || !access["mimo-pro"] {
80 t.Fatalf("provider_access = %+v, want only mimo-pro", got.Desktop.ProviderAccess)
81 }
82 for _, name := range []string{"opencode-go", "opencode-go-anthropic", "opencode-go-responses", "opencode-go-deepseek-responses"} {
83 if _, ok := got.Provider(name); ok {
84 t.Fatalf("provider %q still exists after grouped removal", name)
85 }
86 }
87 wantFallback := "mimo-pro"
88 if got.DefaultModel != wantFallback || got.Agent.PlannerModel != wantFallback || got.Agent.RecoveryModel != "" || got.Agent.SubagentModel != wantFallback || got.Agent.SubagentModels["review"] != wantFallback {
89 t.Fatalf("provider refs were not retargeted: default=%q planner=%q recovery=%q subagent=%q skills=%+v", got.DefaultModel, got.Agent.PlannerModel, got.Agent.RecoveryModel, got.Agent.SubagentModel, got.Agent.SubagentModels)
90 }
91 if got.Agent.VisionModel != "" {
92 t.Fatalf("vision_model = %q, want cleared removed provider reference", got.Agent.VisionModel)
93 }
94 if got.Bot.Model != wantFallback || got.Bot.QQ.Model != wantFallback || got.Bot.Dingtalk.Model != wantFallback || len(got.Bot.Routes) != 1 || got.Bot.Routes[0].Model != wantFallback || len(got.Bot.Connections) != 2 || got.Bot.Connections[0].Model != wantFallback || got.Bot.Connections[1].Model != "mimo-pro/mimo-v2.5-pro" {
95 t.Fatalf("bot refs were not retargeted selectively: model=%q qq=%q dingtalk=%q routes=%+v connections=%+v", got.Bot.Model, got.Bot.QQ.Model, got.Bot.Dingtalk.Model, got.Bot.Routes, got.Bot.Connections)
96 }
97 for index, tab := range tabs {
98 if tab.model != []string{cfg.DefaultModel, cfg.Agent.PlannerModel, cfg.Agent.SubagentModel}[index] {
99 t.Fatalf("saving grouped removal changed tab %q model = %q", tab.ID, tab.model)
100 }
101 }
102 }
103
104 func TestProviderRemovalStateFingerprintCoversAuxiliaryModelReferences(t *testing.T) {
105 newConfig := func() *config.Config {
106 cfg := config.Default()
107 cfg.Providers = []config.ProviderEntry{{Name: "provider", Kind: "openai", BaseURL: "https://provider.example/v1", Model: "model"}}
108 return cfg
109 }
110 base := providerRemovalStateFingerprint(newConfig(), "revision")
111 tests := []struct {
112 name string
113 mutate func(*config.Config)
114 }{
115 {name: "vision", mutate: func(cfg *config.Config) { cfg.Agent.VisionModel = "provider/model" }},
116 {name: "guardian", mutate: func(cfg *config.Config) { cfg.Agent.GuardianModel = "provider/model" }},
117 {name: "bot default", mutate: func(cfg *config.Config) { cfg.Bot.Model = "provider/model" }},
118 {name: "qq", mutate: func(cfg *config.Config) { cfg.Bot.QQ.Model = "provider/model" }},
119 {name: "dingtalk", mutate: func(cfg *config.Config) { cfg.Bot.Dingtalk.Model = "provider/model" }},
120 {name: "route", mutate: func(cfg *config.Config) { cfg.Bot.Routes = []config.BotRouteConfig{{Model: "provider/model"}} }},
121 {name: "connection", mutate: func(cfg *config.Config) {
122 cfg.Bot.Connections = []config.BotConnectionConfig{{Model: "provider/model"}}
123 }},
124 }
125 for _, tt := range tests {
126 t.Run(tt.name, func(t *testing.T) {
127 cfg := newConfig()
128 tt.mutate(cfg)
129 if got := providerRemovalStateFingerprint(cfg, "revision"); got == base {
130 t.Fatalf("fingerprint did not cover %s model reference", tt.name)
131 }
132 })
133 }
134 }
135
136 func TestRemoveProviderAccessesKeepsCoreRoutesWhenRemovingOpenCodeGoSearchSubset(t *testing.T) {
137 isolateDesktopUserDirs(t)
138 setDesktopTestCredential(t, "REASONIX_TEST_KEY", "sk-test")
139
140 coreNames := []string{"opencode-go", "opencode-go-anthropic", "opencode-go-responses"}
141 searchNames := []string{"opencode-go-deepseek-anthropic", "opencode-go-deepseek-responses"}
142 cfg := config.Default()
143 cfg.DefaultModel = "opencode-go/glm-5.3"
144 cfg.Desktop.ProviderAccess = append(append(append([]string(nil), coreNames...), searchNames...), "fallback")
145 cfg.Providers = []config.ProviderEntry{
146 {Name: "opencode-go", Kind: "openai", BaseURL: "https://opencode.ai/zen/go/v1", Model: "glm-5.3", APIKeyEnv: "REASONIX_TEST_KEY"},
147 {Name: "opencode-go-anthropic", Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go", Model: "qwen3.7-plus", APIKeyEnv: "REASONIX_TEST_KEY"},
148 {Name: "opencode-go-responses", Kind: "responses", BaseURL: "https://opencode.ai/zen/go/v1", Model: "grok-4.5", APIKeyEnv: "REASONIX_TEST_KEY"},
149 {Name: "opencode-go-deepseek-anthropic", Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go", Model: "deepseek-v4-flash", APIKeyEnv: "REASONIX_TEST_KEY"},
150 {Name: "opencode-go-deepseek-responses", Kind: "responses", BaseURL: "https://opencode.ai/zen/go/v1", Model: "deepseek-v4-flash", APIKeyEnv: "REASONIX_TEST_KEY"},
151 {Name: "fallback", Kind: "openai", BaseURL: "https://fallback.example/v1", Model: "fallback-model", APIKeyEnv: "REASONIX_TEST_KEY"},
152 }
153 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
154 t.Fatalf("save config: %v", err)
155 }
156
157 if err := NewApp().RemoveProviderAccesses(searchNames); err != nil {
158 t.Fatalf("RemoveProviderAccesses: %v", err)
159 }
160
161 got := config.LoadForEdit(config.UserConfigPath())
162 for _, name := range coreNames {
163 if _, ok := got.Provider(name); !ok {
164 t.Fatalf("core OpenCode Go provider %q was removed with the search subset", name)
165 }
166 }
167 for _, name := range searchNames {
168 if _, ok := got.Provider(name); ok {
169 t.Fatalf("search provider %q still exists", name)
170 }
171 }
172 if got.DefaultModel != cfg.DefaultModel {
173 t.Fatalf("default_model = %q, want preserved %q", got.DefaultModel, cfg.DefaultModel)
174 }
175 }
176
177 func TestRemoveProviderAccessesStillRejectsUnrelatedCustomBatch(t *testing.T) {
178 isolateDesktopUserDirs(t)
179 setDesktopTestCredential(t, "REASONIX_TEST_KEY", "sk-test")
180
181 cfg := config.Default()
182 cfg.Desktop.ProviderAccess = []string{"custom-a", "custom-b"}
183 cfg.Providers = []config.ProviderEntry{
184 {Name: "custom-a", Kind: "openai", BaseURL: "https://a.example/v1", Model: "a", APIKeyEnv: "REASONIX_TEST_KEY"},
185 {Name: "custom-b", Kind: "openai", BaseURL: "https://b.example/v1", Model: "b", APIKeyEnv: "REASONIX_TEST_KEY"},
186 }
187 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
188 t.Fatalf("save config: %v", err)
189 }
190
191 err := NewApp().RemoveProviderAccesses([]string{"custom-a", "custom-b"})
192 if err == nil || !strings.Contains(err.Error(), "custom provider") {
193 t.Fatalf("RemoveProviderAccesses error = %v, want custom-provider batch rejection", err)
194 }
195 got := config.LoadForEdit(config.UserConfigPath())
196 if _, ok := got.Provider("custom-a"); !ok {
197 t.Fatal("custom-a was deleted after rejected batch")
198 }
199 if _, ok := got.Provider("custom-b"); !ok {
200 t.Fatal("custom-b was deleted after rejected batch")
201 }
202 }
203
203 lines GO