ituoke/internal/agent/reasoning_test.go

91 lines
4.2 KiB
Go

package agent
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestReasoningStreamSeparatesThinkingFromAnswer(t *testing.T) {
for _, field := range []string{"reasoning_content", "reasoning"} {
t.Run(field, func(t *testing.T) {
stream := fmt.Sprintf("data: {\"choices\":[{\"delta\":{%q:\"先确认设备。\"}}]}\n\ndata: {\"choices\":[{\"delta\":{%q:\"再检查页面。\",\"content\":\"设备在线。\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n", field, field)
var answer, thought strings.Builder
step, err := readToolStream(strings.NewReader(stream), "", func(delta string) { answer.WriteString(delta) }, func(delta string) { thought.WriteString(delta) })
if err != nil || step.Text != "设备在线。" || answer.String() != step.Text || step.Reasoning != "先确认设备。再检查页面。" || thought.String() != step.Reasoning {
t.Fatalf("reasoning/answer mixed or lost: %+v %q %q %v", step, answer.String(), thought.String(), err)
}
})
}
}
func TestReasoningInterruptedStreamRetainsReceivedThought(t *testing.T) {
stream := "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"尚未完成的思考\"}}]}\n\n"
var received strings.Builder
step, err := readToolStream(strings.NewReader(stream), "", nil, func(delta string) { received.WriteString(delta) })
if err == nil || step.Reasoning != "尚未完成的思考" || received.String() != step.Reasoning || step.Text != "" {
t.Fatalf("partial reasoning lost: %+v %v", step, err)
}
}
func TestReasoningEventResultAndConversationRoundTrip(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
fmt.Fprint(w, "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"先核对预算。\"}}]}\n\ndata: {\"choices\":[{\"delta\":{\"content\":\"预算3500元。\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n")
}))
defer server.Close()
s, secrets := testService(t)
if _, err := s.SaveModelConfig(ModelConfigInput{Config: testConfig(server.URL), APIKey: "key"}); err != nil {
t.Fatal(err)
}
session, err := s.CreateConversation()
if err != nil {
t.Fatal(err)
}
var thought, answer strings.Builder
s.emitDelta = func(delta ChatDelta) {
if delta.RequestID != "reasoning-request" {
t.Errorf("wrong request ID: %s", delta.RequestID)
}
thought.WriteString(delta.ReasoningDelta)
answer.WriteString(delta.Delta)
}
result, err := s.ChatStream(context.Background(), "reasoning-request", session.ID, "预算多少?")
if err != nil || result["reasoning"] != "先核对预算。" || thought.String() != result["reasoning"] || answer.String() != result["content"] {
t.Fatalf("reasoning not delivered: %v %v", result, err)
}
session.Messages = []ConversationMessage{{ID: "u1", Role: "user", Content: "预算多少?", CreatedAt: 1}, {ID: "a1", Role: "assistant", Content: result["content"], Reasoning: result["reasoning"], CreatedAt: 2, Status: "complete"}}
if _, err = s.SaveConversation(session); err != nil {
t.Fatal(err)
}
loaded, err := newService(s.configPath, secrets).GetConversation(session.ID)
if err != nil || loaded.Messages[1].Reasoning != result["reasoning"] {
t.Fatalf("reasoning lost on reopen: %+v %v", loaded, err)
}
raw, _ := json.Marshal(s.history[session.ID])
if strings.Contains(string(raw), "先核对预算") {
t.Fatal("display reasoning leaked into legacy history")
}
}
func TestNonStreamReasoningFromModelAndToolStep(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"choices":[{"message":{"content":"回答","reasoning_content":"思考"},"finish_reason":"stop"}]}`)
}))
defer server.Close()
var thought string
text, err := complete(testConfig(server.URL), "key", nil, func(delta string) { thought += delta })
if err != nil || text != "回答" || thought != "思考" {
t.Fatalf("non-stream reasoning missing: %q %q %v", text, thought, err)
}
step, err := modelToolStep(context.Background(), testConfig(server.URL), "key", nil, nil, nil)
if err != nil || step.Reasoning != "思考" || step.Text != "回答" {
t.Fatalf("tool step reasoning missing: %+v %v", step, err)
}
}