返回 DeepSeek-Reasonix
recovery_test.go
根目录 / internal / provider / recovery_test.go
1 package provider
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12 "time"
13 )
14
15 func TestManagedRecoveryOneHTTPRequestAndServerDelay(t *testing.T) {
16 calls := 0
17 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 calls++
19 w.Header().Set("Retry-After", "120")
20 w.WriteHeader(http.StatusTooManyRequests)
21 _, _ = w.Write([]byte(`{"error":{"code":"rate_limit"}}`))
22 }))
23 defer server.Close()
24 ctx := WithManagedRecovery(context.Background())
25 _, err := SendWithRetry(ctx, server.Client(), SendOptions{}, func(ctx context.Context) (*http.Request, error) {
26 return http.NewRequestWithContext(ctx, http.MethodPost, server.URL, nil)
27 })
28 f := ClassifyRecovery(err)
29 if calls != 1 || !f.Retryable || f.RetryAfter != 2*time.Minute || f.Code != "rate_limit" {
30 t.Fatalf("calls=%d failure=%+v err=%v", calls, f, err)
31 }
32 }
33 func TestRecoveryDoesNotRetryPermanentOrUnknownErrors(t *testing.T) {
34 for _, err := range []error{context.Canceled, context.DeadlineExceeded, errors.New("arbitrary failure"), &APIError{Status: 429, Body: `{"error":{"code":"insufficient_quota"}}`}, &APIError{Status: 503, ShouldRetry: "false"}, &AuthError{Status: 401}} {
35 if ClassifyRecovery(err).Retryable {
36 t.Fatalf("retried %v", err)
37 }
38 }
39 }
40
41 func TestRecoveryWaitExhaustedIsTerminalAndDiagnosable(t *testing.T) {
42 cause := &APIError{Provider: "p", Status: 503, Body: `{"error":{"code":"overloaded"}}`, TraceID: "trace_1"}
43 err := fmt.Errorf("run: %w", &RecoveryWaitExhaustedError{Phase: "headers", Code: "overloaded", Status: 503, Waited: 10 * time.Minute, Attempts: 13, Cause: cause})
44 if f := ClassifyRecovery(err); f.Retryable || f.Phase != "headers" || f.Status != 503 || f.Code != "overloaded" {
45 t.Fatalf("failure=%+v", f)
46 }
47 if d := DiagnoseFailure(err); d.Kind != "recovery_wait_exhausted" || d.Status != 503 || d.TraceID != "trace_1" {
48 t.Fatalf("diagnostic=%+v", d)
49 }
50 if !errors.Is(err, cause) || !strings.Contains(err.Error(), "provider unreachable for 10m0s (headers): p: status 503") {
51 t.Fatalf("err=%v", err)
52 }
53 if AsRecoveryWaitExhausted(cause) != nil || AsRecoveryWaitExhausted(nil) != nil {
54 t.Fatal("plain failures must not read as an exhausted wait")
55 }
56 }
57
58 func TestOpaqueGoBadRequestDoesNotGuessReplayFailure(t *testing.T) {
59 // Observed from Go's custom DeepSeek Anthropic route after an invalid
60 // replay. The same opaque body cannot establish the cause for real users.
61 err := &APIError{Provider: "opencode-go", Status: 400, Body: `{"model":"deepseek-v4-flash"}`}
62 if AsReasoningReplayError(err) != nil || ClassifyRecovery(err).Retryable {
63 t.Fatal("opaque 400 must not trigger guessed history repair or automatic regeneration")
64 }
65 }
66 func TestWriteEvidenceNeverEntersModelMessages(t *testing.T) {
67 original := []Message{{Role: RoleAssistant, ToolCalls: []ToolCall{{ID: "x", Name: "write_file", Arguments: `{}`, WriteIntents: []json.RawMessage{json.RawMessage(`{"version":99,"future":"retain"}`)}}}}}
68 projected := ModelMessages(original)
69 if len(projected[0].ToolCalls[0].WriteIntents) != 0 || len(original[0].ToolCalls[0].WriteIntents) != 1 {
70 t.Fatal("projection leaked or mutated evidence")
71 }
72 b, err := json.Marshal(original)
73 if err != nil {
74 t.Fatal(err)
75 }
76 var decoded []Message
77 if err := json.Unmarshal(b, &decoded); err != nil {
78 t.Fatal(err)
79 }
80 if string(decoded[0].ToolCalls[0].WriteIntents[0]) != string(original[0].ToolCalls[0].WriteIntents[0]) {
81 t.Fatal("unknown version lost")
82 }
83 }
84
84 lines GO