package acp_test import ( "errors" "os" "path/filepath" "strings" "testing" "rickub.com/turbo-editors/turbo-core/acp" "rickub.com/turbo-editors/turbo-core/profile" ) // testProfile is an editor whose user directory is a temporary one, so a test // never reads the agents of whoever is running it. func testProfile(t *testing.T) profile.Profile { t.Helper() p := profile.Profile{Name: "Turbo Test", Slug: "turbo-test", Language: "Test", ToolsMenu: "~T~est"} t.Setenv(p.DirEnvVar(), t.TempDir()) return p } // writeProject puts an agents file into a project directory and returns it. func writeProject(t *testing.T, p profile.Profile, contents string) string { t.Helper() dir := t.TempDir() path := acp.ProjectPath(p, dir) 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 dir } // writeUser puts an agents file where the user's own would be. func writeUser(t *testing.T, p profile.Profile, contents string) { t.Helper() path := acp.UserPath(p) 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) } } func TestAnAgentIsReadWithEveryKey(t *testing.T) { p := testProfile(t) dir := writeProject(t, p, ` [[agent]] name = "Bob" command = "docker" args = ["agent", "serve", "acp", "./agent.yaml"] env = { TELEMETRY_ENABLED = "false" } cwd = "backend" `) list, err := acp.Load(p, dir) if err != nil { t.Fatalf("Load() error = %v", err) } if list.Len() != 1 { t.Fatalf("read %d agents, want 1", list.Len()) } agent := list.Agents()[0] switch { case agent.Name != "Bob": t.Errorf("name is %q", agent.Name) case agent.Command != "docker": t.Errorf("command is %q", agent.Command) case strings.Join(agent.Args, " ") != "agent serve acp ./agent.yaml": t.Errorf("args are %v", agent.Args) case agent.Env["TELEMETRY_ENABLED"] != "false": t.Errorf("env is %v", agent.Env) case agent.Cwd != "backend": t.Errorf("cwd is %q", agent.Cwd) } } func TestEnvMayAlsoBeWrittenAsASubTable(t *testing.T) { // Both spellings are ordinary TOML and the documentation offers both, so a // reader who picks the one this test does not cover must not be wrong. p := testProfile(t) dir := writeProject(t, p, ` [[agent]] name = "Bob" command = "docker" [agent.env] TOKEN = "secret" `) list, err := acp.Load(p, dir) if err != nil { t.Fatalf("Load() error = %v", err) } if got := list.Agents()[0].Env["TOKEN"]; got != "secret" { t.Errorf("env is %v", list.Agents()[0].Env) } } func TestAProjectsAgentReplacesOneOfYours(t *testing.T) { // The user's file is your habits and the project's is this repository's, so // the project's is the more specific statement of the two. The rule is the // one snippets already follow. p := testProfile(t) writeUser(t, p, ` [[agent]] name = "Bob" command = "yours" [[agent]] name = "Everywhere" command = "kept" `) dir := writeProject(t, p, ` [[agent]] name = "Bob" command = "theirs" [[agent]] name = "Only here" command = "added" `) list, err := acp.Load(p, dir) if err != nil { t.Fatalf("Load() error = %v", err) } if list.Len() != 3 { t.Fatalf("read %d agents, want 3: %v", list.Len(), list.Agents()) } bob, _ := list.ByName("Bob") if bob.Command != "theirs" { t.Errorf("Bob runs %q, want the project's %q", bob.Command, "theirs") } if _, ok := list.ByName("Everywhere"); !ok { t.Error("the user's other agent was lost") } if _, ok := list.ByName("Only here"); !ok { t.Error("the project's own agent was lost") } } func TestTheOrderIsTheFilesOrder(t *testing.T) { // Somebody reordering the file expects the menu to reorder. p := testProfile(t) dir := writeProject(t, p, ` [[agent]] name = "Third" command = "c" [[agent]] name = "First" command = "a" `) list, err := acp.Load(p, dir) if err != nil { t.Fatalf("Load() error = %v", err) } if got := list.Agents()[0].Name; got != "Third" { t.Errorf("the first agent is %q, want the file's own first", got) } } func TestAMissingFileIsNotAnError(t *testing.T) { p := testProfile(t) list, err := acp.Load(p, t.TempDir()) if err != nil { t.Fatalf("Load() error = %v", err) } if list.Len() != 0 { t.Errorf("read %d agents from nothing", list.Len()) } } func TestABrokenFileIsRefusedWholesale(t *testing.T) { // A half-loaded menu offering three of your five agents is worse than an // error saying which line is wrong. p := testProfile(t) for _, bad := range []struct { name string contents string says string }{ {"no name", "[[agent]]\ncommand = \"docker\"\n", "has no name"}, {"no command", "[[agent]]\nname = \"Bob\"\n", "has no command"}, {"two of a name", "[[agent]]\nname=\"Bob\"\ncommand=\"a\"\n\n[[agent]]\nname=\"Bob\"\ncommand=\"b\"\n", "two agents are called"}, {"a key that is not one", "[[agent]]\nname=\"Bob\"\ncommand=\"a\"\ncomand=\"typo\"\n", "not a key"}, {"not TOML at all", "[[agent}}\n", "reading"}, } { t.Run(bad.name, func(t *testing.T) { dir := writeProject(t, p, bad.contents) list, err := acp.Load(p, dir) if err == nil { t.Fatalf("Load() accepted it and read %v", list.Agents()) } if !strings.Contains(err.Error(), bad.says) { t.Errorf("Load() error = %q, want %q in it", err, bad.says) } if list.Len() != 0 { t.Errorf("Load() returned %d agents alongside the error", list.Len()) } }) } } func TestAMisspeltKeyIsRefusedRatherThanIgnored(t *testing.T) { // This is the one that matters most in practice: `comand` silently ignored // leaves an agent that cannot start and a file that looks right. p := testProfile(t) dir := writeProject(t, p, "[[agent]]\nname=\"Bob\"\ncommand=\"a\"\nenviron={X=\"1\"}\n") _, err := acp.Load(p, dir) if err == nil { t.Fatal("Load() accepted a key the format does not define") } if !strings.Contains(err.Error(), "environ") { t.Errorf("Load() error = %q, want the offending key named", err) } } func TestExistsSeesOnlyARegularFile(t *testing.T) { p := testProfile(t) empty := t.TempDir() if acp.Exists(p, empty) { t.Error("Exists() is true with no file") } dir := writeProject(t, p, "") if !acp.Exists(p, dir) { t.Error("Exists() is false with a file") } // A directory where the file should be is not something Load could read. inTheWay := t.TempDir() if err := os.MkdirAll(acp.ProjectPath(p, inTheWay), 0o755); err != nil { t.Fatalf("making the directory: %v", err) } if acp.Exists(p, inTheWay) { t.Error("Exists() counted a directory as the file") } } func TestCreateRefusesToOverwrite(t *testing.T) { p := testProfile(t) p.Templates.Agents = "# %[1]s and %[2]s\n" dir := t.TempDir() path, err := acp.Create(p, dir) if err != nil { t.Fatalf("Create() error = %v", err) } if _, err := acp.Create(p, dir); !errors.Is(err, acp.ErrExists) { t.Errorf("Create() error = %v, want ErrExists", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("reading %s: %v", path, err) } if !strings.Contains(string(data), p.ProjectDir()) { t.Errorf("the created file is %q, want the project directory in it", data) } if !strings.Contains(string(data), acp.UserPath(p)) { t.Errorf("the created file is %q, want the user's path in it", data) } } func TestACommandLineReadsAsOne(t *testing.T) { agent := acp.Agent{Command: "docker", Args: []string{"agent", "serve", "acp", "a.yaml"}} if got, want := agent.CommandLine(), "docker agent serve acp a.yaml"; got != want { t.Errorf("CommandLine() = %q, want %q", got, want) } if got, want := (acp.Agent{Command: "solo"}).CommandLine(), "solo"; got != want { t.Errorf("CommandLine() = %q, want %q", got, want) } }