返回 DeepSeek-Reasonix
live_manual_protocol_recovery_test.go
根目录 / internal / agent / live_manual_protocol_recovery_test.go
1 //go:build live
2
3 package agent
4
5 import (
6 "context"
7 "net/http/httptest"
8 "os"
9 "strings"
10 "sync/atomic"
11 "testing"
12 "time"
13
14 "reasonix/internal/event"
15 "reasonix/internal/provider"
16 "reasonix/internal/tool"
17 )
18
19 // The proxy corrupts only the second outbound replay request. Rejections are
20 // real upstream responses. Explicit recovery is measured separately from
21 // automatic success; it never converts a stopped initial turn to a pass.
22 func TestLiveManualProtocolRecovery(t *testing.T) {
23 cases := multiProviderCases()
24 for _, model := range []string{"deepseek-v4-flash", "deepseek-v4-pro"} {
25 for _, protocol := range []string{"chat", "anthropic", "responses"} {
26 base := "https://api.deepseek.com"
27 if protocol == "anthropic" {
28 base += "/anthropic"
29 }
30 cases = append(cases, multiProviderCase{"deepseek", "DEEPSEEK_API_KEY", model, protocol, base, "enabled", "high", "deepseek"})
31 }
32 }
33 executed := 0
34 for _, tc := range cases {
35 if os.Getenv(tc.keyEnv) == "" || !strings.HasPrefix(tc.model, "deepseek-v4-") || strings.Contains(tc.model, "vision") {
36 continue
37 }
38 executed++
39 t.Run(tc.vendor+"/"+tc.model+"/"+tc.protocol, func(t *testing.T) {
40 proxy := &officialRecoveryProxy{protocol: tc.protocol, scenario: "server_replay_rejection", upstreamURL: tc.upstream()}
41 srv := httptest.NewServer(proxy)
42 defer srv.Close()
43 p := tc.new(t, srv.URL, "server_replay_rejection")
44 var executions atomic.Int32
45 reg := tool.NewRegistry()
46 reg.Add(liveRecoveryEchoTool{executions: &executions})
47 sink := &recordSink{}
48 session := NewSession("Call echo once, report only its real marker. Preserve completed work; never repeat completed tool calls when continuing after an error.")
49 a := New(p, reg, session, Options{MaxSteps: 4, MaxOutputTokens: 4096, MissingReasoningWarnStateDir: t.TempDir()}, sink)
50 ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
51 defer cancel()
52 first := a.Run(ctx, "Call echo once and report the result.")
53 explicit := false
54 var recovered error
55 if pending := a.PendingProtocolRecovery(); pending != nil {
56 explicit = true
57 recovered = a.Run(WithInputMessageOrigin(WithProtocolRecovery(ctx, pending.ID), provider.MessageOriginHost), "Continue from valid history; report the completed echo result without repeating it.")
58 if a.PendingProtocolRecovery() != nil {
59 t.Fatal("repair budget renewed")
60 }
61 }
62 proxy.mu.Lock()
63 statuses := append([]int(nil), proxy.statuses...)
64 proxy.mu.Unlock()
65 requests, prompt, output := 0, 0, 0
66 for _, e := range sink.kinds(event.Usage) {
67 if e.Usage != nil {
68 requests += e.Usage.RequestCount
69 prompt += e.Usage.PromptTokens
70 output += e.Usage.CompletionTokens
71 }
72 }
73 t.Logf("initial_success=%t explicit_action=%t recovery_success=%t tools=%d requests=%d prompt=%d output=%d statuses=%v", first == nil, explicit, explicit && recovered == nil, executions.Load(), requests, prompt, output, statuses)
74 if first != nil && !explicit {
75 t.Fatalf("initial error without eligible recovery: %v", first)
76 }
77 if recovered != nil {
78 t.Fatal(recovered)
79 }
80 if executions.Load() != 1 {
81 t.Fatal("completed tool was repeated or never executed")
82 }
83 })
84 }
85 if executed == 0 {
86 t.Skip("no eligible credential or model")
87 }
88 }
89
89 lines GO