v0.2.89 eddf12c

agent_retry_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"fmt"
6	"strings"
7	"sync/atomic"
8	"testing"
9	"time"
10
11	"github.com/patriceckhart/zot/packages/provider"
12)
13
14type retryFakeClient struct {
15	calls int32
16}
17
18func (c *retryFakeClient) Name() string { return "retry-fake" }
19
20func (c *retryFakeClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) {
21	call := atomic.AddInt32(&c.calls, 1)
22	out := make(chan provider.Event, 4)
23	go func() {
24		defer close(out)
25		out <- provider.EventStart{Provider: "retry-fake", Model: req.Model}
26		if call == 1 {
27			out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("anthropic overloaded_error: Overloaded")}
28			return
29		}
30		out <- provider.EventTextDelta{Delta: "ok"}
31		out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
32			Role:    provider.RoleAssistant,
33			Content: []provider.Content{provider.TextBlock{Text: "ok"}},
34		}}
35	}()
36	return out, nil
37}
38
39func TestAgentRetriesOverloadedStreamError(t *testing.T) {
40	client := &retryFakeClient{}
41	a := NewAgent(client, "fake-model", "system", Registry{})
42	a.RetryBaseDelay = time.Millisecond
43
44	var turnErrs []string
45	err := a.Prompt(context.Background(), "hello", nil, func(ev AgentEvent) {
46		if e, ok := ev.(EvTurnEnd); ok && e.Err != nil {
47			turnErrs = append(turnErrs, e.Err.Error())
48		}
49	})
50	if err != nil {
51		t.Fatalf("Prompt returned %v", err)
52	}
53	if got := atomic.LoadInt32(&client.calls); got != 2 {
54		t.Fatalf("Stream calls = %d; want 2", got)
55	}
56	if len(turnErrs) != 1 || !strings.Contains(turnErrs[0], "overloaded_error") {
57		t.Fatalf("turn errors = %v; want one overloaded error before retry", turnErrs)
58	}
59	msgs := a.Messages()
60	if len(msgs) != 2 {
61		t.Fatalf("message count = %d; want user + final assistant", len(msgs))
62	}
63	if got := extractText(msgs[1]); got != "ok" {
64		t.Fatalf("final assistant text = %q; want ok", got)
65	}
66}
67
68type partialRetryFakeClient struct {
69	calls int32
70}
71
72func (c *partialRetryFakeClient) Name() string { return "partial-retry-fake" }
73
74func (c *partialRetryFakeClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) {
75	call := atomic.AddInt32(&c.calls, 1)
76	out := make(chan provider.Event, 4)
77	go func() {
78		defer close(out)
79		out <- provider.EventStart{Provider: "partial-retry-fake", Model: req.Model}
80		if call == 1 {
81			out <- provider.EventTextDelta{Delta: "partial"}
82			out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("provider returned error: 503"), Message: provider.Message{
83				Role:    provider.RoleAssistant,
84				Content: []provider.Content{provider.TextBlock{Text: "partial"}},
85			}}
86			return
87		}
88		out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
89			Role:    provider.RoleAssistant,
90			Content: []provider.Content{provider.TextBlock{Text: "recovered"}},
91		}}
92	}()
93	return out, nil
94}
95
96func TestAgentDropsPartialAssistantBeforeRetry(t *testing.T) {
97	client := &partialRetryFakeClient{}
98	a := NewAgent(client, "fake-model", "system", Registry{})
99	a.RetryBaseDelay = time.Millisecond
100
101	if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil {
102		t.Fatalf("Prompt returned %v", err)
103	}
104	msgs := a.Messages()
105	if len(msgs) != 2 {
106		t.Fatalf("message count = %d; want user + recovered assistant", len(msgs))
107	}
108	if got := extractText(msgs[1]); got != "recovered" {
109		t.Fatalf("final assistant text = %q; want recovered", got)
110	}
111}
112
113// captureClient records the last Request it received so tests can
114// assert what the agent put on the wire.
115type captureClient struct {
116	lastReq provider.Request
117}
118
119func (c *captureClient) Name() string { return "capture" }
120
121func (c *captureClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) {
122	c.lastReq = req
123	out := make(chan provider.Event, 3)
124	go func() {
125		defer close(out)
126		out <- provider.EventStart{Provider: "capture", Model: req.Model}
127		out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
128			Role:    provider.RoleAssistant,
129			Content: []provider.Content{provider.TextBlock{Text: "ok"}},
130		}}
131	}()
132	return out, nil
133}
134
135func TestAgentPropagatesMaxTokens(t *testing.T) {
136	client := &captureClient{}
137	a := NewAgent(client, "fake-model", "system", Registry{})
138	a.MaxTokens = 64000
139
140	if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil {
141		t.Fatalf("Prompt returned %v", err)
142	}
143	if client.lastReq.MaxTokens != 64000 {
144		t.Fatalf("request MaxTokens = %d; want 64000 (Agent.MaxTokens not propagated)", client.lastReq.MaxTokens)
145	}
146}
147
148func TestAgentPropagatesTemperature(t *testing.T) {
149	client := &captureClient{}
150	a := NewAgent(client, "fake-model", "system", Registry{})
151	temp := float32(0)
152	a.Temperature = &temp
153
154	if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil {
155		t.Fatalf("Prompt returned %v", err)
156	}
157	if client.lastReq.Temperature == nil || *client.lastReq.Temperature != temp {
158		t.Fatalf("request Temperature = %v; want %v", client.lastReq.Temperature, temp)
159	}
160}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close