返回 DeepSeek-Reasonix
credproxy_test.go
根目录 / internal / remote / bootstrap / credproxy_test.go
1 package bootstrap
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "reasonix/internal/remote"
11 )
12
13 func credProxyOpts(baseURL string) *CredentialProxyOptions {
14 return &CredentialProxyOptions{
15 BaseURL: baseURL,
16 Token: "virtual-token-123",
17 Provider: "reasonix-desktop-proxy",
18 Model: "deepseek-v4-flash",
19 }
20 }
21
22 // TestEnsureCredentialProviderAppendsAndIsIdempotent covers install and healing.
23 func TestEnsureCredentialProviderAppendsAndIsIdempotent(t *testing.T) {
24 skipOnWindows(t)
25 root := t.TempDir()
26 conn := newFakeConn(t, root, func(string) (remote.ExecResult, error) { return ok("") })
27 fs, err := conn.SFTP()
28 if err != nil {
29 t.Fatal(err)
30 }
31 ctx := context.Background()
32
33 if _, err := ensureCredentialProvider(ctx, fs, root, credProxyOpts("http://127.0.0.1:18999")); err != nil {
34 t.Fatalf("first install: %v", err)
35 }
36 first, rerr := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
37 if rerr != nil {
38 t.Fatal(rerr)
39 }
40 for _, want := range []string{
41 `[[providers]]`,
42 `name = "reasonix-desktop-proxy"`,
43 `base_url = "http://127.0.0.1:18999"`,
44 `api_key_env = "REASONIX_PROXY_TOKEN"`,
45 `model = "deepseek-v4-flash"`,
46 } {
47 if !strings.Contains(string(first), want) {
48 t.Fatalf("config missing %q:\n%s", want, first)
49 }
50 }
51
52 // Idempotent: same options ⇒ no rewrite.
53 if _, err := ensureCredentialProvider(ctx, fs, root, credProxyOpts("http://127.0.0.1:18999")); err != nil {
54 t.Fatalf("second install: %v", err)
55 }
56 second, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
57 if string(first) != string(second) {
58 t.Fatalf("idempotent run rewrote the config:\n%s\n---\n%s", first, second)
59 }
60
61 // A base_url change (tunnel port moved) rewrites only that assignment.
62 if _, err := ensureCredentialProvider(ctx, fs, root, credProxyOpts("http://127.0.0.1:19000")); err != nil {
63 t.Fatalf("port change: %v", err)
64 }
65 third, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
66 if !strings.Contains(string(third), `base_url = "http://127.0.0.1:19000"`) {
67 t.Fatalf("port change not applied:\n%s", third)
68 }
69 if strings.Count(string(third), "[[providers]]") != 1 {
70 t.Fatalf("port change duplicated the block:\n%s", third)
71 }
72 other := credProxyOpts("http://127.0.0.1:19000")
73 other.Provider, other.TokenEnv, other.Token = "reasonix-desktop-proxy-other", "REASONIX_PROXY_TOKEN_OTHER", "other-token"
74 if _, err := ensureCredentialProvider(ctx, fs, root, other); err != nil {
75 t.Fatalf("second workspace: %v", err)
76 }
77 cfg, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
78 env, _ := os.ReadFile(filepath.Join(root, ".reasonix", ".env"))
79 if !strings.Contains(string(cfg), `name = "reasonix-desktop-proxy-other"`) || !strings.Contains(string(env), "REASONIX_PROXY_TOKEN=virtual-token-123") || !strings.Contains(string(env), "REASONIX_PROXY_TOKEN_OTHER=other-token") {
80 t.Fatalf("workspace credentials collided:\nconfig=%s\nenv=%s", cfg, env)
81 }
82 healed := credProxyOpts("http://127.0.0.1:20000")
83 if _, err := ensureCredentialProvider(ctx, fs, root, healed); err != nil {
84 t.Fatalf("multi-workspace port heal: %v", err)
85 }
86 cfg, _ = os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
87 if got := strings.Count(string(cfg), `base_url = "http://127.0.0.1:20000"`); got != 2 {
88 t.Fatalf("managed workspace providers did not heal together (got %d):\n%s", got, cfg)
89 }
90 }
91
92 // TestEnsureCredentialProviderPreservesUserConfig keeps existing content.
93 func TestEnsureCredentialProviderPreservesUserConfig(t *testing.T) {
94 skipOnWindows(t)
95 root := t.TempDir()
96 if err := os.MkdirAll(filepath.Join(root, ".reasonix"), 0o755); err != nil {
97 t.Fatal(err)
98 }
99 existing := "default_model = \"deepseek/deepseek-v4-flash\"\n\n[[providers]]\nname = \"mine\"\nkind = \"openai\"\nbase_url = \"https://api.deepseek.com\"\napi_key_env = \"MY_KEY\"\n"
100 if err := os.WriteFile(filepath.Join(root, ".reasonix", "config.toml"), []byte(existing), 0o600); err != nil {
101 t.Fatal(err)
102 }
103 conn := newFakeConn(t, root, func(string) (remote.ExecResult, error) { return ok("") })
104 fs, err := conn.SFTP()
105 if err != nil {
106 t.Fatal(err)
107 }
108 if _, err := ensureCredentialProvider(context.Background(), fs, root, credProxyOpts("http://127.0.0.1:18999")); err != nil {
109 t.Fatal(err)
110 }
111 got, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
112 if !strings.HasPrefix(string(got), existing) {
113 t.Fatalf("user config prefix disturbed:\n%s", got)
114 }
115 if strings.Count(string(got), "[[providers]]") != 2 {
116 t.Fatalf("expected two provider blocks:\n%s", got)
117 }
118 if idx := providerBlockIndex(string(got), "mine"); idx < 0 {
119 t.Fatalf("user provider block lost:\n%s", got)
120 }
121 }
122
123 // TestLaunchCommandCredentialInjection keeps tokens out of the shell command.
124 func TestLaunchCommandCredentialInjection(t *testing.T) {
125 paths := StatePaths{Dir: "/d", TokenFile: "/d/t", PortFile: "/d/p", PidFile: "/d/i", LogFile: "/d/l"}
126 cmd := LaunchCommand("/usr/bin/reasonix", "/ws", paths, &CredentialProxyOptions{
127 BaseURL: "http://127.0.0.1:18999", Token: "to'ken $x", Provider: "reasonix-desktop-proxy", Model: "m",
128 }, nil)
129 for _, want := range []string{
130 `--model 'reasonix-desktop-proxy'`,
131 `nohup '/usr/bin/reasonix' serve`,
132 } {
133 if !strings.Contains(cmd, want) {
134 t.Errorf("LaunchCommand missing %q:\n%s", want, cmd)
135 }
136 }
137 if strings.Contains(cmd, "to'ken") || strings.Contains(cmd, TokenEnvName) {
138 t.Errorf("virtual token leaked into launch command:\n%s", cmd)
139 }
140 }
141
142 // TestEnsureCredentialProviderMaterializesBuiltinDefault keeps default_model valid.
143 func TestEnsureCredentialProviderMaterializesBuiltinDefault(t *testing.T) {
144 skipOnWindows(t)
145 root := t.TempDir()
146 if err := os.MkdirAll(filepath.Join(root, ".reasonix"), 0o755); err != nil {
147 t.Fatal(err)
148 }
149 cfg := "config_version = 6\n\ndefault_model = \"deepseek-flash\" # user's choice stays\n\n[ui]\ntheme = \"dark\"\n"
150 if err := os.WriteFile(filepath.Join(root, ".reasonix", "config.toml"), []byte(cfg), 0o600); err != nil {
151 t.Fatal(err)
152 }
153 conn := newFakeConn(t, root, func(string) (remote.ExecResult, error) { return ok("") })
154 fs, err := conn.SFTP()
155 if err != nil {
156 t.Fatal(err)
157 }
158 ctx := context.Background()
159 if _, err := ensureCredentialProvider(ctx, fs, root, credProxyOpts("http://127.0.0.1:18999")); err != nil {
160 t.Fatal(err)
161 }
162 first, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
163 got := string(first)
164 for _, want := range []string{
165 `default_model = "deepseek-flash" # user's choice stays`,
166 `name = "deepseek-flash"`,
167 `api_key_env = "DEEPSEEK_API_KEY"`,
168 `name = "reasonix-desktop-proxy"`,
169 } {
170 if !strings.Contains(got, want) {
171 t.Fatalf("config missing %q:\n%s", want, got)
172 }
173 }
174 if n := strings.Count(got, "[[providers]]"); n != 2 {
175 t.Fatalf("provider blocks = %d, want 2 (materialized + ours):\n%s", n, got)
176 }
177 // The materialized entry must come BEFORE ours so file order reads
178 // user-default then desktop-proxy.
179 if strings.Index(got, `name = "deepseek-flash"`) > strings.Index(got, `name = "reasonix-desktop-proxy"`) {
180 t.Fatalf("materialized entry should precede ours:\n%s", got)
181 }
182
183 // Idempotent.
184 if _, err := ensureCredentialProvider(ctx, fs, root, credProxyOpts("http://127.0.0.1:18999")); err != nil {
185 t.Fatal(err)
186 }
187 second, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
188 if string(first) != string(second) {
189 t.Fatalf("second run rewrote the config:\n%s\n---\n%s", first, second)
190 }
191 }
192
193 // TestMaterializeDefaultProviderSkipsNonBuiltin leaves user-owned gaps alone.
194 func TestMaterializeDefaultProviderSkipsNonBuiltin(t *testing.T) {
195 before := "default_model = \"custom/pro-model\"\n"
196 after := materializeDefaultProvider(before)
197 if before != after {
198 t.Fatalf("non-builtin default was rewritten:\n%s", after)
199 }
200 if got := defaultModelProvider("default_model = \"deepseek/deepseek-v4-flash\"\n"); got != "deepseek" {
201 t.Fatalf("provider extraction = %q, want deepseek", got)
202 }
203 if got := defaultModelProvider("[ui]\ndefault_model = \"deepseek-flash\"\n"); got != "" {
204 t.Fatalf("table-scoped default_model leaked: %q", got)
205 }
206 }
207
208 // TestEnsureCredentialProviderRewritesKindDrift heals provider kind changes.
209 func TestEnsureCredentialProviderRewritesKindDrift(t *testing.T) {
210 skipOnWindows(t)
211 root := t.TempDir()
212 conn := newFakeConn(t, root, func(string) (remote.ExecResult, error) { return ok("") })
213 fs, err := conn.SFTP()
214 if err != nil {
215 t.Fatal(err)
216 }
217 ctx := context.Background()
218
219 if _, err := ensureCredentialProvider(ctx, fs, root, credProxyOpts("http://127.0.0.1:18999")); err != nil {
220 t.Fatal(err)
221 }
222 installed, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
223 if !strings.Contains(string(installed), "kind = \"openai\"") {
224 t.Fatalf("default install missing the openai kind:\n%s", installed)
225 }
226
227 switched := credProxyOpts("http://127.0.0.1:18999")
228 switched.Kind = "anthropic"
229 if _, err := ensureCredentialProvider(ctx, fs, root, switched); err != nil {
230 t.Fatal(err)
231 }
232 rewritten, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
233 if !strings.Contains(string(rewritten), "kind = \"anthropic\"") || strings.Contains(string(rewritten), "kind = \"openai\"") {
234 t.Fatalf("kind drift not rewritten:\n%s", rewritten)
235 }
236 if !strings.Contains(string(rewritten), "base_url = \"http://127.0.0.1:18999\"") {
237 t.Fatalf("base_url lost in the kind rewrite:\n%s", rewritten)
238 }
239
240 // Idempotent once the kind matches.
241 if _, err := ensureCredentialProvider(ctx, fs, root, switched); err != nil {
242 t.Fatal(err)
243 }
244 again, _ := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
245 if string(again) != string(rewritten) {
246 t.Fatalf("matching re-run rewrote the config:\n%s\n---\n%s", rewritten, again)
247 }
248 }
249
250 func TestEnsureCredentialProviderPersistsLateBuiltinMaterialization(t *testing.T) {
251 skipOnWindows(t)
252 root := t.TempDir()
253 configDir := filepath.Join(root, ".reasonix")
254 if err := os.MkdirAll(configDir, 0o755); err != nil {
255 t.Fatal(err)
256 }
257 // Simulate an older desktop: its proxy block is current, but it never
258 // materialized the builtin selected by default_model.
259 legacy := "default_model = \"deepseek-flash\"\n" + credentialProviderBlock(credProxyOpts("http://127.0.0.1:18999"))
260 if err := os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(legacy), 0o600); err != nil {
261 t.Fatal(err)
262 }
263 conn := newFakeConn(t, root, func(string) (remote.ExecResult, error) { return ok("") })
264 fs, err := conn.SFTP()
265 if err != nil {
266 t.Fatal(err)
267 }
268 changed, err := ensureCredentialProvider(context.Background(), fs, root, credProxyOpts("http://127.0.0.1:18999"))
269 if err != nil {
270 t.Fatal(err)
271 }
272 if !changed {
273 t.Fatal("late builtin materialization was not reported as a config change")
274 }
275 got, err := os.ReadFile(filepath.Join(configDir, "config.toml"))
276 if err != nil {
277 t.Fatal(err)
278 }
279 if !strings.Contains(string(got), `name = "deepseek-flash"`) {
280 t.Fatalf("materialized builtin was not persisted:\n%s", got)
281 }
282 }
283
284 func TestEnsureCredentialProviderRewritesModelDrift(t *testing.T) {
285 skipOnWindows(t)
286 root := t.TempDir()
287 conn := newFakeConn(t, root, func(string) (remote.ExecResult, error) { return ok("") })
288 fs, err := conn.SFTP()
289 if err != nil {
290 t.Fatal(err)
291 }
292 ctx := context.Background()
293 if _, err := ensureCredentialProvider(ctx, fs, root, credProxyOpts("http://127.0.0.1:18999")); err != nil {
294 t.Fatal(err)
295 }
296 switched := credProxyOpts("http://127.0.0.1:18999")
297 switched.Model = "deepseek-v4-pro"
298 if _, err := ensureCredentialProvider(ctx, fs, root, switched); err != nil {
299 t.Fatal(err)
300 }
301 got, err := os.ReadFile(filepath.Join(root, ".reasonix", "config.toml"))
302 if err != nil {
303 t.Fatal(err)
304 }
305 if !strings.Contains(string(got), `model = "deepseek-v4-pro"`) || strings.Contains(string(got), `model = "deepseek-v4-flash"`) {
306 t.Fatalf("model drift not rewritten:\n%s", got)
307 }
308 }
309
309 lines GO