1package core
2
3import (
4 "context"
5 "errors"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/bnema/zut/packages/provider"
11)
12
13type identityRecordingClient struct {
14 mu sync.Mutex
15 requests []provider.Request
16 failOnce bool
17}
18
19func (c *identityRecordingClient) Name() string { return "identity-recording" }
20
21func (c *identityRecordingClient) Stream(_ context.Context, req provider.Request) (<-chan provider.Event, error) {
22 c.mu.Lock()
23 c.requests = append(c.requests, req)
24 fail := c.failOnce
25 c.failOnce = false
26 c.mu.Unlock()
27
28 out := make(chan provider.Event, 2)
29 out <- provider.EventStart{Provider: c.Name(), Model: req.Model}
30 if fail {
31 out <- provider.EventDone{Stop: provider.StopError, Err: errors.New("anthropic overloaded_error: overloaded")}
32 } else {
33 out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
34 Role: provider.RoleAssistant,
35 Content: []provider.Content{
36 provider.TextBlock{Text: "done"},
37 },
38 }}
39 }
40 close(out)
41 return out, nil
42}
43
44func (c *identityRecordingClient) Requests() []provider.Request {
45 c.mu.Lock()
46 defer c.mu.Unlock()
47 requests := make([]provider.Request, len(c.requests))
48 copy(requests, c.requests)
49 return requests
50}
51
52func TestAgentRequestContextSeparatesCacheThreadAndTurnIdentities(t *testing.T) {
53 client := &identityRecordingClient{failOnce: true}
54 agent := NewAgent(client, "test-model", "system", Registry{})
55 agent.MaxRetries = 1
56 agent.RetryBaseDelay = time.Millisecond
57 if err := agent.BindRequestIdentity("cache-root-1", "thread-1"); err != nil {
58 t.Fatalf("BindRequestIdentity: %v", err)
59 }
60
61 if err := agent.Prompt(context.Background(), "first", nil, nil); err != nil {
62 t.Fatalf("first prompt: %v", err)
63 }
64 if err := agent.Prompt(context.Background(), "second", nil, nil); err != nil {
65 t.Fatalf("second prompt: %v", err)
66 }
67
68 requests := client.Requests()
69 if len(requests) != 3 {
70 t.Fatalf("request count = %d, want 3", len(requests))
71 }
72 for i, request := range requests {
73 if request.Context.CacheSessionID != "cache-root-1" || request.Context.ThreadID != "thread-1" {
74 t.Fatalf("request %d identity = %+v, want cache-root-1/thread-1", i, request.Context)
75 }
76 if request.Context.TurnID == "" {
77 t.Fatalf("request %d has empty turn ID", i)
78 }
79 }
80 if requests[0].Context.TurnID != requests[1].Context.TurnID {
81 t.Fatalf("retry turn IDs = %q and %q, want one stable ID", requests[0].Context.TurnID, requests[1].Context.TurnID)
82 }
83 if requests[1].Context.TurnID == requests[2].Context.TurnID {
84 t.Fatalf("prompt turn IDs = %q and %q, want distinct IDs", requests[1].Context.TurnID, requests[2].Context.TurnID)
85 }
86 if err := agent.BindRequestIdentity("other-cache-root", "other-thread"); err == nil {
87 t.Fatal("BindRequestIdentity succeeded after the first provider request")
88 }
89}
90
91func TestAgentNoSessionIdentityIsStableForRun(t *testing.T) {
92 client := &identityRecordingClient{}
93 agent := NewAgent(client, "test-model", "system", Registry{})
94
95 if err := agent.Prompt(context.Background(), "first", nil, nil); err != nil {
96 t.Fatalf("first prompt: %v", err)
97 }
98 if err := agent.Prompt(context.Background(), "second", nil, nil); err != nil {
99 t.Fatalf("second prompt: %v", err)
100 }
101 requests := client.Requests()
102 if len(requests) != 2 {
103 t.Fatalf("request count = %d, want 2", len(requests))
104 }
105 if requests[0].Context.CacheSessionID == "" || requests[0].Context.CacheSessionID != requests[1].Context.CacheSessionID || requests[0].Context.ThreadID != requests[0].Context.CacheSessionID || requests[1].Context.ThreadID != requests[1].Context.CacheSessionID {
106 t.Fatalf("request identities = %+v and %+v, want stable shared fallback identity", requests[0].Context, requests[1].Context)
107 }
108}