1package subagents
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/zut/packages/core"
10)
11
12func TestJournaledResidentChildReceivesLiveJournalEvents(t *testing.T) {
13 journal, err := OpenResidentJournal(t.TempDir(), "live-child")
14 if err != nil {
15 t.Fatal(err)
16 }
17 child := newJournaledResidentChild(ResidentChildSpec{}, journal, func(context.Context, string) error { return nil }, nil)
18 defer child.Close(context.Background())
19 oldActivity := time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
20 child.mu.Lock()
21 child.activityUpdatedAt = oldActivity
22 child.mu.Unlock()
23 child.live.Start("turn-1")
24 if err := journal.RecordAgentEvent(core.EvTextDelta{Delta: "streaming"}); err != nil {
25 t.Fatal(err)
26 }
27 if got := child.Live(); got.AssistantText != "streaming" || got.Revision == 0 {
28 t.Fatalf("live child = %#v", got)
29 }
30 if got := child.ActivityUpdatedAt(); !got.After(oldActivity) {
31 t.Fatalf("activity time = %s, want after %s", got, oldActivity)
32 }
33}
34
35func TestResidentChildRunsAcceptedPromptsFIFOWithOneActiveTurn(t *testing.T) {
36 started := make(chan string, 2)
37 releaseFirst := make(chan struct{})
38 completed := make(chan string, 2)
39 child := NewResidentChild(func(ctx context.Context, prompt string) error {
40 started <- prompt
41 if prompt == "first" {
42 select {
43 case <-releaseFirst:
44 case <-ctx.Done():
45 return ctx.Err()
46 }
47 }
48 completed <- prompt
49 return nil
50 })
51 defer child.Close(context.Background())
52
53 if err := child.Resume(context.Background(), "first"); err != nil {
54 t.Fatalf("first Resume: %v", err)
55 }
56 if err := child.Resume(context.Background(), "second"); err != nil {
57 t.Fatalf("second Resume: %v", err)
58 }
59 select {
60 case got := <-started:
61 if got != "first" {
62 t.Fatalf("first start = %q", got)
63 }
64 case <-time.After(time.Second):
65 t.Fatal("first turn did not start")
66 }
67 select {
68 case got := <-started:
69 t.Fatalf("second turn started before first completed: %q", got)
70 default:
71 }
72 close(releaseFirst)
73 for _, want := range []string{"first", "second"} {
74 select {
75 case got := <-completed:
76 if got != want {
77 t.Fatalf("completion = %q, want %q", got, want)
78 }
79 case <-time.After(time.Second):
80 t.Fatalf("%s turn did not complete", want)
81 }
82 }
83}
84
85func TestResidentChildReportsAndPreservesFailureWhenTurnStartCannotPersist(t *testing.T) {
86 journal, err := OpenResidentJournal(t.TempDir(), "start-persistence-failure")
87 if err != nil {
88 t.Fatal(err)
89 }
90 if err := journal.Close(); err != nil {
91 t.Fatal(err)
92 }
93 completed := make(chan ResidentCompletion, 1)
94 run := make(chan struct{}, 1)
95 child := newJournaledResidentChild(
96 ResidentChildSpec{ID: "start-persistence-failure"},
97 journal,
98 func(context.Context, string) error {
99 run <- struct{}{}
100 return nil
101 },
102 func(completion ResidentCompletion) { completed <- completion },
103 )
104
105 request := residentPrompt{turnID: "turn", prompt: "task", ack: make(chan error, 1)}
106 select {
107 case child.inbox <- request:
108 case <-time.After(time.Second):
109 t.Fatal("child did not accept test prompt")
110 }
111 select {
112 case completion := <-completed:
113 if completion.ChildID != "start-persistence-failure" || completion.TurnID != "turn" || completion.Task != "task" {
114 t.Fatalf("completion = %#v", completion)
115 }
116 if completion.Err == nil || !strings.Contains(completion.Err.Error(), "persist resident child start state") {
117 t.Fatalf("completion error = %v", completion.Err)
118 }
119 case <-time.After(time.Second):
120 t.Fatal("failed turn did not report completion")
121 }
122 select {
123 case <-child.done:
124 case <-time.After(time.Second):
125 t.Fatal("child did not stop after start persistence failure")
126 }
127 if state := child.State(); state != ResidentFailed {
128 t.Fatalf("state = %q, want %q", state, ResidentFailed)
129 }
130 select {
131 case <-run:
132 t.Fatal("runner started after turn-start persistence failure")
133 default:
134 }
135}