Fork tasks from snapshots; snapshot only when text follows tool calls
This commit is contained in:
@@ -203,8 +203,8 @@ func TestTurnAndFollowUp(t *testing.T) {
|
||||
if got := gitOut(t, e.repo, "show", latest.CommitSha+":hi.txt"); strings.TrimSpace(got) != "hi" {
|
||||
t.Fatalf("hi.txt in the latest snapshot: %q", got)
|
||||
}
|
||||
if snaps[0].Trigger != api.SnapshotTriggerToolCall {
|
||||
t.Fatalf("first snapshot trigger %s, want tool_call", snaps[0].Trigger)
|
||||
if snaps[0].Trigger != api.SnapshotTriggerMessage {
|
||||
t.Fatalf("first snapshot trigger %s, want message: the final message followed the tool calls", snaps[0].Trigger)
|
||||
}
|
||||
|
||||
window := 0
|
||||
@@ -1015,6 +1015,77 @@ func TestStartTodo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestForkSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newEnv(t)
|
||||
m := e.manager()
|
||||
|
||||
src, err := m.Create(e.ctx, e.task, e.user, "Use the Write tool to create a.txt containing exactly APPLE-1. Once that call has returned, write the sentence \"a.txt is written.\" as text. Only after that, use the Write tool to create b.txt containing exactly BANANA-2. Then reply with the single word ok.", testModel)
|
||||
if err != nil {
|
||||
t.Fatalf("create session: %v", err)
|
||||
}
|
||||
e.waitIdle()
|
||||
|
||||
snaps, err := e.store.ListSnapshots(e.ctx, e.task.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("list snapshots: %v", err)
|
||||
}
|
||||
n := 0
|
||||
for _, snap := range snaps {
|
||||
files := gitOut(t, e.repo, "ls-tree", "--name-only", snap.CommitSha)
|
||||
if strings.Contains(files, "a.txt") && !strings.Contains(files, "b.txt") {
|
||||
n = snap.N
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("no snapshot holds a.txt without b.txt: %+v", snaps)
|
||||
}
|
||||
|
||||
fork, err := m.Fork(e.ctx, e.task, e.project, n, e.user)
|
||||
if err != nil {
|
||||
t.Fatalf("fork: %v", err)
|
||||
}
|
||||
if fork.ForkedFrom == nil || *fork.ForkedFrom != e.task.Id || fork.ForkedAt == nil || *fork.ForkedAt != n {
|
||||
t.Fatalf("fork task %+v", fork)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fork.WorktreePath, "a.txt")); err != nil {
|
||||
t.Fatalf("a.txt missing from the fork's worktree: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(fork.WorktreePath, "b.txt")); err == nil {
|
||||
t.Fatalf("b.txt, written after the snapshot, is in the fork's worktree")
|
||||
}
|
||||
|
||||
sessions, err := e.store.ListSessions(e.ctx, fork.Id)
|
||||
if err != nil || len(sessions) != 1 {
|
||||
t.Fatalf("fork sessions %+v %v", sessions, err)
|
||||
}
|
||||
sess := sessions[0]
|
||||
if sess.State != api.AgentSessionStateIdle || sess.ForkedFrom == nil || *sess.ForkedFrom != src.VendorSessionId || sess.VendorSessionId == src.VendorSessionId {
|
||||
t.Fatalf("fork session %+v", sess)
|
||||
}
|
||||
|
||||
_, err = m.Send(e.ctx, sess.Id, login, "Without using any tools, list the contents of every file you have created in this conversation, one per line, and nothing else.", "")
|
||||
if err != nil {
|
||||
t.Fatalf("send to fork: %v", err)
|
||||
}
|
||||
reply := ""
|
||||
for _, ev := range e.waitFor("fork turn end", func(ev api.Event) bool {
|
||||
return ev.Kind == "turn.ended" && ev.TaskId != nil && *ev.TaskId == fork.Id
|
||||
}) {
|
||||
if ev.Kind == "agent.message" && ev.TaskId != nil && *ev.TaskId == fork.Id {
|
||||
reply += ev.Payload["text"].(string)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(reply, "APPLE-1") || strings.Contains(reply, "BANANA-2") {
|
||||
t.Fatalf("fork's conversation isn't cut at the snapshot; it said %q", reply)
|
||||
}
|
||||
|
||||
_, err = m.Fork(e.ctx, e.task, e.project, n+100, e.user)
|
||||
if !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("fork of a missing snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTestFile(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
err := os.WriteFile(path, []byte(content), 0o644)
|
||||
|
||||
Reference in New Issue
Block a user