91 lines
4.2 KiB
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)
|
|
}
|
|
}
|