| 🛟 Updated. 28d5985 k33g 13h ago | 1 | package acp_test |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 📦 Turbo Core f3ade8d k33g 6h ago | 10 | "rickub.com/turbo-editors/turbo-core/acp" |
| 11 | "rickub.com/turbo-editors/turbo-core/profile" |
| 🛟 Updated. 28d5985 k33g 13h ago | 12 | ) |
| 13 | |
| 14 | // testProfile is an editor whose user directory is a temporary one, so a test |
| 15 | // never reads the agents of whoever is running it. |
| 16 | func testProfile(t *testing.T) profile.Profile { |
| 17 | t.Helper() |
| 18 | |
| 19 | p := profile.Profile{Name: "Turbo Test", Slug: "turbo-test", Language: "Test", ToolsMenu: "~T~est"} |
| 20 | t.Setenv(p.DirEnvVar(), t.TempDir()) |
| 21 | return p |
| 22 | } |
| 23 | |
| 24 | // writeProject puts an agents file into a project directory and returns it. |
| 25 | func writeProject(t *testing.T, p profile.Profile, contents string) string { |
| 26 | t.Helper() |
| 27 | |
| 28 | dir := t.TempDir() |
| 29 | path := acp.ProjectPath(p, dir) |
| 30 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| 31 | t.Fatalf("making %s: %v", filepath.Dir(path), err) |
| 32 | } |
| 33 | if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { |
| 34 | t.Fatalf("writing %s: %v", path, err) |
| 35 | } |
| 36 | return dir |
| 37 | } |
| 38 | |
| 39 | // writeUser puts an agents file where the user's own would be. |
| 40 | func writeUser(t *testing.T, p profile.Profile, contents string) { |
| 41 | t.Helper() |
| 42 | |
| 43 | path := acp.UserPath(p) |
| 44 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| 45 | t.Fatalf("making %s: %v", filepath.Dir(path), err) |
| 46 | } |
| 47 | if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { |
| 48 | t.Fatalf("writing %s: %v", path, err) |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | func TestAnAgentIsReadWithEveryKey(t *testing.T) { |
| 53 | p := testProfile(t) |
| 54 | dir := writeProject(t, p, ` |
| 55 | [[agent]] |
| 56 | name = "Bob" |
| 57 | command = "docker" |
| 58 | args = ["agent", "serve", "acp", "./agent.yaml"] |
| 59 | env = { TELEMETRY_ENABLED = "false" } |
| 60 | cwd = "backend" |
| 61 | `) |
| 62 | |
| 63 | list, err := acp.Load(p, dir) |
| 64 | if err != nil { |
| 65 | t.Fatalf("Load() error = %v", err) |
| 66 | } |
| 67 | if list.Len() != 1 { |
| 68 | t.Fatalf("read %d agents, want 1", list.Len()) |
| 69 | } |
| 70 | |
| 71 | agent := list.Agents()[0] |
| 72 | switch { |
| 73 | case agent.Name != "Bob": |
| 74 | t.Errorf("name is %q", agent.Name) |
| 75 | case agent.Command != "docker": |
| 76 | t.Errorf("command is %q", agent.Command) |
| 77 | case strings.Join(agent.Args, " ") != "agent serve acp ./agent.yaml": |
| 78 | t.Errorf("args are %v", agent.Args) |
| 79 | case agent.Env["TELEMETRY_ENABLED"] != "false": |
| 80 | t.Errorf("env is %v", agent.Env) |
| 81 | case agent.Cwd != "backend": |
| 82 | t.Errorf("cwd is %q", agent.Cwd) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func TestEnvMayAlsoBeWrittenAsASubTable(t *testing.T) { |
| 87 | // Both spellings are ordinary TOML and the documentation offers both, so a |
| 88 | // reader who picks the one this test does not cover must not be wrong. |
| 89 | p := testProfile(t) |
| 90 | dir := writeProject(t, p, ` |
| 91 | [[agent]] |
| 92 | name = "Bob" |
| 93 | command = "docker" |
| 94 | |
| 95 | [agent.env] |
| 96 | TOKEN = "secret" |
| 97 | `) |
| 98 | |
| 99 | list, err := acp.Load(p, dir) |
| 100 | if err != nil { |
| 101 | t.Fatalf("Load() error = %v", err) |
| 102 | } |
| 103 | if got := list.Agents()[0].Env["TOKEN"]; got != "secret" { |
| 104 | t.Errorf("env is %v", list.Agents()[0].Env) |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | func TestAProjectsAgentReplacesOneOfYours(t *testing.T) { |
| 109 | // The user's file is your habits and the project's is this repository's, so |
| 110 | // the project's is the more specific statement of the two. The rule is the |
| 111 | // one snippets already follow. |
| 112 | p := testProfile(t) |
| 113 | writeUser(t, p, ` |
| 114 | [[agent]] |
| 115 | name = "Bob" |
| 116 | command = "yours" |
| 117 | |
| 118 | [[agent]] |
| 119 | name = "Everywhere" |
| 120 | command = "kept" |
| 121 | `) |
| 122 | dir := writeProject(t, p, ` |
| 123 | [[agent]] |
| 124 | name = "Bob" |
| 125 | command = "theirs" |
| 126 | |
| 127 | [[agent]] |
| 128 | name = "Only here" |
| 129 | command = "added" |
| 130 | `) |
| 131 | |
| 132 | list, err := acp.Load(p, dir) |
| 133 | if err != nil { |
| 134 | t.Fatalf("Load() error = %v", err) |
| 135 | } |
| 136 | if list.Len() != 3 { |
| 137 | t.Fatalf("read %d agents, want 3: %v", list.Len(), list.Agents()) |
| 138 | } |
| 139 | |
| 140 | bob, _ := list.ByName("Bob") |
| 141 | if bob.Command != "theirs" { |
| 142 | t.Errorf("Bob runs %q, want the project's %q", bob.Command, "theirs") |
| 143 | } |
| 144 | if _, ok := list.ByName("Everywhere"); !ok { |
| 145 | t.Error("the user's other agent was lost") |
| 146 | } |
| 147 | if _, ok := list.ByName("Only here"); !ok { |
| 148 | t.Error("the project's own agent was lost") |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | func TestTheOrderIsTheFilesOrder(t *testing.T) { |
| 153 | // Somebody reordering the file expects the menu to reorder. |
| 154 | p := testProfile(t) |
| 155 | dir := writeProject(t, p, ` |
| 156 | [[agent]] |
| 157 | name = "Third" |
| 158 | command = "c" |
| 159 | |
| 160 | [[agent]] |
| 161 | name = "First" |
| 162 | command = "a" |
| 163 | `) |
| 164 | |
| 165 | list, err := acp.Load(p, dir) |
| 166 | if err != nil { |
| 167 | t.Fatalf("Load() error = %v", err) |
| 168 | } |
| 169 | if got := list.Agents()[0].Name; got != "Third" { |
| 170 | t.Errorf("the first agent is %q, want the file's own first", got) |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | func TestAMissingFileIsNotAnError(t *testing.T) { |
| 175 | p := testProfile(t) |
| 176 | |
| 177 | list, err := acp.Load(p, t.TempDir()) |
| 178 | if err != nil { |
| 179 | t.Fatalf("Load() error = %v", err) |
| 180 | } |
| 181 | if list.Len() != 0 { |
| 182 | t.Errorf("read %d agents from nothing", list.Len()) |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | func TestABrokenFileIsRefusedWholesale(t *testing.T) { |
| 187 | // A half-loaded menu offering three of your five agents is worse than an |
| 188 | // error saying which line is wrong. |
| 189 | p := testProfile(t) |
| 190 | |
| 191 | for _, bad := range []struct { |
| 192 | name string |
| 193 | contents string |
| 194 | says string |
| 195 | }{ |
| 196 | {"no name", "[[agent]]\ncommand = \"docker\"\n", "has no name"}, |
| 197 | {"no command", "[[agent]]\nname = \"Bob\"\n", "has no command"}, |
| 198 | {"two of a name", "[[agent]]\nname=\"Bob\"\ncommand=\"a\"\n\n[[agent]]\nname=\"Bob\"\ncommand=\"b\"\n", "two agents are called"}, |
| 199 | {"a key that is not one", "[[agent]]\nname=\"Bob\"\ncommand=\"a\"\ncomand=\"typo\"\n", "not a key"}, |
| 200 | {"not TOML at all", "[[agent}}\n", "reading"}, |
| 201 | } { |
| 202 | t.Run(bad.name, func(t *testing.T) { |
| 203 | dir := writeProject(t, p, bad.contents) |
| 204 | |
| 205 | list, err := acp.Load(p, dir) |
| 206 | if err == nil { |
| 207 | t.Fatalf("Load() accepted it and read %v", list.Agents()) |
| 208 | } |
| 209 | if !strings.Contains(err.Error(), bad.says) { |
| 210 | t.Errorf("Load() error = %q, want %q in it", err, bad.says) |
| 211 | } |
| 212 | if list.Len() != 0 { |
| 213 | t.Errorf("Load() returned %d agents alongside the error", list.Len()) |
| 214 | } |
| 215 | }) |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | func TestAMisspeltKeyIsRefusedRatherThanIgnored(t *testing.T) { |
| 220 | // This is the one that matters most in practice: `comand` silently ignored |
| 221 | // leaves an agent that cannot start and a file that looks right. |
| 222 | p := testProfile(t) |
| 223 | dir := writeProject(t, p, "[[agent]]\nname=\"Bob\"\ncommand=\"a\"\nenviron={X=\"1\"}\n") |
| 224 | |
| 225 | _, err := acp.Load(p, dir) |
| 226 | if err == nil { |
| 227 | t.Fatal("Load() accepted a key the format does not define") |
| 228 | } |
| 229 | if !strings.Contains(err.Error(), "environ") { |
| 230 | t.Errorf("Load() error = %q, want the offending key named", err) |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | func TestExistsSeesOnlyARegularFile(t *testing.T) { |
| 235 | p := testProfile(t) |
| 236 | |
| 237 | empty := t.TempDir() |
| 238 | if acp.Exists(p, empty) { |
| 239 | t.Error("Exists() is true with no file") |
| 240 | } |
| 241 | |
| 242 | dir := writeProject(t, p, "") |
| 243 | if !acp.Exists(p, dir) { |
| 244 | t.Error("Exists() is false with a file") |
| 245 | } |
| 246 | |
| 247 | // A directory where the file should be is not something Load could read. |
| 248 | inTheWay := t.TempDir() |
| 249 | if err := os.MkdirAll(acp.ProjectPath(p, inTheWay), 0o755); err != nil { |
| 250 | t.Fatalf("making the directory: %v", err) |
| 251 | } |
| 252 | if acp.Exists(p, inTheWay) { |
| 253 | t.Error("Exists() counted a directory as the file") |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | func TestCreateRefusesToOverwrite(t *testing.T) { |
| 258 | p := testProfile(t) |
| 259 | p.Templates.Agents = "# %[1]s and %[2]s\n" |
| 260 | dir := t.TempDir() |
| 261 | |
| 262 | path, err := acp.Create(p, dir) |
| 263 | if err != nil { |
| 264 | t.Fatalf("Create() error = %v", err) |
| 265 | } |
| 266 | if _, err := acp.Create(p, dir); !errors.Is(err, acp.ErrExists) { |
| 267 | t.Errorf("Create() error = %v, want ErrExists", err) |
| 268 | } |
| 269 | |
| 270 | data, err := os.ReadFile(path) |
| 271 | if err != nil { |
| 272 | t.Fatalf("reading %s: %v", path, err) |
| 273 | } |
| 274 | if !strings.Contains(string(data), p.ProjectDir()) { |
| 275 | t.Errorf("the created file is %q, want the project directory in it", data) |
| 276 | } |
| 277 | if !strings.Contains(string(data), acp.UserPath(p)) { |
| 278 | t.Errorf("the created file is %q, want the user's path in it", data) |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | func TestACommandLineReadsAsOne(t *testing.T) { |
| 283 | agent := acp.Agent{Command: "docker", Args: []string{"agent", "serve", "acp", "a.yaml"}} |
| 284 | if got, want := agent.CommandLine(), "docker agent serve acp a.yaml"; got != want { |
| 285 | t.Errorf("CommandLine() = %q, want %q", got, want) |
| 286 | } |
| 287 | if got, want := (acp.Agent{Command: "solo"}).CommandLine(), "solo"; got != want { |
| 288 | t.Errorf("CommandLine() = %q, want %q", got, want) |
| 289 | } |
| 290 | } |