1package subagents
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10)
11
12func TestBatchIDsRemainUniqueWithFixedClock(t *testing.T) {
13 now := time.Unix(123, 0)
14 f := New(Config{
15 Root: t.TempDir(), RepoRoot: t.TempDir(), Now: func() time.Time { return now },
16 Policy: SubagentPolicy{MaxConcurrent: 2, MaxTotalSpawned: 4},
17 NewRunner: func(*Agent) Runner {
18 return RunnerFunc(func(context.Context, Sink) error { return nil })
19 },
20 })
21 t.Cleanup(f.StopAll)
22 first, err := f.SpawnBatch(context.Background(), BatchRequest{Tasks: []string{"one"}})
23 if err != nil {
24 t.Fatal(err)
25 }
26 second, err := f.SpawnBatch(context.Background(), BatchRequest{Tasks: []string{"two"}})
27 if err != nil {
28 t.Fatal(err)
29 }
30 first.Wait()
31 second.Wait()
32 if first.ID == second.ID {
33 t.Fatalf("duplicate batch ids: %q", first.ID)
34 }
35}
36
37func TestBatchReloadsDurableResult(t *testing.T) {
38 root := t.TempDir()
39 newSupervisor := func() *Supervisor {
40 return New(Config{
41 Root: root, RepoRoot: root,
42 NewRunner: func(*Agent) Runner {
43 return RunnerFunc(func(context.Context, Sink) error { return nil })
44 },
45 })
46 }
47 first := newSupervisor()
48 t.Cleanup(first.StopAll)
49 batch, err := first.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}})
50 if err != nil {
51 t.Fatal(err)
52 }
53 want, err := first.WaitBatch(batch.ID)
54 if err != nil {
55 t.Fatal(err)
56 }
57 if want.Status != BatchSucceeded {
58 t.Fatalf("first batch status = %s", want.Status)
59 }
60
61 second := newSupervisor()
62 t.Cleanup(second.StopAll)
63 loaded, errs := second.Reload()
64 if len(errs) != 0 {
65 t.Fatalf("reload errors: %v", errs)
66 }
67 if loaded != 2 {
68 t.Fatalf("reload loaded %d agents, want 2", loaded)
69 }
70 got, err := second.WaitBatch(batch.ID)
71 if err != nil {
72 t.Fatal(err)
73 }
74 if got.Status != BatchSucceeded || len(got.Results) != 2 {
75 t.Fatalf("reloaded batch = %#v", got)
76 }
77}
78
79func TestBatchReloadRejectsUnsafeIDs(t *testing.T) {
80 root := t.TempDir()
81 batchDir := filepath.Join(root, "batches")
82 if err := os.MkdirAll(batchDir, 0o700); err != nil {
83 t.Fatal(err)
84 }
85 data, err := json.Marshal(BatchResult{
86 Version: ProtocolVersion, BatchID: "../escape", TaskCount: 0, Status: BatchRunning,
87 })
88 if err != nil {
89 t.Fatal(err)
90 }
91 if err := os.WriteFile(filepath.Join(batchDir, "queued.json"), data, 0o600); err != nil {
92 t.Fatal(err)
93 }
94 f := New(Config{Root: root, RepoRoot: root})
95 if _, errs := f.Reload(); len(errs) == 0 {
96 t.Fatal("unsafe batch metadata was accepted")
97 }
98 if _, err := os.Stat(filepath.Join(root, "escape.json")); !os.IsNotExist(err) {
99 t.Fatalf("unsafe batch path was written: %v", err)
100 }
101}
102
103func TestBatchSpawnWaitAndCollect(t *testing.T) {
104 root := t.TempDir()
105 f := New(Config{
106 Root: root, RepoRoot: root,
107 Policy: SubagentPolicy{MaxConcurrent: 2, MaxConcurrentPerParent: 2, MaxTotalSpawned: 4, DefaultTimeout: 0, IdleTimeout: 0},
108 NewRunner: func(*Agent) Runner {
109 return RunnerFunc(func(context.Context, Sink) error { return nil })
110 },
111 })
112 t.Cleanup(f.StopAll)
113 batch, err := f.SpawnBatch(context.Background(), BatchRequest{Context: "review", Tasks: []string{"one", "two"}, MaxConcurrent: 1})
114 if err != nil {
115 t.Fatal(err)
116 }
117 result, err := f.WaitBatch(batch.ID)
118 if err != nil {
119 t.Fatal(err)
120 }
121 if result.Status != BatchSucceeded || len(result.ChildIDs) != 2 || len(result.Results) != 2 {
122 t.Fatalf("batch result = %#v", result)
123 }
124 if batch.Status() != BatchSucceeded {
125 t.Fatalf("batch status = %s", batch.Status())
126 }
127}