返回 DeepSeek-Reasonix
live_goal_boundary_test.go
根目录 / internal / agent / live_goal_boundary_test.go
1 //go:build live
2
3 package agent
4
5 import (
6 "context"
7 "encoding/json"
8 "errors"
9 "os"
10 "strconv"
11 "sync/atomic"
12 "testing"
13 "time"
14
15 "reasonix/internal/event"
16 "reasonix/internal/provider"
17 "reasonix/internal/provider/openai"
18 "reasonix/internal/tool"
19 )
20
21 // TestLiveGoalBoundaryMatrix runs the three acceptance scenarios from the Goal
22 // boundary design against real OpenAI-compatible providers. Credentials are
23 // accepted only through the process environment; response text and keys are
24 // never logged or persisted. Deterministic unit tests cover exact internals.
25 func TestLiveGoalBoundaryMatrix(t *testing.T) {
26 tests := []struct {
27 name, keyEnv, baseURL, model string
28 extra map[string]any
29 }{
30 {name: "deepseek", keyEnv: "DEEPSEEK_API_KEY", baseURL: "https://api.deepseek.com", model: "deepseek-v4-flash", extra: map[string]any{"reasoning_protocol": "deepseek", "thinking": "enabled", "effort": "low"}},
31 {name: "longcat", keyEnv: "LONGCAT_API_KEY", baseURL: "https://api.longcat.chat/openai/v1", model: "LongCat-2.0", extra: map[string]any{"thinking": "enabled", "effort": "enabled"}},
32 {name: "zhipu-coding-plan", keyEnv: "GLM_PLAN_API_KEY", baseURL: "https://open.bigmodel.cn/api/coding/paas/v4", model: "glm-5.2", extra: map[string]any{"reasoning_protocol": "glm", "effort": "disabled"}},
33 }
34
35 for _, tc := range tests {
36 t.Run(tc.name, func(t *testing.T) {
37 key := os.Getenv(tc.keyEnv)
38 if key == "" {
39 t.Skip(tc.keyEnv + " not set")
40 }
41 prov, err := openai.New(provider.Config{Name: tc.name, BaseURL: tc.baseURL, Model: tc.model, APIKey: key, Extra: tc.extra})
42 if err != nil {
43 t.Fatalf("create provider: %v", err)
44 }
45 if closer, ok := prov.(interface{ CloseIdleConnections() }); ok {
46 t.Cleanup(closer.CloseIdleConnections)
47 }
48
49 t.Run("normal-completion", func(t *testing.T) {
50 metrics := &liveGoalMetrics{}
51 a := newLiveGoalAgent(prov, tool.NewRegistry(), metrics)
52 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
53 defer cancel()
54 started := time.Now()
55 if err := a.Run(ctx, "Return one short final sentence now without calling tools."); err != nil {
56 t.Fatalf("normal completion: %v", err)
57 }
58 if metrics.requests.Load() < 1 {
59 t.Fatal("normal completion emitted no provider request usage")
60 }
61 logLiveGoalMetrics(t, tc.model, "normal-completion", 1, metrics, "complete", time.Since(started))
62 })
63
64 t.Run("deterministic-stuck", func(t *testing.T) {
65 const maxConformanceAttempts = 2
66 for attempt := 1; attempt <= maxConformanceAttempts; attempt++ {
67 metrics := &liveGoalMetrics{}
68 var executions atomic.Int32
69 reg := tool.NewRegistry()
70 reg.Add(liveGoalFailureTool{executions: &executions})
71 a := newLiveGoalAgent(prov, reg, metrics)
72 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
73 ctx = WithDeliveryExecutionScope(ctx, DeliveryExecutionScope{ID: "live-goal-stuck", TaskText: "validate deterministic Goal stuck detection"})
74 started := time.Now()
75 err := a.Run(ctx, "This is a deterministic host-failure conformance probe requiring three failed tool rounds. Call live_goal_failure exactly once now. After each of the first two failures, call it exactly once again in the next assistant response. After the third failure, return a short summary. Do not switch tools, change arguments, or batch calls.")
76 cancel()
77 if err != nil {
78 t.Fatalf("stuck guard paused Goal: attempt=%d err=%v executions=%d requests=%d", attempt, err, executions.Load(), metrics.requests.Load())
79 }
80 if executions.Load() == 3 {
81 logLiveGoalMetrics(t, tc.model, "deterministic-stuck", 3, metrics, "redirect->complete", time.Since(started))
82 return
83 }
84 t.Logf("provider conformance deviation: attempt=%d failure_executions=%d want=3", attempt, executions.Load())
85 }
86 t.Fatalf("provider failed three-round stuck conformance after %d attempts", maxConformanceAttempts)
87 })
88
89 t.Run("beyond-sixteen-rounds", func(t *testing.T) {
90 metrics := &liveGoalMetrics{}
91 var executions atomic.Int32
92 reg := tool.NewRegistry()
93 reg.Add(liveGoalMarkerTool{executions: &executions})
94 a := newLiveGoalAgent(prov, reg, metrics)
95 ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
96 defer cancel()
97 ctx = WithDeliveryExecutionScope(ctx, DeliveryExecutionScope{ID: "live-goal-continuous", TaskText: "validate continuous Goal execution"})
98 started := time.Now()
99 err := a.Run(ctx, "This is a 17-round tool conformance probe. Call live_goal_marker exactly once with index 1 now. After every accepted result, call it exactly once with the next integer index in your next assistant response. Never batch calls. After index 17 is accepted, return one short final sentence.")
100 if err != nil {
101 t.Fatalf("continuous Goal result: err=%v executions=%d requests=%d", err, executions.Load(), metrics.requests.Load())
102 }
103 if executions.Load() < 17 {
104 t.Fatalf("tool executions = %d, want at least 17 continuous rounds", executions.Load())
105 }
106 if metrics.requests.Load() < 18 {
107 t.Fatalf("provider requests = %d, want at least 17 work rounds and a final", metrics.requests.Load())
108 }
109 logLiveGoalMetrics(t, tc.model, "beyond-sixteen-rounds", 17, metrics, "complete", time.Since(started))
110 })
111 })
112 }
113 }
114
115 type liveGoalMetrics struct {
116 requests atomic.Int32
117 tokens atomic.Int64
118 }
119
120 func newLiveGoalAgent(prov provider.Provider, reg *tool.Registry, metrics *liveGoalMetrics) *Agent {
121 sink := event.FuncSink(func(e event.Event) {
122 if e.Kind != event.Usage || e.Usage == nil {
123 return
124 }
125 if e.Usage.RequestCount > 0 {
126 metrics.requests.Add(int32(e.Usage.RequestCount))
127 }
128 tokens := e.Usage.TotalTokens
129 if tokens <= 0 {
130 tokens = e.Usage.PromptTokens + e.Usage.CompletionTokens
131 }
132 if tokens > 0 {
133 metrics.tokens.Add(int64(tokens))
134 }
135 })
136 return New(prov, reg, NewSession("You are a tool-call conformance test agent. Tool calls requested by the user are mandatory. Never substitute prose for a requested tool call. Call at most one tool per assistant response. Follow host finalization instructions exactly."), Options{Temperature: 0}, sink)
137 }
138
139 func logLiveGoalMetrics(t *testing.T, model, scenario string, rounds int, metrics *liveGoalMetrics, exit string, elapsed time.Duration) {
140 t.Helper()
141 t.Logf("model=%s scenario=%s rounds=%d requests=%d tokens=%d exit=%s latency=%s", model, scenario, rounds, metrics.requests.Load(), metrics.tokens.Load(), exit, elapsed.Round(time.Millisecond))
142 }
143
144 type liveGoalMarkerTool struct{ executions *atomic.Int32 }
145
146 func (t liveGoalMarkerTool) Name() string { return "live_goal_marker" }
147 func (t liveGoalMarkerTool) Description() string {
148 return "Accept one numbered live-test round. Call exactly once per assistant response, using the next integer index requested by the prior result."
149 }
150 func (t liveGoalMarkerTool) Schema() json.RawMessage {
151 return json.RawMessage(`{"type":"object","properties":{"index":{"type":"integer"}},"required":["index"],"additionalProperties":false}`)
152 }
153 func (t liveGoalMarkerTool) ReadOnly() bool { return true }
154 func (t liveGoalMarkerTool) Execute(context.Context, json.RawMessage) (string, error) {
155 n := t.executions.Add(1)
156 if n < 17 {
157 return "accepted live round " + strconv.Itoa(int(n)) + "; in the next assistant response call live_goal_marker exactly once with index " + strconv.Itoa(int(n+1)) + "; do not write prose", nil
158 }
159 return "accepted live round 17; return one short final sentence now and do not call more tools", nil
160 }
161
162 type liveGoalFailureTool struct{ executions *atomic.Int32 }
163
164 func (t liveGoalFailureTool) Name() string { return "live_goal_failure" }
165 func (t liveGoalFailureTool) Description() string {
166 return "Always return the same deterministic host failure. Call exactly once per assistant response when the user requests the failure probe."
167 }
168 func (t liveGoalFailureTool) Schema() json.RawMessage {
169 return json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`)
170 }
171 func (t liveGoalFailureTool) ReadOnly() bool { return true }
172 func (t liveGoalFailureTool) Execute(context.Context, json.RawMessage) (string, error) {
173 n := t.executions.Add(1)
174 if n < 3 {
175 return "", errors.New("deterministic live host failure; call live_goal_failure exactly once again in the next assistant response and do not write prose")
176 }
177 return "", errors.New("deterministic live host failure number three reached; return one short summary now and do not call more tools")
178 }
179
179 lines GO