返回 DeepSeek-Reasonix
realdeepseek_test.go
根目录 / internal / agent / realdeepseek_test.go
1 //go:build live
2
3 package agent_test
4
5 import (
6 "context"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "sync/atomic"
13 "testing"
14 "time"
15
16 "reasonix/internal/agent"
17 "reasonix/internal/event"
18 "reasonix/internal/provider"
19 "reasonix/internal/provider/anthropic"
20 "reasonix/internal/tool"
21 )
22
23 func TestRealDeepSeekAgentInterruptedToolResume(t *testing.T) {
24 key := os.Getenv("DEEPSEEK_API_KEY")
25 if key == "" {
26 t.Skip("DEEPSEEK_API_KEY not set — skipping live probe")
27 }
28 p := newLiveDeepSeekAgentProvider(t, key, false)
29 marker := &liveMarkerTool{}
30 reg := tool.NewRegistry()
31 reg.Add(marker)
32 sess := agent.NewSession("You are a concise tool-using assistant. Call the requested tool before answering.")
33
34 ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
35 defer cancel()
36 sink := event.FuncSink(func(e event.Event) {
37 if e.Kind == event.ToolResult && e.Tool.Name == marker.Name() {
38 cancel()
39 }
40 })
41 a := agent.New(p, reg, sess, agent.Options{MaxSteps: 4, MaxOutputTokens: 512}, sink)
42 err := a.Run(ctx, "Call get_marker and report its result.")
43 if !errors.Is(err, context.Canceled) {
44 t.Fatalf("interrupted run error = %v, want context cancellation", err)
45 }
46 if marker.executions.Load() != 1 {
47 t.Fatalf("tool executions before restart = %d, want 1", marker.executions.Load())
48 }
49
50 path := filepath.Join(t.TempDir(), "session.jsonl")
51 if err := sess.Save(path); err != nil {
52 t.Fatalf("Save: %v", err)
53 }
54 reloaded, err := agent.LoadSession(path)
55 if err != nil {
56 t.Fatalf("LoadSession: %v", err)
57 }
58 reopened := agent.New(p, reg, reloaded, agent.Options{MaxSteps: 4, MaxOutputTokens: 512}, event.Discard)
59 resumeCtx, resumeCancel := context.WithTimeout(context.Background(), 90*time.Second)
60 defer resumeCancel()
61 if err := reopened.Run(resumeCtx, "Continue from the completed tool result without calling the tool again. Reply with the marker."); err != nil {
62 t.Fatalf("resume after restart: %v", err)
63 }
64 if marker.executions.Load() != 1 {
65 t.Fatalf("old tool was executed again after restart: executions=%d", marker.executions.Load())
66 }
67 t.Logf("interrupted tool turn resumed after save/load with executions=%d messages=%d", marker.executions.Load(), len(reloaded.Snapshot()))
68 }
69
70 func TestRealDeepSeekAgentWebSearchContinuation(t *testing.T) {
71 key := os.Getenv("DEEPSEEK_API_KEY")
72 if key == "" {
73 t.Skip("DEEPSEEK_API_KEY not set — skipping live probe")
74 }
75 p := newLiveDeepSeekAgentProvider(t, key, true)
76 sess := agent.NewSession("You are a concise assistant. Use server-side web search when explicitly requested.")
77 a := agent.New(p, tool.NewRegistry(), sess, agent.Options{MaxSteps: 3, MaxOutputTokens: 512}, event.Discard)
78 ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
79 defer cancel()
80 if err := a.Run(ctx, "Search the web for the latest DeepSeek API documentation update and reply with one source URL."); err != nil {
81 t.Fatalf("web search turn: %v", err)
82 }
83 var found bool
84 for _, m := range sess.Snapshot() {
85 if m.Role == provider.RoleAssistant && len(m.ServerSearch) > 0 {
86 found = true
87 if strings.TrimSpace(m.ReasoningContent) == "" {
88 t.Fatal("stored web-search turn lost provider reasoning")
89 }
90 }
91 }
92 if !found {
93 t.Fatal("agent session contains no server-search assistant turn")
94 }
95 if err := a.Run(ctx, "Without searching again, reply with the hostname of that source."); err != nil {
96 t.Fatalf("web search continuation: %v", err)
97 }
98 t.Logf("agent web-search continuation completed with messages=%d", len(sess.Snapshot()))
99 }
100
101 func newLiveDeepSeekAgentProvider(t *testing.T, key string, webSearch bool) provider.Provider {
102 t.Helper()
103 p, err := anthropic.New(provider.Config{
104 Name: "deepseek-anthropic", BaseURL: "https://api.deepseek.com/anthropic", Model: "deepseek-v4-flash", APIKey: key,
105 Extra: map[string]any{"api_key_env": "DEEPSEEK_API_KEY", "thinking": "enabled", "effort": "high", "web_search": webSearch},
106 })
107 if err != nil {
108 t.Fatalf("New: %v", err)
109 }
110 if closer, ok := p.(interface{ CloseIdleConnections() }); ok {
111 t.Cleanup(closer.CloseIdleConnections)
112 }
113 return p
114 }
115
116 type liveMarkerTool struct{ executions atomic.Int32 }
117
118 func (*liveMarkerTool) Name() string { return "get_marker" }
119 func (*liveMarkerTool) Description() string { return "Return a fixed integration-test marker." }
120 func (*liveMarkerTool) Schema() json.RawMessage {
121 return json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`)
122 }
123 func (m *liveMarkerTool) Execute(context.Context, json.RawMessage) (string, error) {
124 m.executions.Add(1)
125 return "protocol-round-trip-ok", nil
126 }
127 func (*liveMarkerTool) ReadOnly() bool { return true }
128
128 lines GO