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