Run tasks on Codex beside Claude; drop turn caps and mid-turn snapshots
This commit is contained in:
@@ -105,11 +105,19 @@ func (e *env) addTask() api.Task {
|
||||
}
|
||||
|
||||
func (e *env) manager() *agents.Manager {
|
||||
m := agents.New(e.store, agents.DefaultLimits, e.worktrees, e.leads)
|
||||
m := agents.New(e.store, e.worktrees, e.leads, codexHome(e.t))
|
||||
e.t.Cleanup(m.Close)
|
||||
return m
|
||||
}
|
||||
|
||||
func codexHome(t *testing.T) string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Fatalf("home dir: %v", err)
|
||||
}
|
||||
return filepath.Join(home, ".codex")
|
||||
}
|
||||
|
||||
func (e *env) waitFor(desc string, match func(api.Event) bool) []api.Event {
|
||||
e.t.Helper()
|
||||
seen := []api.Event{}
|
||||
@@ -203,8 +211,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.SnapshotTriggerMessage {
|
||||
t.Fatalf("first snapshot trigger %s, want message: the final message followed the tool calls", snaps[0].Trigger)
|
||||
if len(snaps) != 1 || snaps[0].Trigger != api.SnapshotTriggerTurnEnd {
|
||||
t.Fatalf("snapshots %+v, want one turn_end snapshot", snaps)
|
||||
}
|
||||
|
||||
window := 0
|
||||
@@ -314,7 +322,7 @@ func TestInterrupt(t *testing.T) {
|
||||
func TestRecoverMidTurn(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newEnv(t)
|
||||
first := agents.New(e.store, agents.DefaultLimits, e.worktrees, e.leads)
|
||||
first := agents.New(e.store, e.worktrees, e.leads, codexHome(t))
|
||||
|
||||
sess, err := first.Create(e.ctx, e.task, e.user, "Run the bash command `sleep 15` in the foreground (not in the background). After it finishes, create a file done.txt containing exactly the word done.", testModel)
|
||||
if err != nil {
|
||||
@@ -1089,11 +1097,18 @@ func TestForkSnapshot(t *testing.T) {
|
||||
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)
|
||||
src, err := m.Create(e.ctx, e.task, e.user, "Use the Write tool to create a.txt containing exactly APPLE-1. Then reply with the single word ok.", testModel)
|
||||
if err != nil {
|
||||
t.Fatalf("create session: %v", err)
|
||||
}
|
||||
e.waitIdle()
|
||||
e.waitFor("first snapshot", func(ev api.Event) bool { return ev.Kind == "snapshot.created" })
|
||||
|
||||
_, err = m.Send(e.ctx, src.Id, login, "Use the Write tool to create b.txt containing exactly BANANA-2. Then reply with the single word ok.", "")
|
||||
if err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
e.waitIdle()
|
||||
|
||||
snaps, err := e.store.ListSnapshots(e.ctx, e.task.Id)
|
||||
if err != nil {
|
||||
@@ -1221,61 +1236,88 @@ func TestFollowUpQuestionThread(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrencyQueue(t *testing.T) {
|
||||
const codexModel = "gpt-5.6-luna"
|
||||
|
||||
func TestCodexSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newEnv(t)
|
||||
m := agents.New(e.store, agents.Limits{Global: 1, PerVendor: map[api.AgentSessionVendor]int{api.Claude: 1}}, e.worktrees, e.leads)
|
||||
t.Cleanup(m.Close)
|
||||
m := e.manager()
|
||||
|
||||
first, err := m.Create(e.ctx, e.task, e.user, longCommandPrompt, testModel)
|
||||
sess, err := m.Create(e.ctx, e.task, e.user, "Create a file hi.txt containing exactly the word hi. Then end your turn with a one-line summary.", codexModel)
|
||||
if err != nil {
|
||||
t.Fatalf("create first: %v", err)
|
||||
t.Fatalf("create session: %v", err)
|
||||
}
|
||||
e.waitFor("first tool call", func(ev api.Event) bool { return ev.Kind == "agent.tool_call" })
|
||||
|
||||
second, err := m.Create(e.ctx, e.addTask(), e.user, "Reply with the single word ok.", testModel)
|
||||
if err != nil {
|
||||
t.Fatalf("create second: %v", err)
|
||||
if sess.Vendor != api.Codex {
|
||||
t.Fatalf("session vendor %s for %s, want codex", sess.Vendor, codexModel)
|
||||
}
|
||||
if second.State != api.AgentSessionStateQueued {
|
||||
t.Fatalf("second session is %s with one slot taken, want queued", second.State)
|
||||
}
|
||||
|
||||
usage, err := m.Usage(e.ctx)
|
||||
if err != nil || len(usage.Running) != 1 || len(usage.Queued) != 1 || usage.Queued[0].TaskId != second.TaskId {
|
||||
t.Fatalf("usage %+v %v", usage, err)
|
||||
}
|
||||
|
||||
cancelled, err := m.Interrupt(e.ctx, second.Id)
|
||||
if err != nil || cancelled.State != api.AgentSessionStateIdle {
|
||||
t.Fatalf("cancel queued turn: %+v %v", cancelled, err)
|
||||
}
|
||||
|
||||
second, err = m.Send(e.ctx, second.Id, login, "Reply with the single word ok.", "")
|
||||
if err != nil || second.State != api.AgentSessionStateQueued {
|
||||
t.Fatalf("requeue: %+v %v", second, err)
|
||||
}
|
||||
|
||||
_, err = m.Interrupt(e.ctx, first.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("interrupt first: %v", err)
|
||||
}
|
||||
|
||||
e.waitFor("first idle", func(ev api.Event) bool {
|
||||
return ev.Kind == "session.state" && *ev.AgentSessionId == first.Id && ev.Payload["state"] == string(api.AgentSessionStateIdle)
|
||||
})
|
||||
events := e.waitFor("second idle", func(ev api.Event) bool {
|
||||
return ev.Kind == "session.state" && *ev.AgentSessionId == second.Id && ev.Payload["state"] == string(api.AgentSessionStateIdle)
|
||||
})
|
||||
|
||||
replied := false
|
||||
for _, ev := range events {
|
||||
if ev.Kind == "agent.message" && *ev.AgentSessionId == second.Id && strings.Contains(strings.ToLower(ev.Payload["text"].(string)), "ok") {
|
||||
replied = true
|
||||
events := e.waitIdle()
|
||||
for _, kind := range []string{"user.message", "agent.tool_call", "agent.tool_result", "agent.message", "turn.ended", "session.context"} {
|
||||
if !has(events, kind) {
|
||||
t.Fatalf("missing %s in %v", kind, kinds(events))
|
||||
}
|
||||
}
|
||||
if !replied {
|
||||
t.Fatalf("queued turn never ran: %v", kinds(events))
|
||||
stored, err := e.store.GetSession(e.ctx, sess.Id)
|
||||
if err != nil || stored.VendorSessionId == "" {
|
||||
t.Fatalf("thread id not stored: %+v %v", stored, err)
|
||||
}
|
||||
for _, ev := range events {
|
||||
if ev.Kind != "turn.ended" {
|
||||
continue
|
||||
}
|
||||
if ev.Payload["is_error"] != false || ev.Payload["vendor_uuid"] == "" || ev.Payload["inbox_item_id"] == nil || ev.Payload["commit"] == nil {
|
||||
t.Fatalf("turn.ended %v", ev.Payload)
|
||||
}
|
||||
}
|
||||
if got := gitOut(t, e.task.WorktreePath, "show", "HEAD:hi.txt"); strings.TrimSpace(got) != "hi" {
|
||||
t.Fatalf("hi.txt in byre's turn commit: %q", got)
|
||||
}
|
||||
|
||||
_, err = m.Send(e.ctx, sess.Id, login, "x", "claude-haiku-5-5")
|
||||
if !errors.Is(err, agents.ErrInvalid) {
|
||||
t.Fatalf("switching a codex session to a claude model: %v", err)
|
||||
}
|
||||
|
||||
_, err = m.Send(e.ctx, sess.Id, login, "Ask the human, with byre's ask_human tool and blocking false, whether hi.txt should say hello instead; recommend keeping hi. Then end your turn with a one-line summary.", "")
|
||||
if err != nil {
|
||||
t.Fatalf("send: %v", err)
|
||||
}
|
||||
events = e.waitIdle()
|
||||
asked := false
|
||||
for _, ev := range events {
|
||||
if ev.Kind == "inbox.created" && ev.Payload["kind"] == string(api.Question) {
|
||||
asked = true
|
||||
}
|
||||
}
|
||||
if !asked {
|
||||
t.Fatalf("no question filed through ask_human: %v", kinds(events))
|
||||
}
|
||||
|
||||
snaps, err := e.store.ListSnapshots(e.ctx, e.task.Id)
|
||||
if err != nil || len(snaps) == 0 {
|
||||
t.Fatalf("no snapshots: %+v %v", snaps, err)
|
||||
}
|
||||
fork, err := m.Fork(e.ctx, e.task, e.project, snaps[0].N, e.user)
|
||||
if err != nil {
|
||||
t.Fatalf("fork: %v", err)
|
||||
}
|
||||
forked, err := e.store.ListSessions(e.ctx, fork.Id)
|
||||
if err != nil || len(forked) != 1 || forked[0].Vendor != api.Codex {
|
||||
t.Fatalf("fork sessions %+v %v", forked, err)
|
||||
}
|
||||
_, err = m.Send(e.ctx, forked[0].Id, login, "Without running any commands, from memory: what file did you create, and did you ask the human anything? Reply in one line.", "")
|
||||
if err != nil {
|
||||
t.Fatalf("send to fork: %v", err)
|
||||
}
|
||||
reply := ""
|
||||
for _, ev := range e.waitFor("fork idle", func(ev api.Event) bool {
|
||||
return ev.Kind == "session.state" && *ev.AgentSessionId == forked[0].Id && ev.Payload["state"] == string(api.AgentSessionStateIdle)
|
||||
}) {
|
||||
if ev.Kind == "agent.message" && *ev.AgentSessionId == forked[0].Id {
|
||||
reply += ev.Payload["text"].(string)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(reply, "hi.txt") {
|
||||
t.Fatalf("fork lost the first turn; assistant said %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user