返回 DeepSeek-Reasonix
anthropic_compatibility_e2e_test.go
根目录 / internal / agent / anthropic_compatibility_e2e_test.go
1 package agent
2
3 import (
4 "bytes"
5 "context"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "sync"
10 "testing"
11
12 "reasonix/internal/event"
13 "reasonix/internal/provider"
14 "reasonix/internal/provider/anthropic"
15 )
16
17 func TestCustomAnthropicCompatibilityToolLoop(t *testing.T) {
18 for _, tc := range []struct {
19 name, first string
20 rejectFollowup bool
21 }{
22 {"complete missing thinking", missingReasoningToolSSE, false},
23 {"complete unsigned thinking", recoveredReasoningToolSSE, false},
24 {"server rejects unsigned history", recoveredReasoningToolSSE, true},
25 } {
26 t.Run(tc.name, func(t *testing.T) {
27 var mu sync.Mutex
28 var bodies [][]byte
29 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30 body, err := io.ReadAll(r.Body)
31 if err != nil {
32 t.Error(err)
33 w.WriteHeader(http.StatusBadRequest)
34 return
35 }
36 mu.Lock()
37 bodies = append(bodies, body)
38 n := len(bodies)
39 mu.Unlock()
40 if n == 2 && tc.rejectFollowup {
41 w.WriteHeader(http.StatusBadRequest)
42 _, _ = io.WriteString(w, `{"error":{"message":"The content[].thinking in the thinking mode must be passed back to the API"}}`)
43 return
44 }
45 if n > 3 {
46 t.Errorf("unexpected HTTP attempt %d", n)
47 w.WriteHeader(http.StatusUnauthorized)
48 return
49 }
50 w.Header().Set("Content-Type", "text/event-stream")
51 response := finalAnswerSSE
52 if n == 1 {
53 response = tc.first
54 }
55 _, _ = io.WriteString(w, response)
56 }))
57 defer srv.Close()
58 p, err := anthropic.New(provider.Config{Name: "custom-anthropic", BaseURL: srv.URL, Model: "deepseek-v4-flash", APIKey: "fake-key", Extra: map[string]any{"thinking": "adaptive"}})
59 if err != nil {
60 t.Fatal(err)
61 }
62 sink := &recordSink{}
63 a := New(p, echoRegistry(), NewSession(""), Options{MissingReasoningWarnStateDir: t.TempDir()}, sink)
64 if err := a.Run(withNoClosedLoop(context.Background()), "go"); err != nil {
65 t.Fatal(err)
66 }
67 mu.Lock()
68 defer mu.Unlock()
69 expected := 2
70 if tc.rejectFollowup {
71 expected = 3
72 }
73 if len(bodies) != expected || len(sink.kinds(event.ToolResult)) != 1 {
74 t.Fatalf("HTTP=%d tools=%d", len(bodies), len(sink.kinds(event.ToolResult)))
75 }
76 if !bytes.Contains(bodies[1], []byte(`"type":"tool_result"`)) {
77 t.Fatalf("follow-up omitted completed tool: %s", bodies[1])
78 }
79 if tc.first == recoveredReasoningToolSSE && !bytes.Contains(bodies[1], []byte(`"thinking":"call echo safely"`)) {
80 t.Fatalf("lost unsigned thinking: %s", bodies[1])
81 }
82 if bytes.Contains(bodies[1], []byte(`"signature"`)) {
83 t.Fatal("fabricated a signature")
84 }
85 if tc.rejectFollowup {
86 if !bytes.Contains(bodies[2], []byte("completed_tools")) || bytes.Contains(bodies[2], []byte(`"type":"tool_use"`)) {
87 t.Fatalf("invalid recovery view: %s", bodies[2])
88 }
89 } else if len(sink.kinds(event.Retrying)) != 0 {
90 t.Fatal("compatible turn regenerated")
91 }
92 var originals int
93 for _, m := range a.Session().Snapshot() {
94 if m.Role == provider.RoleAssistant && m.ReasoningContent == "call echo safely" {
95 originals++
96 }
97 }
98 if tc.first == recoveredReasoningToolSSE && originals != 1 {
99 t.Fatal("canonical thinking not retained")
100 }
101 })
102 }
103 }
104
105 func TestNativeTextConversionDoesNotClaimMissingReasoningIncident(t *testing.T) {
106 p, err := anthropic.New(provider.Config{Name: "native", Model: "claude-sonnet-4-6", APIKey: "fake-key", Extra: map[string]any{"thinking": "adaptive"}})
107 if err != nil {
108 t.Fatal(err)
109 }
110 a := New(p, echoRegistry(), NewSession(""), Options{MissingReasoningWarnStateDir: t.TempDir()}, event.Discard)
111 m := provider.Message{Role: provider.RoleAssistant, ReasoningContent: "complete unsigned text", ReasoningState: provider.ReasoningComplete}
112 if missing, retry := a.observeMissingAssistantReasoning(m, true); missing || retry || a.sess.missingReasoning.active {
113 t.Fatal("compatible text consumed strict recovery budget")
114 }
115 }
116
116 lines GO