返回 DeepSeek-Reasonix
setup_key_env_test.go
根目录 / internal / cli / setup_key_env_test.go
1 package cli
2
3 import (
4 "bufio"
5 "bytes"
6 "errors"
7 "os"
8 "strings"
9 "testing"
10
11 "reasonix/internal/config"
12 )
13
14 const opencodeBaseURL = "https://opencode.ai/zen/go/v1"
15
16 func newKeyEnvTestSession(t *testing.T, fixture func(*config.Config)) (*providerSetupSession, string) {
17 t.Helper()
18 isolateUserConfig(t)
19 t.Setenv("REASONIX_HOME", t.TempDir())
20 for _, key := range []string{"CUSTOM_OPENCODE_AI_API_KEY", "OPENCODE_KEY", "ORPHAN_KEY", "SHARED_RELAY_KEY"} {
21 t.Setenv(key, "") // registers the restore; saving pins the key into the process
22 _ = os.Unsetenv(key)
23 }
24 path := config.UserConfigPath()
25 c := config.Default()
26 if fixture != nil {
27 fixture(c)
28 }
29 if err := c.SaveTo(path); err != nil {
30 t.Fatal(err)
31 }
32 c, err := config.LoadForEditReadOnlyStrict(path)
33 if err != nil {
34 t.Fatal(err)
35 }
36 return newProviderSetupSessionForPath(c, path), path
37 }
38
39 func addOpencodeThroughWizard(t *testing.T, s *providerSetupSession, input string, out *bytes.Buffer) bool {
40 t.Helper()
41 in := bufio.NewScanner(strings.NewReader(input))
42 result, err := promptCustomProviderManualWith(in, opencodeBaseURL, "", false, "sk-opencode")
43 if err != nil {
44 t.Fatal(err)
45 }
46 return s.addPrompted(in, out, result)
47 }
48
49 func savedKeyEnv(t *testing.T, path, provider string) string {
50 t.Helper()
51 saved, err := config.LoadForEditReadOnlyStrict(path)
52 if err != nil {
53 t.Fatal(err)
54 }
55 entry, ok := saved.Provider(provider)
56 if !ok {
57 t.Fatalf("provider %q was not saved", provider)
58 }
59 return entry.APIKeyEnv
60 }
61
62 func TestSetupHonoursATypedKeyEnvName(t *testing.T) {
63 s, path := newKeyEnvTestSession(t, nil)
64 var out bytes.Buffer
65 if !addOpencodeThroughWizard(t, s, "chat\nCUSTOM_OPENCODE_AI_API_KEY\n\n", &out) {
66 t.Fatalf("wizard refused the provider: %s", out.String())
67 }
68 if _, err := commitProviderSetupSession(s, path); err != nil {
69 t.Fatal(err)
70 }
71 if got := savedKeyEnv(t, path, "custom-opencode-ai"); got != "CUSTOM_OPENCODE_AI_API_KEY" {
72 t.Fatalf("api_key_env = %q, want the typed CUSTOM_OPENCODE_AI_API_KEY", got)
73 }
74 if res := config.ResolveCredentialForRootGlobalFirst(".", "CUSTOM_OPENCODE_AI_API_KEY"); res.Value != "sk-opencode" {
75 t.Fatalf("typed variable holds %q, want the entered key", res.Value)
76 }
77 }
78
79 func TestSetupEnterForDefaultKeepsAPrivateSlot(t *testing.T) {
80 s, path := newKeyEnvTestSession(t, nil)
81 var out bytes.Buffer
82 if !addOpencodeThroughWizard(t, s, "chat\n\n\n", &out) {
83 t.Fatalf("wizard refused the provider: %s", out.String())
84 }
85 if _, err := commitProviderSetupSession(s, path); err != nil {
86 t.Fatal(err)
87 }
88 if got := savedKeyEnv(t, path, "custom-opencode-ai"); !strings.HasPrefix(got, "REASONIX_CONNECTION_") {
89 t.Fatalf("api_key_env = %q, want a private REASONIX_CONNECTION_ slot", got)
90 }
91 }
92
93 func TestSetupRefusesATypedNameAnotherProviderHolds(t *testing.T) {
94 other := config.ProviderEntry{Name: "relay", Kind: "openai", BaseURL: "https://relay.invalid/v1", Model: "chat", APIKeyEnv: "SHARED_RELAY_KEY"}
95 s, path := newKeyEnvTestSession(t, func(c *config.Config) { c.Providers = append(c.Providers, other) })
96 var out bytes.Buffer
97 if !addOpencodeThroughWizard(t, s, "chat\nSHARED_RELAY_KEY\n\n\n", &out) {
98 t.Fatalf("wizard refused the provider: %s", out.String())
99 }
100 if !strings.Contains(out.String(), "SHARED_RELAY_KEY is already used by provider relay") {
101 t.Fatalf("wizard did not say why the typed name was refused: %q", out.String())
102 }
103 if _, err := commitProviderSetupSession(s, path); err != nil {
104 t.Fatal(err)
105 }
106 if got := savedKeyEnv(t, path, "custom-opencode-ai"); !strings.HasPrefix(got, "REASONIX_CONNECTION_") {
107 t.Fatalf("api_key_env = %q, want a private slot after Enter", got)
108 }
109 if config.CredentialStored("SHARED_RELAY_KEY") {
110 t.Fatal("the new provider's key was written into relay's variable")
111 }
112 }
113
114 func TestSetupRefusesATypedNameThatAlreadyHoldsAKey(t *testing.T) {
115 s, path := newKeyEnvTestSession(t, nil)
116 if _, err := config.SetCredential("ORPHAN_KEY", "sk-someone-else"); err != nil {
117 t.Fatal(err)
118 }
119 var out bytes.Buffer
120 if !addOpencodeThroughWizard(t, s, "chat\nORPHAN_KEY\n\nOPENCODE_KEY\n", &out) {
121 t.Fatalf("wizard refused the provider: %s", out.String())
122 }
123 if !strings.Contains(out.String(), "ORPHAN_KEY already holds a saved credential") {
124 t.Fatalf("wizard did not say why the typed name was refused: %q", out.String())
125 }
126 if _, err := commitProviderSetupSession(s, path); err != nil {
127 t.Fatal(err)
128 }
129 if got := savedKeyEnv(t, path, "custom-opencode-ai"); got != "OPENCODE_KEY" {
130 t.Fatalf("api_key_env = %q, want the second typed name", got)
131 }
132 if res := config.ResolveCredentialForRootGlobalFirst(".", "ORPHAN_KEY"); res.Value != "sk-someone-else" {
133 t.Fatalf("existing credential became %q", res.Value)
134 }
135 }
136
137 func TestSetupSaveRefusesATypedNameClaimedAfterTheWizard(t *testing.T) {
138 // A declared access list keeps the concurrent write from also changing which providers count as configured.
139 s, path := newKeyEnvTestSession(t, func(c *config.Config) { c.Desktop.ProviderAccess = []string{"deepseek"} })
140 var out bytes.Buffer
141 if !addOpencodeThroughWizard(t, s, "chat\nCUSTOM_OPENCODE_AI_API_KEY\n\n", &out) {
142 t.Fatalf("wizard refused the provider: %s", out.String())
143 }
144 if _, err := config.SetCredential("CUSTOM_OPENCODE_AI_API_KEY", "sk-written-meanwhile"); err != nil {
145 t.Fatal(err)
146 }
147 _, err := commitProviderSetupSession(s, path)
148 var inUse *config.CredentialKeyInUseError
149 if !errors.As(err, &inUse) || inUse.Holder != config.CredentialKeyHeldByStore {
150 t.Fatalf("commit error = %v, want CredentialKeyInUseError held by the store", err)
151 }
152 if res := config.ResolveCredentialForRootGlobalFirst(".", "CUSTOM_OPENCODE_AI_API_KEY"); res.Value != "sk-written-meanwhile" {
153 t.Fatalf("commit overwrote the concurrent credential with %q", res.Value)
154 }
155 }
156
157 func TestSetupRefusesATypedNameABotSettingReads(t *testing.T) {
158 s, path := newKeyEnvTestSession(t, nil)
159 secret := s.cfg.Bot.QQ.AppSecretEnv
160 var out bytes.Buffer
161 if !addOpencodeThroughWizard(t, s, "chat\n"+secret+"\n\n\n", &out) {
162 t.Fatalf("wizard refused the provider: %s", out.String())
163 }
164 if !strings.Contains(out.String(), secret+" is read by a bot or remote-host setting") {
165 t.Fatalf("wizard did not say why %s was refused: %q", secret, out.String())
166 }
167 if _, err := commitProviderSetupSession(s, path); err != nil {
168 t.Fatal(err)
169 }
170 if config.CredentialStored(secret) {
171 t.Fatalf("the provider key was written into the bot secret %s", secret)
172 }
173 }
174
175 func TestSetupRefusesATypedNameTheEnvironmentSets(t *testing.T) {
176 for _, key := range []string{"PATH", "OPENAI_API_KEY"} {
177 t.Run(key, func(t *testing.T) {
178 s, path := newKeyEnvTestSession(t, nil)
179 before := "shell-value"
180 if key == "PATH" {
181 before = os.Getenv("PATH")
182 }
183 t.Setenv(key, before)
184 var out bytes.Buffer
185 if !addOpencodeThroughWizard(t, s, "chat\n"+key+"\n\n\n", &out) {
186 t.Fatalf("wizard refused the provider: %s", out.String())
187 }
188 if !strings.Contains(out.String(), key+" is already set in the environment") {
189 t.Fatalf("wizard did not say why %s was refused: %q", key, out.String())
190 }
191 if _, err := commitProviderSetupSession(s, path); err != nil {
192 t.Fatal(err)
193 }
194 if got := os.Getenv(key); got != before {
195 t.Fatalf("%s changed to %q", key, got)
196 }
197 if config.CredentialStored(key) {
198 t.Fatalf("%s was written to the credential store", key)
199 }
200 })
201 }
202 }
203
204 func TestSetupRotatesAKeyUnderATypedNameInPlace(t *testing.T) {
205 typed := config.ProviderEntry{Name: "opencode", Kind: "openai", BaseURL: opencodeBaseURL, Model: "chat", APIKeyEnv: "CUSTOM_OPENCODE_AI_API_KEY"}
206 s, path := newKeyEnvTestSession(t, func(c *config.Config) { c.Providers = append(c.Providers, typed) })
207 if _, err := config.SetCredential("CUSTOM_OPENCODE_AI_API_KEY", "sk-old"); err != nil {
208 t.Fatal(err)
209 }
210 if err := s.setCredentialForProviders([]string{"opencode"}, "CUSTOM_OPENCODE_AI_API_KEY", "sk-new"); err != nil {
211 t.Fatal(err)
212 }
213 if _, err := commitProviderSetupSession(s, path); err != nil {
214 t.Fatal(err)
215 }
216 if got := savedKeyEnv(t, path, "opencode"); got != "CUSTOM_OPENCODE_AI_API_KEY" {
217 t.Fatalf("api_key_env = %q, want the typed name kept", got)
218 }
219 if res := config.ResolveCredentialForRootGlobalFirst(".", "CUSTOM_OPENCODE_AI_API_KEY"); res.Value != "sk-new" {
220 t.Fatalf("typed variable holds %q after rotation, want the new key", res.Value)
221 }
222 }
223
223 lines GO