返回 DeepSeek-Reasonix
replay_projection_test.go
根目录 / internal / provider / anthropic / replay_projection_test.go
1 package anthropic
2
3 import (
4 "context"
5 "testing"
6
7 "reasonix/internal/provider"
8 )
9
10 func TestBuildRequestDeepSeekDropsUnreplayableToolActivity(t *testing.T) {
11 c := &client{model: "deepseek-v4-flash", deepseek: true, thinking: "enabled"}
12 r := c.buildRequest(context.Background(), provider.Request{Messages: []provider.Message{
13 {Role: provider.RoleUser, Content: "inspect the change"},
14 {
15 Role: provider.RoleAssistant, Content: "I checked the file.",
16 ToolCalls: []provider.ToolCall{{ID: "read-1", Name: "read_file", Arguments: `{"path":"main.go"}`}},
17 },
18 {Role: provider.RoleTool, ToolCallID: "read-1", Name: "read_file", Content: "package main"},
19 {Role: provider.RoleUser, Content: "continue"},
20 }})
21
22 if len(r.Messages) != 3 {
23 t.Fatalf("messages = %#v, want user/plain assistant/user", r.Messages)
24 }
25 if got := r.Messages[1].Content; len(got) != 1 || got[0].Type != "text" || got[0].Text != "I checked the file." {
26 t.Fatalf("projected assistant = %#v, want visible text only", got)
27 }
28 for _, message := range r.Messages {
29 for _, block := range message.Content {
30 if block.Type == "tool_use" || block.Type == "tool_result" || block.Type == "thinking" {
31 t.Fatalf("unreplayable activity reached DeepSeek wire: %#v", r.Messages)
32 }
33 }
34 }
35 }
36
37 func TestDeepSeekReplayProjectionKeepsHealthyHistoryBacking(t *testing.T) {
38 c := &client{model: "deepseek-v4-flash", deepseek: true, thinking: "enabled"}
39 msgs := []provider.Message{{
40 Role: provider.RoleAssistant, ReasoningContent: "read it first",
41 ToolCalls: []provider.ToolCall{{ID: "read-1", Name: "read_file", Arguments: `{}`}},
42 }}
43 got, changed := provider.ProjectReplaySafeMessages(c, msgs)
44 if changed || len(got) != 1 || &got[0] != &msgs[0] {
45 t.Fatal("healthy replay history allocated or changed")
46 }
47 }
48
49 func TestBuildRequestDeepSeekMergedAssistantTurnKeepsThinkingFirst(t *testing.T) {
50 c := &client{model: "deepseek-v4-flash", deepseek: true, thinking: "enabled"}
51 r := c.buildRequest(context.Background(), provider.Request{Messages: []provider.Message{
52 {Role: provider.RoleUser, Content: "start"},
53 {
54 Role: provider.RoleAssistant, Content: "first answer",
55 ToolCalls: []provider.ToolCall{{ID: "t1", Name: "read_file", Arguments: `{}`}},
56 }, // reasoning lost: projection degrades this turn to plain text
57 {Role: provider.RoleTool, ToolCallID: "t1", Name: "read_file", Content: "data"},
58 {
59 Role: provider.RoleAssistant, Content: "second answer", ReasoningContent: "fresh thinking",
60 ToolCalls: []provider.ToolCall{{ID: "t2", Name: "read_file", Arguments: `{}`}},
61 },
62 {Role: provider.RoleTool, ToolCallID: "t2", Name: "read_file", Content: "more data"},
63 {Role: provider.RoleUser, Content: "continue"},
64 }})
65
66 // The degraded first turn and the healthy second turn merge into one
67 // assistant message; its thinking block must lead.
68 if len(r.Messages) != 3 {
69 t.Fatalf("messages = %#v, want user/merged-assistant/user", r.Messages)
70 }
71 blocks := r.Messages[1].Content
72 if len(blocks) != 4 ||
73 blocks[0].Type != "thinking" || blocks[0].Thinking != "fresh thinking" ||
74 blocks[1].Type != "text" || blocks[1].Text != "first answer" ||
75 blocks[2].Type != "text" || blocks[2].Text != "second answer" ||
76 blocks[3].Type != "tool_use" || blocks[3].ID != "t2" {
77 t.Fatalf("merged assistant blocks = %#v, want [thinking, text, text, tool_use]", blocks)
78 }
79 }
80
81 func TestBuildRequestNativeAnthropicKeepsItsExistingValidationPath(t *testing.T) {
82 c := &client{model: "claude-sonnet", thinking: "adaptive"}
83 r := c.buildRequest(context.Background(), provider.Request{Messages: []provider.Message{{
84 Role: provider.RoleAssistant,
85 ToolCalls: []provider.ToolCall{{ID: "read-1", Name: "read_file", Arguments: `{}`}},
86 }}})
87 if len(r.Messages) != 2 || len(r.Messages[0].Content) != 1 || r.Messages[0].Content[0].Type != "tool_use" ||
88 len(r.Messages[1].Content) != 1 || r.Messages[1].Content[0].Type != "tool_result" {
89 t.Fatalf("native Anthropic history unexpectedly projected: %#v", r.Messages)
90 }
91 }
92
93 func TestBuildRequestDeepSeekReplaysReasoningFromHistory(t *testing.T) {
94 toolTurn := []provider.Message{
95 {Role: provider.RoleUser, Content: "weather?"},
96 {Role: provider.RoleAssistant, ReasoningContent: "I should call the tool.",
97 ToolCalls: []provider.ToolCall{{ID: "t1", Name: "get_weather", Arguments: `{"city":"Paris"}`}}},
98 {Role: provider.RoleTool, ToolCallID: "t1", Content: "sunny"},
99 }
100 for _, tc := range []struct {
101 name string
102 thinking string
103 effort string
104 }{
105 {name: "current request has no tools", thinking: "enabled", effort: "high"},
106 {name: "thinking disabled after tool call", thinking: "enabled", effort: "disabled"},
107 } {
108 t.Run(tc.name, func(t *testing.T) {
109 c := &client{model: "deepseek-v4-flash", deepseek: true, thinking: tc.thinking, effort: tc.effort}
110 r := c.buildRequest(context.Background(), provider.Request{Messages: toolTurn})
111 if len(r.Tools) != 0 {
112 t.Fatalf("current request tools = %+v, want none", r.Tools)
113 }
114 if len(r.Messages) != 3 {
115 t.Fatalf("messages = %+v, want user/assistant/user", r.Messages)
116 }
117 blocks := r.Messages[1].Content
118 if len(blocks) != 2 || blocks[0].Type != "thinking" || blocks[0].Thinking != "I should call the tool." || blocks[1].Type != "tool_use" {
119 t.Fatalf("assistant blocks = %+v, want historical thinking before tool_use", blocks)
120 }
121 })
122 }
123
124 t.Run("reasoning without a tool call is replayed", func(t *testing.T) {
125 // DeepSeek's thinking mode requires every historical assistant turn's
126 // thinking block back when the request declares tools, even for plain
127 // question-answer turns (api-docs.deepseek.com/guides/thinking_mode).
128 c := &client{model: "deepseek-v4-flash", deepseek: true, thinking: "enabled", effort: "high"}
129 r := c.buildRequest(context.Background(), provider.Request{
130 Messages: []provider.Message{
131 {Role: provider.RoleUser, Content: "hello"},
132 {Role: provider.RoleAssistant, Content: "hi", ReasoningContent: "scratchpad"},
133 },
134 Tools: []provider.ToolSchema{{Name: "get_weather"}},
135 })
136 blocks := r.Messages[1].Content
137 if len(r.Messages) != 2 || len(blocks) != 2 || blocks[0].Type != "thinking" || blocks[0].Thinking != "scratchpad" || blocks[1].Type != "text" {
138 t.Fatalf("non-tool assistant blocks = %+v, want thinking before text", r.Messages)
139 }
140 })
141 }
142
142 lines GO