返回 DeepSeek-Reasonix
model_settings_retry_test.go
根目录 / internal / boot / model_settings_retry_test.go
1 package boot
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "net/http/httptest"
10 "sync"
11 "sync/atomic"
12 "testing"
13 "time"
14
15 "reasonix/internal/config"
16 "reasonix/internal/event"
17 "reasonix/internal/provider"
18 )
19
20 func TestModelSettingsHTTPFailureKeepsAcceptedCredential(t *testing.T) {
21 isolateConfigHome(t)
22 root := robustTempDir(t)
23 t.Chdir(root)
24 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
25 defer cancel()
26 entered, release := make(chan struct{}), make(chan struct{})
27 var unblock sync.Once
28 t.Cleanup(func() { unblock.Do(func() { close(release) }) })
29 var calls atomic.Int32
30 keys := make(chan string, 8)
31 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32 _, _ = io.Copy(io.Discard, r.Body)
33 keys <- r.Header.Get("Authorization")
34 if calls.Add(1) == 1 {
35 close(entered)
36 select {
37 case <-release:
38 case <-r.Context().Done():
39 return
40 }
41 http.Error(w, "temporarily unavailable", http.StatusServiceUnavailable)
42 return
43 }
44 w.Header().Set("Content-Type", "text/event-stream")
45 fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"content\":\"recovered\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n")
46 }))
47 defer server.Close()
48 if _, err := config.SetCredential("RETRY_SNAPSHOT_KEY", "before-save"); err != nil {
49 t.Fatal(err)
50 }
51 cfg := config.Default()
52 cfg.DefaultModel = "p/m"
53 cfg.Providers = []config.ProviderEntry{{Name: "p", Kind: "openai", BaseURL: server.URL, Model: "m", APIKeyEnv: "RETRY_SNAPSHOT_KEY"}}
54 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
55 t.Fatal(err)
56 }
57 ctrl, err := Build(ctx, Options{WorkspaceRoot: root, Sink: event.Discard})
58 if err != nil {
59 t.Fatal(err)
60 }
61 defer ctrl.Close()
62 done := make(chan error, 1)
63 go func() { done <- ctrl.RunTurn(ctx, "retry this request") }()
64 select {
65 case <-entered:
66 case <-ctx.Done():
67 t.Fatal(ctx.Err())
68 }
69 if _, err := config.SetCredential("RETRY_SNAPSHOT_KEY", "after-save"); err != nil {
70 t.Fatal(err)
71 }
72 unblock.Do(func() { close(release) })
73 select {
74 case err := <-done:
75 var api *provider.APIError
76 if !errors.As(err, &api) || api.Status != http.StatusServiceUnavailable {
77 t.Fatalf("expected original 503 without retry: %v", err)
78 }
79 case <-ctx.Done():
80 t.Fatal(ctx.Err())
81 }
82 if calls.Load() != 1 {
83 t.Fatalf("expected one failed request, got %d", calls.Load())
84 }
85 if key := <-keys; key != "Bearer before-save" {
86 t.Fatal("accepted request crossed credential generations")
87 }
88 next, err := Build(ctx, Options{WorkspaceRoot: root, Sink: event.Discard})
89 if err != nil {
90 t.Fatal(err)
91 }
92 defer next.Close()
93 if err := next.RunTurn(ctx, "next request"); err != nil {
94 t.Fatal(err)
95 }
96 if key := <-keys; key != "Bearer after-save" {
97 t.Fatal("next runtime did not use the saved credential")
98 }
99 if calls.Load() != 2 {
100 t.Fatalf("explicit next request was retried: %d calls", calls.Load())
101 }
102 }
103
103 lines GO