返回 DeepSeek-Reasonix
project_provider_load_test.go
根目录 / internal / config / project_provider_load_test.go
1 package config
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7 )
8
9 func TestLoadForRootKeepsOfficialProviderAliasesDistinct(t *testing.T) {
10 isolateUserConfigHome(t)
11 root := t.TempDir()
12 userPath := UserConfigPath()
13 if err := os.MkdirAll(filepath.Dir(userPath), 0o755); err != nil {
14 t.Fatal(err)
15 }
16 if err := os.WriteFile(userPath, []byte(`
17 config_version = 2
18 default_model = "deepseek/deepseek-v4-flash"
19
20 [desktop]
21 provider_access = ["deepseek"]
22
23 [[providers]]
24 name = "deepseek"
25 kind = "openai"
26 base_url = "https://api.deepseek.com"
27 models = ["deepseek-v4-flash", "deepseek-v4-pro"]
28 default = "deepseek-v4-flash"
29 api_key_env = "USER_DEEPSEEK_KEY"
30 `), 0o644); err != nil {
31 t.Fatal(err)
32 }
33 if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte(`
34 [[providers]]
35 name = "deepseek-flash"
36 kind = "openai"
37 base_url = "https://api.deepseek.com"
38 model = "deepseek-v4-flash"
39 api_key_env = "PROJECT_DEEPSEEK_KEY"
40 effort = "max"
41 `), 0o644); err != nil {
42 t.Fatal(err)
43 }
44 approveWorkspace(t, root)
45
46 cfg, err := LoadForRoot(root)
47 if err != nil {
48 t.Fatalf("LoadForRoot: %v", err)
49 }
50 userProvider, ok := cfg.Provider("deepseek")
51 if !ok {
52 t.Fatalf("user deepseek provider missing: %+v", cfg.Providers)
53 }
54 if userProvider.APIKeyEnv != "USER_DEEPSEEK_KEY" {
55 t.Fatalf("deepseek provider = %+v, want user provider preserved", userProvider)
56 }
57 projectProvider, ok := cfg.Provider("deepseek-flash")
58 if !ok {
59 t.Fatalf("project deepseek-flash provider missing: %+v", cfg.Providers)
60 }
61 if projectProvider.APIKeyEnv != "PROJECT_DEEPSEEK_KEY" || projectProvider.Effort != "max" {
62 t.Fatalf("deepseek-flash provider = %+v, want project provider preserved", projectProvider)
63 }
64 }
65
66 func TestLoadForRootKeepsUserProviderOverSameNamedProjectProvider(t *testing.T) {
67 isolateUserConfigHome(t)
68 root := t.TempDir()
69 userPath := UserConfigPath()
70 if err := os.MkdirAll(filepath.Dir(userPath), 0o755); err != nil {
71 t.Fatal(err)
72 }
73 if err := os.WriteFile(userPath, []byte(`
74 [[providers]]
75 name = "shared"
76 kind = "openai"
77 base_url = "https://global.example/v1"
78 model = "global-model"
79 api_key_env = "GLOBAL_SHARED_KEY"
80 `), 0o644); err != nil {
81 t.Fatal(err)
82 }
83 if err := os.WriteFile(filepath.Join(root, "reasonix.toml"), []byte(`
84 [[providers]]
85 name = "shared"
86 kind = "openai"
87 base_url = "https://project.example/v1"
88 model = "project-model"
89 api_key_env = "PROJECT_SHARED_KEY"
90
91 [[providers]]
92 name = "project-only"
93 kind = "openai"
94 base_url = "https://project.example/v1"
95 model = "project-only-model"
96 api_key_env = "PROJECT_ONLY_KEY"
97 `), 0o644); err != nil {
98 t.Fatal(err)
99 }
100 approveWorkspace(t, root)
101
102 cfg, err := LoadForRoot(root)
103 if err != nil {
104 t.Fatalf("LoadForRoot: %v", err)
105 }
106 shared, ok := cfg.Provider("shared")
107 if !ok {
108 t.Fatalf("shared provider missing: %+v", cfg.Providers)
109 }
110 if shared.BaseURL != "https://global.example/v1" || shared.APIKeyEnv != "GLOBAL_SHARED_KEY" || shared.Model != "global-model" {
111 t.Fatalf("shared provider = %+v, want global provider to win over project provider", shared)
112 }
113 if _, ok := cfg.Provider("project-only"); !ok {
114 t.Fatalf("project-only provider missing: %+v", cfg.Providers)
115 }
116 }
117
118 func TestLoadForRootResolvesProviderCredentialsOverInheritedEnv(t *testing.T) {
119 project := t.TempDir()
120 cfgHome := t.TempDir()
121 key := "KEY_PROVIDER_GLOBAL_PRIORITY"
122
123 t.Setenv("HOME", cfgHome)
124 t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
125 t.Setenv("USERPROFILE", cfgHome)
126 t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
127 t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))
128 t.Setenv(key, "from_env")
129
130 cred := UserCredentialsPath()
131 if cred == "" {
132 t.Skip("user config dir unresolved on this platform")
133 }
134 if err := os.MkdirAll(filepath.Dir(cred), 0o755); err != nil {
135 t.Fatal(err)
136 }
137 if err := os.WriteFile(cred, []byte(key+"=from_credentials\n"), 0o600); err != nil {
138 t.Fatal(err)
139 }
140 if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
141 default_model = "custom/m"
142 [[providers]]
143 name = "custom"
144 kind = "openai"
145 base_url = "https://example.invalid/v1"
146 model = "m"
147 api_key_env = "`+key+`"
148 `), 0o644); err != nil {
149 t.Fatal(err)
150 }
151 approveWorkspace(t, project)
152
153 cfg, err := LoadForRoot(project)
154 if err != nil {
155 t.Fatalf("LoadForRoot: %v", err)
156 }
157 provider, ok := cfg.Provider("custom")
158 if !ok {
159 t.Fatalf("provider missing: %+v", cfg.Providers)
160 }
161 if got := provider.APIKey(); got != "from_credentials" {
162 t.Fatalf("provider API key = %q, want credentials value", got)
163 }
164 if got := os.Getenv(key); got != "from_credentials" {
165 t.Fatalf("process env = %q, want credentials value pinned over inherited env", got)
166 }
167 }
168
169 func TestLoadForRootIgnoresProjectProviderEnvAndInheritedEnv(t *testing.T) {
170 project := t.TempDir()
171 cfgHome := t.TempDir()
172 key := "KEY_PROVIDER_PROJECT_PRIORITY"
173
174 t.Setenv("HOME", cfgHome)
175 t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
176 t.Setenv("USERPROFILE", cfgHome)
177 t.Setenv("XDG_CONFIG_HOME", filepath.Join(cfgHome, ".config"))
178 t.Setenv("AppData", filepath.Join(cfgHome, "AppData"))
179 t.Setenv(key, "from_env")
180
181 if err := os.WriteFile(filepath.Join(project, ".env"), []byte(key+"=from_project\n"), 0o600); err != nil {
182 t.Fatal(err)
183 }
184 if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(`
185 default_model = "custom/m"
186 [[providers]]
187 name = "custom"
188 kind = "openai"
189 base_url = "https://example.invalid/v1"
190 model = "m"
191 api_key_env = "`+key+`"
192 `), 0o644); err != nil {
193 t.Fatal(err)
194 }
195 approveWorkspace(t, project)
196
197 cfg, err := LoadForRoot(project)
198 if err != nil {
199 t.Fatalf("LoadForRoot: %v", err)
200 }
201 provider, ok := cfg.Provider("custom")
202 if !ok {
203 t.Fatalf("provider missing: %+v", cfg.Providers)
204 }
205 if got := provider.APIKey(); got != "" {
206 t.Fatalf("provider API key = %q, want no key without global credentials", got)
207 }
208 if got := os.Getenv(key); got != "from_env" {
209 t.Fatalf("process env = %q, want inherited env left untouched", got)
210 }
211 }
212
212 lines GO