返回 DeepSeek-Reasonix
credentials_keyring_timeout_test.go
根目录 / internal / config / credentials_keyring_timeout_test.go
1 package config
2
3 import (
4 "context"
5 "encoding/base64"
6 "strings"
7 "sync"
8 "testing"
9 "time"
10 )
11
12 func TestLookupLegacyKeyringBatchInProcessFourState(t *testing.T) {
13 oldLookup := legacyKeyringProbeLookup
14 t.Cleanup(func() { legacyKeyringProbeLookup = oldLookup })
15
16 legacyKeyringProbeLookup = func(_ context.Context, key string) legacyKeyringOutcome {
17 switch key {
18 case "FOUND":
19 return legacyKeyringOutcome{Status: legacyKeyringFound, Value: "secret"}
20 case "EMPTY":
21 return legacyKeyringOutcome{Status: legacyKeyringFound, Value: ""}
22 case "ERR":
23 return legacyKeyringOutcome{Status: legacyKeyringError}
24 default:
25 return legacyKeyringOutcome{Status: legacyKeyringAbsent}
26 }
27 }
28 got := lookupLegacyKeyringBatch([]string{"FOUND", "EMPTY", "MISSING", "ERR"}, time.Second)
29 if got["FOUND"].Status != legacyKeyringFound || got["FOUND"].Value != "" {
30 t.Fatalf("FOUND = %+v, want found with scrubbed value", got["FOUND"])
31 }
32 if !credentialCurrentStoreHasKey("FOUND") {
33 t.Fatal("FOUND secret should have been stored via store-if-absent")
34 }
35 if got["EMPTY"].Status != legacyKeyringAbsent {
36 t.Fatalf("EMPTY = %+v, want absent", got["EMPTY"])
37 }
38 if got["MISSING"].Status != legacyKeyringAbsent {
39 t.Fatalf("MISSING = %+v, want absent", got["MISSING"])
40 }
41 if got["ERR"].Status != legacyKeyringError {
42 t.Fatalf("ERR = %+v, want error", got["ERR"])
43 }
44 }
45
46 func TestLegacyKeyringErrorDoesNotWriteMarker(t *testing.T) {
47 home := t.TempDir()
48 t.Setenv("REASONIX_HOME", home)
49 t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
50
51 oldLookup := legacyKeyringProbeLookup
52 t.Cleanup(func() { legacyKeyringProbeLookup = oldLookup })
53
54 legacyKeyringProbeLookup = func(context.Context, string) legacyKeyringOutcome {
55 return legacyKeyringOutcome{Status: legacyKeyringError}
56 }
57 outcomes := lookupLegacyKeyringBatch([]string{"DEEPSEEK_API_KEY"}, time.Second)
58 if outcomes["DEEPSEEK_API_KEY"].Status != legacyKeyringError {
59 t.Fatalf("status = %+v", outcomes["DEEPSEEK_API_KEY"])
60 }
61 if outcomes["DEEPSEEK_API_KEY"].Status == legacyKeyringAbsent {
62 _ = markLegacyKeyringMigrationDone("DEEPSEEK_API_KEY")
63 }
64 if legacyKeyringMigrationDone("DEEPSEEK_API_KEY") {
65 t.Fatal("error outcome must not write a migration marker")
66 }
67 }
68
69 func TestLookupLegacyKeyringBatchSharedContextTimeout(t *testing.T) {
70 oldLookup := legacyKeyringProbeLookup
71 oldTimeout := legacyKeyringLookupTimeout
72 legacyKeyringLookupTimeout = 40 * time.Millisecond
73 t.Cleanup(func() {
74 legacyKeyringProbeLookup = oldLookup
75 legacyKeyringLookupTimeout = oldTimeout
76 })
77
78 legacyKeyringProbeLookup = func(ctx context.Context, key string) legacyKeyringOutcome {
79 if key == "FAST" {
80 return legacyKeyringOutcome{Status: legacyKeyringAbsent}
81 }
82 // Block until the shared budget cancels; must not leave a hung goroutine
83 // after the batch returns (probe returns when ctx is done).
84 <-ctx.Done()
85 return legacyKeyringOutcome{Status: legacyKeyringTimeout}
86 }
87
88 start := time.Now()
89 got := lookupLegacyKeyringBatch([]string{"FAST", "SLOW"}, legacyKeyringLookupTimeout)
90 elapsed := time.Since(start)
91 if got["FAST"].Status != legacyKeyringAbsent {
92 t.Fatalf("FAST = %+v", got["FAST"])
93 }
94 if got["SLOW"].Status != legacyKeyringTimeout {
95 t.Fatalf("SLOW = %+v, want timeout", got["SLOW"])
96 }
97 if elapsed > 250*time.Millisecond {
98 t.Fatalf("elapsed %v, shared budget did not bound the scan", elapsed)
99 }
100 }
101
102 // TestLookupLegacyKeyringBatchDoesNotClobberUserWrite forces the production
103 // probe→store window: the batch has already decided the key needs import
104 // (probe returns found), then the user writes a new value before store-if-absent.
105 func TestLookupLegacyKeyringBatchDoesNotClobberUserWrite(t *testing.T) {
106 home := t.TempDir()
107 t.Setenv("REASONIX_HOME", home)
108 t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
109
110 oldLookup := legacyKeyringProbeLookup
111 t.Cleanup(func() { legacyKeyringProbeLookup = oldLookup })
112
113 probeReached := make(chan struct{})
114 releaseProbe := make(chan struct{})
115 var once sync.Once
116 legacyKeyringProbeLookup = func(ctx context.Context, key string) legacyKeyringOutcome {
117 if key != "DEEPSEEK_API_KEY" {
118 return legacyKeyringOutcome{Status: legacyKeyringAbsent}
119 }
120 once.Do(func() { close(probeReached) })
121 select {
122 case <-releaseProbe:
123 case <-ctx.Done():
124 return legacyKeyringOutcome{Status: legacyKeyringTimeout}
125 }
126 return legacyKeyringOutcome{Status: legacyKeyringFound, Value: "sk-old-keyring"}
127 }
128
129 done := make(chan map[string]legacyKeyringOutcome, 1)
130 go func() {
131 done <- lookupLegacyKeyringBatch([]string{"DEEPSEEK_API_KEY"}, time.Second)
132 }()
133
134 select {
135 case <-probeReached:
136 case <-time.After(2 * time.Second):
137 t.Fatal("probe did not reach the interleaving checkpoint")
138 }
139 if _, err := SetCredential("DEEPSEEK_API_KEY", "sk-user-new"); err != nil {
140 t.Fatal(err)
141 }
142 close(releaseProbe)
143
144 got := <-done
145 if got["DEEPSEEK_API_KEY"].Status != legacyKeyringFound {
146 t.Fatalf("batch status = %+v, want found (store skipped)", got["DEEPSEEK_API_KEY"])
147 }
148 val, ok := envFileValue(UserCredentialsPath(), "DEEPSEEK_API_KEY")
149 if !ok || val != "sk-user-new" {
150 t.Fatalf("credential = (%q, %v), want sk-user-new (keyring must not clobber)", val, ok)
151 }
152 }
153
154 // TestLookupLegacyKeyringBatchDoesNotReviveTombstone forces the same production
155 // probe→store window against a cleared tombstone written after the probe starts.
156 func TestLookupLegacyKeyringBatchDoesNotReviveTombstone(t *testing.T) {
157 home := t.TempDir()
158 t.Setenv("REASONIX_HOME", home)
159 t.Setenv("REASONIX_CREDENTIALS_STORE", "file")
160
161 // Start empty: no value and no tombstone so the batch will probe.
162 oldLookup := legacyKeyringProbeLookup
163 t.Cleanup(func() { legacyKeyringProbeLookup = oldLookup })
164
165 probeReached := make(chan struct{})
166 releaseProbe := make(chan struct{})
167 var once sync.Once
168 legacyKeyringProbeLookup = func(ctx context.Context, key string) legacyKeyringOutcome {
169 if key != "DEEPSEEK_API_KEY" {
170 return legacyKeyringOutcome{Status: legacyKeyringAbsent}
171 }
172 once.Do(func() { close(probeReached) })
173 select {
174 case <-releaseProbe:
175 case <-ctx.Done():
176 return legacyKeyringOutcome{Status: legacyKeyringTimeout}
177 }
178 return legacyKeyringOutcome{Status: legacyKeyringFound, Value: "sk-old-keyring"}
179 }
180
181 done := make(chan map[string]legacyKeyringOutcome, 1)
182 go func() {
183 done <- lookupLegacyKeyringBatch([]string{"DEEPSEEK_API_KEY"}, time.Second)
184 }()
185
186 select {
187 case <-probeReached:
188 case <-time.After(2 * time.Second):
189 t.Fatal("probe did not reach the interleaving checkpoint")
190 }
191 // Write then clear while probe is blocked: tombstone must win.
192 if _, err := SetCredential("DEEPSEEK_API_KEY", "sk-temp"); err != nil {
193 t.Fatal(err)
194 }
195 if err := RemoveCredential("DEEPSEEK_API_KEY"); err != nil {
196 t.Fatal(err)
197 }
198 if !credentialCurrentStoreClearedKey("DEEPSEEK_API_KEY") {
199 t.Fatal("expected cleared tombstone before releasing probe")
200 }
201 close(releaseProbe)
202
203 got := <-done
204 if got["DEEPSEEK_API_KEY"].Status != legacyKeyringFound {
205 // store-if-absent skipped → still reported found (already handled)
206 t.Fatalf("batch status = %+v", got["DEEPSEEK_API_KEY"])
207 }
208 if credentialCurrentStoreHasKey("DEEPSEEK_API_KEY") {
209 t.Fatal("keyring import revived a tombstoned credential")
210 }
211 if !credentialCurrentStoreClearedKey("DEEPSEEK_API_KEY") {
212 t.Fatal("tombstone missing after batch")
213 }
214 }
215
216 func TestLegacyKeyringMarkerUsesRawURLBase64(t *testing.T) {
217 home := t.TempDir()
218 t.Setenv("REASONIX_HOME", home)
219 key := "A/B+C="
220 path := legacyKeyringMigrationMarkerPath(key)
221 wantName := base64.RawURLEncoding.EncodeToString([]byte(key))
222 if !strings.HasSuffix(path, wantName) {
223 t.Fatalf("marker path = %q, want suffix %q", path, wantName)
224 }
225 other := legacyKeyringMigrationMarkerPath("A_B_C_")
226 if path == other {
227 t.Fatalf("marker collision between %q and A_B_C_", key)
228 }
229 }
230
230 lines GO