返回 DeepSeek-Reasonix
reasoning_replay_projection_test.go
根目录 / internal / provider / reasoning_replay_projection_test.go
1 package provider
2
3 import (
4 "context"
5 "encoding/json"
6 "testing"
7 )
8
9 type replayProjectionProvider struct {
10 allowEmpty bool
11 }
12
13 func (replayProjectionProvider) Name() string { return "replay-projection-test" }
14
15 func (replayProjectionProvider) Stream(context.Context, Request) (<-chan Chunk, error) {
16 panic("unexpected Stream call")
17 }
18
19 func (replayProjectionProvider) RequiresAssistantReasoningReplay(m Message) bool {
20 return len(m.ToolCalls) > 0 || len(m.ServerSearch) > 0
21 }
22
23 func (p replayProjectionProvider) AllowsEmptyReasoningFallback() bool { return p.allowEmpty }
24
25 func TestProjectReplaySafeMessagesClearsProviderActivityMetadata(t *testing.T) {
26 p := replayProjectionProvider{}
27 msgs := []Message{
28 {Role: RoleUser, Content: "inspect"},
29 {
30 Role: RoleAssistant,
31 Content: "visible answer",
32 ReasoningSignature: "signature",
33 ReasoningID: "reasoning-id",
34 ReasoningStatus: "completed",
35 ToolCalls: []ToolCall{{ID: "call-1", Name: "read_file"}},
36 ResponsesItems: []json.RawMessage{json.RawMessage(`{"type":"reasoning"}`)},
37 ServerSearch: []ServerSearchCall{{ID: "search-1", Query: "query"}},
38 },
39 {Role: RoleTool, ToolCallID: "call-1", Name: "read_file", Content: "result"},
40 {Role: RoleUser, Content: "continue"},
41 }
42
43 got, changed := ProjectReplaySafeMessages(p, msgs)
44 if !changed {
45 t.Fatal("malformed replay history was not projected")
46 }
47 if len(got) != 3 || got[1].Role != RoleAssistant || got[1].Content != "visible answer" {
48 t.Fatalf("projection = %#v, want user/plain assistant/user", got)
49 }
50 plain := got[1]
51 if plain.ReasoningContent != "" || plain.ReasoningSignature != "" || plain.ReasoningID != "" || plain.ReasoningStatus != "" ||
52 len(plain.ToolCalls) != 0 || len(plain.ResponsesItems) != 0 || len(plain.ServerSearch) != 0 {
53 t.Fatalf("provider activity metadata survived projection: %#v", plain)
54 }
55 if len(msgs[1].ToolCalls) != 1 || len(msgs[1].ResponsesItems) != 1 || len(msgs[1].ServerSearch) != 1 {
56 t.Fatal("projection mutated canonical history")
57 }
58 }
59
60 func TestProjectReplaySafeMessagesKeepsStableBacking(t *testing.T) {
61 healthy := []Message{{
62 Role: RoleAssistant, ReasoningContent: "reasoning",
63 ToolCalls: []ToolCall{{ID: "call-1", Name: "read_file"}},
64 }}
65 if got, changed := ProjectReplaySafeMessages(replayProjectionProvider{}, healthy); changed || &got[0] != &healthy[0] {
66 t.Fatal("healthy history must retain its backing slice")
67 }
68
69 emptyFallback := []Message{{
70 Role: RoleAssistant,
71 ToolCalls: []ToolCall{{ID: "call-1", Name: "read_file"}},
72 }}
73 if got, changed := ProjectReplaySafeMessages(replayProjectionProvider{allowEmpty: true}, emptyFallback); changed || &got[0] != &emptyFallback[0] {
74 t.Fatal("empty-reasoning fallback history must retain its backing slice")
75 }
76 }
77
78 func TestProjectReasoningStrippedMessagesBypassesEmptyFallbackAfter400(t *testing.T) {
79 p := replayProjectionProvider{allowEmpty: true}
80 msgs := []Message{
81 {Role: RoleUser, Content: "inspect"},
82 {
83 Role: RoleAssistant,
84 Content: "visible answer",
85 ReasoningContent: "stale reasoning",
86 ToolCalls: []ToolCall{{ID: "call-1", Name: "read_file"}},
87 ReasoningSignature: "stale signature",
88 },
89 {Role: RoleTool, ToolCallID: "call-1", Name: "read_file", Content: "result"},
90 {Role: RoleUser, Content: "continue"},
91 }
92
93 got, changed := ProjectReasoningStrippedMessages(p, msgs)
94 if !changed {
95 t.Fatal("stale reasoning history was not changed")
96 }
97 if len(got) != 3 || got[1].Role != RoleAssistant || got[1].Content != "visible answer" {
98 t.Fatalf("projection = %#v, want user/plain assistant/user", got)
99 }
100 if got[1].ReasoningContent != "" || got[1].ReasoningSignature != "" || len(got[1].ToolCalls) != 0 {
101 t.Fatalf("stale assistant metadata survived projection: %#v", got[1])
102 }
103 if msgs[1].ReasoningContent != "stale reasoning" || len(msgs[1].ToolCalls) != 1 || len(msgs[2].Content) == 0 {
104 t.Fatal("strong projection mutated canonical history")
105 }
106 }
107
108 func TestProjectReasoningStrippedMessagesPrefixKeepsAppendedToolRound(t *testing.T) {
109 p := replayProjectionProvider{}
110 msgs := []Message{
111 {Role: RoleUser, Content: "inspect"},
112 {
113 Role: RoleAssistant,
114 Content: "old answer",
115 ReasoningContent: "stale reasoning",
116 },
117 {Role: RoleUser, Content: "continue"},
118 {
119 Role: RoleAssistant,
120 ReasoningContent: "fresh reasoning",
121 ToolCalls: []ToolCall{{ID: "fresh", Name: "read_file"}},
122 },
123 {Role: RoleTool, ToolCallID: "fresh", Name: "read_file", Content: "fresh result"},
124 }
125
126 got, changed := ProjectReasoningStrippedMessagesPrefix(p, msgs, 3)
127 if !changed {
128 t.Fatal("stale prefix was not projected")
129 }
130 if len(got) != len(msgs) {
131 t.Fatalf("projection length = %d, want %d", len(got), len(msgs))
132 }
133 if got[1].ReasoningContent != "" {
134 t.Fatalf("stale prefix reasoning survived: %#v", got[1])
135 }
136 if got[3].ReasoningContent != "fresh reasoning" || len(got[3].ToolCalls) != 1 || got[4].Role != RoleTool {
137 t.Fatalf("appended tool round changed: %#v", got)
138 }
139 }
140
140 lines GO