]> git.giacomolli.uk - onechat/blobdiff - subagent_run_test.go
Show the subagents in the clients
[onechat] / subagent_run_test.go
index 3027fe6dd38dd5818fccaefdbf50366f2a9a004f..5caf0d886ef3a6855ef142f1ed28ca41efec46f9 100644 (file)
@@ -5,6 +5,7 @@ import (
        "net/http"
        "os"
        "path/filepath"
+       "strconv"
        "strings"
        "testing"
        "time"
@@ -56,6 +57,13 @@ func TestSpawnRunsASubagentInTheBackground(t *testing.T) {
                "work: " + report,
                "reply: Noted.",
        }, test.Log(t))
+       require.Equal(t, Event{
+               Kind:   "work",
+               ID:     4,
+               Text:   report,
+               Source: name,
+               Status: subagentFinished,
+       }, test.events.LastEvent("work"))
 
        var first, second *ModelFakeCall
        for n := range server.Calls() {
@@ -130,7 +138,7 @@ func TestReportJoinsTheNextRequestOfARunningTurn(t *testing.T) {
 
        <-arrived
        report := subagentReport("GreenOtter", []string{"It is done."})
-       test.loop.Report(report, false)
+       test.loop.Report("GreenOtter", report, subagentFinished)
        close(release)
 
        test.events.WaitFor(t, "idle", 1)
@@ -145,7 +153,7 @@ func TestReportOfAStoppedSubagentStartsNoTurn(t *testing.T) {
        server := newModelFake(t)
        test := newTestTurnLoop(t, server)
        report := subagentReport("GreenOtter", []string{"failed: context canceled"})
-       test.loop.Report(report, true)
+       test.loop.Report("GreenOtter", report, subagentStopped)
 
        test.events.WaitFor(t, "work", 1)
        time.Sleep(50 * time.Millisecond)
@@ -177,6 +185,7 @@ func TestFullContextGivesAFailedReport(t *testing.T) {
                "work: [" + name + "]\nfailed: model call: HTTP 400: " + subagentTestFullContext + "\n\nFull chat: zoom(\"" + name + "\")",
                "reply: It failed.",
        }, test.Log(t))
+       require.Equal(t, subagentFailed, test.events.LastEvent("work").Status)
 }
 
 func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) {
@@ -224,6 +233,156 @@ func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) {
        require.Len(t, entries, 1)
 }
 
+func TestTellReachesARunningSubagentBetweenItsToolCalls(t *testing.T) {
+       server := newModelFake(t)
+       arrived := make(chan struct{})
+       told := make(chan struct{})
+       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+               switch {
+               case !subagentTestCall(call):
+                       modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
+               case call.number == 0:
+                       close(arrived)
+                       <-told
+                       modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"true"}`)}, 100, 0, 5))
+               default:
+                       modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Used the second disk.")}, 100, 0, 5))
+               }
+       }
+
+       test := newTestTurnLoop(t, server)
+       runs := test.loop.runs
+       _, err := runs.Spawn(t.Context(), []string{"Copy the files."}, nil)
+       require.NoError(t, err)
+
+       <-arrived
+       name := test.events.Last("subagent")
+       output, err := runs.Tell(t.Context(), name, "Use the second disk.")
+       require.NoError(t, err)
+       require.Equal(t, "Sent. "+name+" reads it between its tool calls, and its report answers it.", output)
+       close(told)
+
+       test.events.WaitFor(t, "idle", 1)
+       require.Equal(t, "Use the second disk.", subagentTestText(subagentTestLastItem(server.Call(1))))
+       require.Equal(t, "work: "+subagentReport(name, []string{"Used the second disk."}), test.Log(t)[0])
+}
+
+func TestTellAfterTheFinalAnswerStartsAnotherChain(t *testing.T) {
+       server := newModelFake(t)
+       arrived := make(chan struct{})
+       told := make(chan struct{})
+       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+               switch {
+               case !subagentTestCall(call):
+                       modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
+               case call.number == 0:
+                       close(arrived)
+                       <-told
+                       modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("First.")}, 100, 0, 5))
+               default:
+                       modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Second.")}, 100, 0, 5))
+               }
+       }
+
+       test := newTestTurnLoop(t, server)
+       runs := test.loop.runs
+       _, err := runs.Spawn(t.Context(), []string{"Count."}, nil)
+       require.NoError(t, err)
+
+       <-arrived
+       name := test.events.Last("subagent")
+       _, err = runs.Tell(t.Context(), name, "Count again.")
+       require.NoError(t, err)
+       close(told)
+
+       test.events.WaitFor(t, "idle", 1)
+       require.Equal(t, "0|note: subagent\n\n1|user: Count.\n\n2|user: Count again.\n\n3|reply: First.", subagentTestText(subagentTestLastItem(server.Call(1))))
+       require.Equal(t, "work: "+subagentReport(name, []string{"First.", "Second."}), test.Log(t)[0])
+}
+
+func TestTellToAFinishedSubagentGoesOnFromItsLog(t *testing.T) {
+       server := newModelFake(t)
+       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+               text := "Noted."
+               if subagentTestCall(call) {
+                       text = "Reply " + strconv.Itoa(call.number) + "."
+               }
+
+               modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5))
+       }
+
+       test := newTestTurnLoop(t, server)
+       runs := test.loop.runs
+       effort := "high"
+       _, err := runs.Spawn(t.Context(), []string{"Count."}, &effort)
+       require.NoError(t, err)
+       test.events.WaitFor(t, "idle", 1)
+
+       name := test.events.Last("subagent")
+       output, err := runs.Tell(t.Context(), name, "And the hidden files?")
+       require.NoError(t, err)
+       require.Equal(t, "Sent. "+name+"'s reply will reach you as a message.", output)
+       test.events.WaitFor(t, "idle", 2)
+
+       call := server.Call(2)
+       require.True(t, subagentTestCall(call))
+       require.Equal(t, "high", call.body["reasoning"].(map[string]any)["effort"])
+       require.Equal(t, "0|note: subagent at effort high\n\n1|user: Count.\n\n2|reply: Reply 0.\n\n3|user: And the hidden files?", subagentTestText(subagentTestLastItem(call)))
+
+       // The view of the new run holds the chat of now, with the first report.
+       texts := turnInputTexts(t, call)
+       require.Contains(t, strings.Join(texts[:len(texts)-2], ""), "work: ["+name+"]")
+       require.Equal(t, "work: "+subagentReport(name, []string{"Reply 2."}), test.Log(t)[2])
+
+       _, err = runs.Tell(t.Context(), "BlueFox", "Hello.")
+       require.EqualError(t, err, "no agent BlueFox")
+}
+
+func TestStopEndsASubagentAndItsCommand(t *testing.T) {
+       server := newModelFake(t)
+       server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+               modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"touch started && sleep 30"}`)}, 100, 0, 5))
+       }
+
+       test := newTestTurnLoop(t, server)
+       runs := test.loop.runs
+       _, err := runs.Spawn(t.Context(), []string{"Wait long."}, nil)
+       require.NoError(t, err)
+
+       name := test.events.Last("subagent")
+       subagent, err := runs.subagents.Get(t.Context(), name)
+       require.NoError(t, err)
+
+       require.Eventually(t, func() bool {
+               _, err := os.Stat(filepath.Join(subagent.WorkDir(), "started"))
+               return err == nil
+       }, 10*time.Second, time.Millisecond)
+
+       start := time.Now()
+       found, err := test.loop.StopSubagent(name)
+       require.NoError(t, err)
+       require.True(t, found)
+
+       test.events.WaitFor(t, "work", 1)
+       require.Less(t, time.Since(start), 10*time.Second)
+       require.Equal(t, []string{
+               "note: Stopped by the user: " + name + ".",
+               "work: " + subagentReport(name, []string{"stopped"}),
+       }, test.Log(t))
+       require.Equal(t, subagentStopped, test.events.LastEvent("work").Status)
+
+       log, err := subagent.Show()
+       require.NoError(t, err)
+       require.Contains(t, log, "echo: \n[interrupted]")
+
+       found, err = test.loop.StopSubagent(name)
+       require.NoError(t, err)
+       require.False(t, found)
+
+       time.Sleep(50 * time.Millisecond)
+       require.Zero(t, test.events.Count("busy"))
+}
+
 func TestCloseEndsTheSubagentsWithNoReport(t *testing.T) {
        server := newModelFake(t)
        arrived := make(chan struct{})