func TestToolRunsAfterItsCallIsDurable(t *testing.T) {
dir := t.TempDir()
- log := &recordingLog{
+ log := &ToolRecordingLog{
marker: filepath.Join(dir, "ran"),
}
func TestToolDoesNotRunAfterAnInterruptBeforeTheFlush(t *testing.T) {
dir := t.TempDir()
- log := &recordingLog{
+ log := &ToolRecordingLog{
marker: filepath.Join(dir, "ran"),
}
func TestShellRunsInTheWorkingDirectory(t *testing.T) {
dir := t.TempDir()
- box := newTestToolbox(t, &recordingLog{}, dir)
+ box := newTestToolbox(t, &ToolRecordingLog{}, dir)
testCases := []struct {
name string
command string
}
func TestShellInterruptKillsTheCommandAndItsChildren(t *testing.T) {
- box := newTestToolbox(t, &recordingLog{}, t.TempDir())
+ box := newTestToolbox(t, &ToolRecordingLog{}, t.TempDir())
ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond)
defer cancel()
func TestFileTools(t *testing.T) {
dir := t.TempDir()
- box := newTestToolbox(t, &recordingLog{}, dir)
+ box := newTestToolbox(t, &ToolRecordingLog{}, dir)
path := filepath.Join(dir, "notes", "a.txt")
require.Equal(t, "wrote 12 bytes to "+path, box.run(t.Context(), "write", `{"path":"notes/a.txt","text":"one two two\n"}`))
require.Equal(t, "error: old occurs 2 times in "+path+", not once", box.run(t.Context(), "edit", `{"path":"notes/a.txt","old":"two","new":"2"}`))
require.Equal(t, "edited "+path, box.run(t.Context(), "edit", `{"path":"notes/a.txt","old":"one","new":"1"}`))
require.Equal(t, "1 two two\n", box.run(t.Context(), "read", `{"path":"notes/a.txt"}`))
- require.Contains(t, box.run(t.Context(), "read", `{"path":"missing.txt"}`), "error: open "+filepath.Join(dir, "missing.txt"))
+ require.Contains(t, box.run(t.Context(), "read", `{"path":"missing.txt"}`), "error: open tool file: open "+filepath.Join(dir, "missing.txt"))
require.Equal(t, `error: arguments: json: unknown field "file"`, box.run(t.Context(), "read", `{"file":"a.txt"}`))
require.Equal(t, `error: unknown tool "grep"`, box.run(t.Context(), "grep", `{}`))
}
func TestZoomAndDateReadTheChat(t *testing.T) {
- memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
+ memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer(), discardLog())
require.NoError(t, err)
defer func() {
require.NoError(t, err)
}
- box := &toolbox{
- log: memory,
- chat: memory,
- workdir: t.TempDir(),
+ box := &Toolbox{
+ log: memory,
+ chat: memory,
+ subagents: newTestSubagents(t),
+ workdir: t.TempDir(),
}
testCases := []struct {
require.Equal(t, "error: message 4 does not exist", box.run(t.Context(), "date", `{"id":4}`))
}
+func TestZoomWithANameGivesTheWholeChatOfTheAgent(t *testing.T) {
+ box := newTestToolbox(t, &ToolRecordingLog{}, t.TempDir())
+ subagent, err := box.subagents.Create(t.Context())
+ require.NoError(t, err)
+
+ for _, message := range [][2]string{{"note", "subagent"}, {"user", "Count the files."}} {
+ _, err = subagent.Append(message[0], message[1])
+ require.NoError(t, err)
+ }
+
+ output := box.run(t.Context(), "zoom", `{"id":0,"n":1,"name":"`+subagent.name+`"}`)
+ require.Equal(t, "0|note: subagent\n\n1|user: Count the files.", output)
+
+ require.Equal(t, "error: no agent BlueFox", box.run(t.Context(), "zoom", `{"id":0,"n":1,"name":"BlueFox"}`))
+ require.Equal(t, "error: line 0+1 goes past the last message, -1", box.run(t.Context(), "zoom", `{"id":0,"n":1,"name":null}`))
+}
+
func TestClipKeepsTheHeadAndTheTail(t *testing.T) {
- short := strings.Repeat("a", clipCharacters)
- require.Equal(t, short, clip(short))
+ short := strings.Repeat("a", toolClipCharacters)
+ require.Equal(t, short, toolClip(short))
- text := strings.Repeat("h", clipCharacters/2) + strings.Repeat("é", 5000) + strings.Repeat("t", clipCharacters/2)
- require.Equal(t, strings.Repeat("h", clipCharacters/2)+"\n[... 10000 bytes cut ...]\n"+strings.Repeat("t", clipCharacters/2), clip(text))
+ text := strings.Repeat("h", toolClipCharacters/2) + strings.Repeat("é", 5000) + strings.Repeat("t", toolClipCharacters/2)
+ require.Equal(t, strings.Repeat("h", toolClipCharacters/2)+"\n[... 10000 bytes cut ...]\n"+strings.Repeat("t", toolClipCharacters/2), toolClip(text))
- half := strings.Repeat("é", clipCharacters/2)
- clipped := clip(strings.Repeat("é", clipCharacters+2))
+ half := strings.Repeat("é", toolClipCharacters/2)
+ clipped := toolClip(strings.Repeat("é", toolClipCharacters+2))
require.True(t, utf8.ValidString(clipped))
require.Equal(t, half+"\n[... 4 bytes cut ...]\n"+half, clipped)
}
func TestHeadTailKeepsTheEndsOfALongOutput(t *testing.T) {
- output := &headTail{}
+ output := &ToolHeadTail{}
for n := range 1000 {
_, err := fmt.Fprintf(output, "%04d%s\n", n, strings.Repeat("x", 995))
require.NoError(t, err)
text := output.String()
require.True(t, strings.HasPrefix(text, "0000xxx"))
require.True(t, strings.HasSuffix(text, "0999"+strings.Repeat("x", 995)+"\n"))
- require.Contains(t, text, fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-clipCharacters))
- require.Equal(t, clipCharacters+len(fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-clipCharacters)), len(text))
+ require.Contains(t, text, fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-toolClipCharacters))
+ require.Len(t, text, toolClipCharacters+len(fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-toolClipCharacters)))
}
-// recordingLog is a chat log that records each append and flush. The flush
+// ToolRecordingLog is a chat log that records each append and flush. The flush
// records whether the marker file exists.
-type recordingLog struct {
+type ToolRecordingLog struct {
mutex sync.Mutex
marker string
events []string
}
-func (r *recordingLog) Append(kind, text string) (Message, error) {
+func (r *ToolRecordingLog) Append(kind, text string) (Message, error) {
r.mutex.Lock()
defer r.mutex.Unlock()
return Message{}, nil
}
-func (r *recordingLog) Flush(ctx context.Context) error {
+func (r *ToolRecordingLog) Flush(ctx context.Context) error {
r.mutex.Lock()
defer r.mutex.Unlock()
return ctx.Err()
}
-func (r *recordingLog) Events() []string {
+func (r *ToolRecordingLog) Events() []string {
r.mutex.Lock()
defer r.mutex.Unlock()
// newTestToolbox returns a toolbox with log, an empty chat, and the working
// directory dir.
-func newTestToolbox(t *testing.T, log chatLog, dir string) *toolbox {
+func newTestToolbox(t *testing.T, log ToolLog, dir string) *Toolbox {
t.Helper()
- memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
+ memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer(), discardLog())
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, memory.Close())
})
- return &toolbox{
- log: log,
- chat: memory,
- workdir: dir,
+ return &Toolbox{
+ log: log,
+ chat: memory,
+ subagents: newTestSubagents(t),
+ workdir: dir,
}
}