turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

agents_test.go · 244 lines · 7.1 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 20h ago1package app
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
📦 Turbo Core f3ade8d k33g 13h ago9 "rickub.com/turbo-editors/turbo-core/acp"
10 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g 20h ago11)
12
13// newAgentsApp returns an editor whose project holds the given acp.toml, and
14// whose user-level one is empty.
15func newAgentsApp(t *testing.T, contents string) (*App, string) {
16 t.Helper()
17
18 a, root := newProjectApp(t)
19 t.Setenv(testProfile().DirEnvVar(), t.TempDir())
20
21 if contents != "" {
22 path := acp.ProjectPath(testProfile(), root)
23 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
24 t.Fatalf("making %s: %v", filepath.Dir(path), err)
25 }
26 if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
27 t.Fatalf("writing %s: %v", path, err)
28 }
29 }
30 return a, root
31}
32
33// agentMenuLabels returns what the Agent menu offers.
34func agentMenuLabels(a *App) []string {
35 var out []string
36 for _, item := range a.agentItems() {
37 if item.Separator {
38 continue
39 }
40 out = append(out, ui.PlainLabel(item.Label))
41 }
42 return out
43}
44
45func TestTheAgentMenuIsOnTheBarWithNoAgentsConfigured(t *testing.T) {
46 // It has to be: Create agents file is reachable from nowhere else.
47 a, _ := newAgentsApp(t, "")
48
49 found := false
50 for _, menu := range a.menu.Menus() {
51 if ui.PlainLabel(menu.Label) == "Agent" {
52 found = true
53 }
54 }
55 if !found {
56 t.Fatalf("the bar is %v, with no Agent menu", barLabels(a))
57 }
58
59 labels := agentMenuLabels(a)
60 if len(labels) == 0 || !strings.Contains(labels[0], "no agents") {
61 t.Errorf("the menu offers %v, want it to say there are none", labels)
62 }
63 if !strings.Contains(strings.Join(labels, " "), "Create agents file") {
64 t.Errorf("the menu offers %v, want Create agents file in it", labels)
65 }
66}
67
68func TestTheAgentMenuListsEachConfiguredAgent(t *testing.T) {
69 a, _ := newAgentsApp(t, "[[agent]]\nname=\"Bob\"\ncommand=\"cat\"\n\n[[agent]]\nname=\"Alice\"\ncommand=\"cat\"\n")
70
71 labels := agentMenuLabels(a)
72 joined := strings.Join(labels, " | ")
73 if !strings.Contains(joined, "Bob") || !strings.Contains(joined, "Alice") {
74 t.Errorf("the menu offers %v, want both agents", labels)
75 }
76 if labels[0] != "Bob" {
77 t.Errorf("the menu starts with %q, want the file's own order", labels[0])
78 }
79}
80
81func TestABrokenAgentsFileShowsAsAGreyedLineRatherThanAnEmptyMenu(t *testing.T) {
82 // A silent drop looks exactly like having no agents configured.
83 a, _ := newAgentsApp(t, "[[agent]\nbroken\n")
84
85 labels := agentMenuLabels(a)
86 if len(labels) == 0 || !strings.Contains(labels[0], "error") {
87 t.Errorf("the menu offers %v, want it to say the file is broken", labels)
88 }
89}
90
91func TestTheAgentMenuIsRebuiltFromTheFileEachTimeItOpens(t *testing.T) {
92 // Editing acp.toml must never need a restart, which is the whole reason
93 // the menu has an OnOpen.
94 a, root := newAgentsApp(t, "[[agent]]\nname=\"Bob\"\ncommand=\"cat\"\n")
95
96 path := acp.ProjectPath(testProfile(), root)
97 if err := os.WriteFile(path, []byte("[[agent]]\nname=\"Carol\"\ncommand=\"cat\"\n"), 0o644); err != nil {
98 t.Fatalf("rewriting the file: %v", err)
99 }
100
101 if got := strings.Join(agentMenuLabels(a), " "); !strings.Contains(got, "Carol") {
102 t.Errorf("the menu offers %v after the file changed", got)
103 }
104}
105
106func TestCreateAgentsFileWritesItAndOpensIt(t *testing.T) {
107 a, root := newAgentsApp(t, "")
108
109 a.CreateAgentsFile()
110
111 path := acp.ProjectPath(testProfile(), root)
112 if _, err := os.Stat(path); err != nil {
113 t.Fatalf("the file was not written: %v", err)
114 }
115 if a.desktop.Active() == nil {
116 t.Fatal("the created file was not opened")
117 }
118 if got := a.desktop.Active().Title(); !strings.Contains(got, acp.FileName) {
119 t.Errorf("the window in front is %q, want the agents file", got)
120 }
121 if a.hasNoAgentsFile() {
122 t.Error("hasNoAgentsFile() is still true after one was created")
123 }
124}
125
126func TestTheCreateItemIsGreyedOutOnceThereIsAFile(t *testing.T) {
127 a, _ := newAgentsApp(t, "[[agent]]\nname=\"Bob\"\ncommand=\"cat\"\n")
128
129 for _, item := range a.agentItems() {
130 if ui.PlainLabel(item.Label) != "Create agents file" {
131 continue
132 }
133 if item.Enabled == nil || item.Enabled() {
134 t.Error("Create agents file is offered although the project has one")
135 }
136 return
137 }
138 t.Fatal("the menu has no Create agents file item")
139}
140
141func TestAgentStatusNamesBothFilesAndEveryAgent(t *testing.T) {
142 // This is the answer to "why will my agent not start", so it has to say
143 // what was read and what the command line actually came out as.
144 a, root := newAgentsApp(t, "[[agent]]\nname=\"Bob\"\ncommand=\"docker\"\nargs=[\"agent\",\"serve\",\"acp\"]\n")
145
146 report := a.agentReport()
147 for _, want := range []string{
148 acp.ProjectPath(testProfile(), root),
149 acp.UserPath(testProfile()),
150 "Bob",
151 "docker agent serve acp",
152 } {
153 if !strings.Contains(report, want) {
154 t.Errorf("the status never mentions %q:\n%s", want, report)
155 }
156 }
157}
158
159func TestAgentStatusSaysWhyABrokenFileIsBroken(t *testing.T) {
160 a, _ := newAgentsApp(t, "[[agent]]\nname=\"Bob\"\n")
161
162 if report := a.agentReport(); !strings.Contains(report, "has no command") {
163 t.Errorf("the status does not say what is wrong:\n%s", report)
164 }
165}
166
167func TestOpeningAnAgentThatIsNotConfiguredSaysSo(t *testing.T) {
168 a, _ := newAgentsApp(t, "")
169
170 a.NewAgent("Nobody")
171
172 if a.Modals() == 0 {
173 t.Fatal("nothing was said about an agent that does not exist")
174 }
175 if len(a.agents) != 0 {
176 t.Errorf("a window was opened for an agent that does not exist")
177 }
178}
179
180func TestAnAgentWindowOpensAndClosesWithoutAskingAnything(t *testing.T) {
181 // `cat` is an agent that never answers the handshake, which is exactly the
182 // case that must not hang the editor: the window opens, says it is
183 // starting, and closes cleanly.
184 a, _ := newAgentsApp(t, "[[agent]]\nname=\"Echo\"\ncommand=\"cat\"\n")
185
186 a.NewAgent("Echo")
187
188 if len(a.agents) != 1 {
189 t.Fatalf("%d agent windows are open, want 1", len(a.agents))
190 }
191 window := a.desktop.Active()
192 if window == nil {
193 t.Fatal("no window came to the front")
194 }
195 if !a.isAgentWindow(window) {
196 t.Error("the window is not recognised as an agent's")
197 }
198 if got := window.Title(); !strings.Contains(got, "Echo") {
199 t.Errorf("the window is called %q", got)
200 }
201
202 // Closing asks nothing: a conversation is a running process, not unsaved
203 // work — the same bargain a terminal makes.
204 a.CloseFile()
205
206 if a.Modals() != 0 {
207 t.Error("closing an agent window asked a question")
208 }
209 if len(a.agents) != 0 {
210 t.Errorf("%d agent windows are still open", len(a.agents))
211 }
212}
213
214func TestLeavingTheEditorStopsEveryAgent(t *testing.T) {
215 a, _ := newAgentsApp(t, "[[agent]]\nname=\"Echo\"\ncommand=\"cat\"\n")
216
217 a.NewAgent("Echo")
218 a.NewAgent("Echo")
219 if len(a.agents) != 2 {
220 t.Fatalf("%d windows are open, want two independent conversations", len(a.agents))
221 }
222
223 a.closeAgents()
224
225 if len(a.agents) != 0 {
226 t.Errorf("%d agents survived the editor", len(a.agents))
227 }
228}
229
230func TestAnAgentWindowIsNotTakenForAFile(t *testing.T) {
231 // The file actions must leave it alone: ActiveView is what they all go
232 // through, and a conversation has no buffer.
233 a, _ := newAgentsApp(t, "[[agent]]\nname=\"Echo\"\ncommand=\"cat\"\n")
234
235 a.NewAgent("Echo")
236
237 if a.ActiveView() != nil {
238 t.Error("an agent window was taken for an editing one")
239 }
240 a.SaveFile() // must not panic, and must not open anything
241 if a.Modals() != 0 {
242 t.Error("saving with an agent window in front opened a dialog")
243 }
244}