import (
"context"
"encoding/json"
- "errors"
+ "log"
"net/http"
"path/filepath"
"strings"
require.Equal(t, []string{"user", "busy", "tool", "echo", "reply", "idle"}, test.events.Kinds())
var turns []string
+
for _, record := range modelReadUsage(t, test.usagePath) {
turns = append(turns, record.Turn)
}
require.NoError(t, err)
<-arrived
+
_, err = test.loop.Submit(terminalSource, "second, during the turn")
require.NoError(t, err)
close(release)
test.events.WaitFor(t, "idle", 1)
+
input := server.Call(1).body["input"].([]any)
require.Equal(t, map[string]any{
"role": "user",
require.NoError(t, err)
<-arrived
+
_, err = test.loop.Submit(terminalSource, "second")
require.NoError(t, err)
close(release)
require.NoError(t, err)
test.events.WaitFor(t, "idle", 2)
require.Equal(t, "reply: answer", test.Log(t)[2])
+ require.Empty(t, test.logs.String())
}
func TestFailedCallEndsTheTurnWithoutARetry(t *testing.T) {
server := newModelFake(t)
- server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+ server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
http.Error(w, "busy", http.StatusServiceUnavailable)
}
time.Sleep(50 * time.Millisecond)
require.Equal(t, 1, server.Calls())
require.Contains(t, test.events.Last("error"), "HTTP 503")
+ require.Equal(t, 1, strings.Count(test.logs.String(), "\n"))
+ require.Contains(t, test.logs.String(), "turn failed: ")
+ require.Contains(t, test.logs.String(), "HTTP 503")
+ require.NotContains(t, test.logs.String(), "hello")
}
func TestViewMessagesOnlyGrowBetweenTurns(t *testing.T) {
first := server.Call(1).body["input"].([]any)
second := server.Call(2).body["input"].([]any)
+
require.Len(t, first, 4)
require.Len(t, second, 5)
require.Equal(t, first[:2], second[:2])
memory *Memory
loop *TurnLoop
events *TurnEventRecorder
+ logs *SyncBuffer
usagePath string
}
t.Helper()
var lines []string
+
for id := range l.memory.Len() {
message, err := l.memory.Message(id)
require.NoError(t, err)
// Last returns the text of the last event of kind.
func (r *TurnEventRecorder) Last(kind string) string {
+ return r.LastEvent(kind).Text
+}
+
+// LastEvent returns the last event of kind.
+func (r *TurnEventRecorder) LastEvent(kind string) Event {
r.mutex.Lock()
defer r.mutex.Unlock()
for n := len(r.events) - 1; n >= 0; n-- {
if r.events[n].Kind == kind {
- return r.events[n].Text
+ return r.events[n]
}
}
- return ""
+ return Event{}
}
func (r *TurnEventRecorder) Add(event Event) {
require.NoError(t, json.Unmarshal(call.raw, &body))
var texts []string
+
for _, item := range body.Input {
if item.Role != "user" {
continue
}
var text strings.Builder
+
for _, part := range item.Content {
text.WriteString(part.Text)
}
agent.config.Workdir = t.TempDir()
clock := newTestClock()
- memory, err := openMemory(t.Context(), t.TempDir(), clock.Now, summarizer)
+ memory, err := openMemory(t.Context(), t.TempDir(), clock.Now, summarizer, discardLog())
require.NoError(t, err)
test := &TurnTestLoop{
agent: agent,
memory: memory,
events: &TurnEventRecorder{},
+ logs: &SyncBuffer{},
usagePath: filepath.Join(t.TempDir(), "usage.jsonl"),
}
client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), test.usagePath)
- test.loop, err = newTurnLoop(agent, memory, newTestSubagents(t), client, clock.Now, test.events.Add)
+ whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), clock)
+ test.loop, err = newTurnLoop(agent, memory, newTestSubagents(t), whatsapp, newTestSchedules(t, clock.Now), client, clock.Now, log.New(test.logs, "", 0), test.events.Add)
require.NoError(t, err)
+
ctx, cancel := context.WithCancel(t.Context())
done := make(chan error)
go func() {
t.Cleanup(func() {
cancel()
- require.True(t, errors.Is(<-done, context.Canceled))
+ require.ErrorIs(t, <-done, context.Canceled)
require.NoError(t, memory.Close())
})