package core import ( "os" "path/filepath" "testing" ) func TestCommandRegistry_AddAndResolve(t *testing.T) { r := NewCommandRegistry() r.Add("greet", "Say hello", "Hello {{1}}", "", "", "config") cmd, ok := r.Resolve("greet") if !ok { t.Fatal("expected to resolve 'greet'") } if cmd.Name != "greet" { t.Errorf("Name = %q, want greet", cmd.Name) } if cmd.Prompt != "Hello {{1}}" { t.Errorf("Prompt = %q", cmd.Prompt) } } func TestCommandRegistry_CaseInsensitive(t *testing.T) { r := NewCommandRegistry() r.Add("Hello", "test", "prompt", "", "", "config") _, ok := r.Resolve("hello") if !ok { t.Error("resolve should be case-insensitive") } } func TestCommandRegistry_Remove(t *testing.T) { r := NewCommandRegistry() r.Add("tmp", "temp", "prompt", "", "", "config") if !r.Remove("tmp") { t.Error("Remove should return true") } if r.Remove("tmp") { t.Error("second Remove should return false") } if _, ok := r.Resolve("tmp"); ok { t.Error("should not resolve after remove") } } func TestCommandRegistry_ClearSource(t *testing.T) { r := NewCommandRegistry() r.Add("a", "", "", "", "", "config") r.Add("b", "", "", "", "", "config") r.Add("c", "", "", "", "", "agent") r.ClearSource("config") if _, ok := r.Resolve("a"); ok { t.Error("'a' should be cleared") } if _, ok := r.Resolve("c"); !ok { t.Error("'c' from agent source should remain") } } func TestCommandRegistry_AgentDirResolve(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "deploy.md"), []byte("Deploy to production"), 0644) r := NewCommandRegistry() r.SetAgentDirs([]string{dir}) cmd, ok := r.Resolve("deploy") if !ok { t.Fatal("expected to resolve 'deploy' from agent dir") } if cmd.Source != "agent" { t.Errorf("Source = %q, want agent", cmd.Source) } if cmd.Prompt != "Deploy to production" { t.Errorf("Prompt = %q", cmd.Prompt) } } func TestCommandRegistry_ConfigOverridesAgent(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "deploy.md"), []byte("agent prompt"), 0644) r := NewCommandRegistry() r.SetAgentDirs([]string{dir}) r.Add("deploy", "config deploy", "config prompt", "", "", "config") cmd, ok := r.Resolve("deploy") if !ok { t.Fatal("expected to resolve") } if cmd.Source != "config" { t.Errorf("config command should take priority, got source=%q", cmd.Source) } } func TestCommandRegistry_PathTraversal(t *testing.T) { dir := t.TempDir() parent := filepath.Dir(dir) os.WriteFile(filepath.Join(parent, "secret.md"), []byte("secret"), 0644) r := NewCommandRegistry() r.SetAgentDirs([]string{dir}) _, ok := r.Resolve("../secret") if ok { t.Error("path traversal should be blocked") } } func TestCommandRegistry_ListAll(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "build.md"), []byte("Build project"), 0644) r := NewCommandRegistry() r.Add("test", "Run tests", "go test ./...", "", "", "config") r.SetAgentDirs([]string{dir}) all := r.ListAll() if len(all) < 2 { t.Errorf("expected at least 2 commands, got %d", len(all)) } names := map[string]bool{} for _, c := range all { names[c.Name] = true } if !names["test"] { t.Error("missing 'test' command") } if !names["build"] { t.Error("missing 'build' command from agent dir") } } func TestExpandPrompt_NoPlaceholders(t *testing.T) { got := ExpandPrompt("Do the thing", []string{"arg1"}) if got != "Do the thing\n\narg1" { t.Errorf("got %q", got) } } func TestExpandPrompt_NoArgs(t *testing.T) { got := ExpandPrompt("Just a template", nil) if got != "Just a template" { t.Errorf("got %q", got) } } func TestExpandPrompt_Positional(t *testing.T) { got := ExpandPrompt("Find user {{1}} in {{2}}", []string{"john", "prod"}) if got != "Find user john in prod" { t.Errorf("got %q", got) } } func TestExpandPrompt_PositionalDefault(t *testing.T) { got := ExpandPrompt("Deploy to {{1:staging}}", nil) if got != "Deploy to staging" { t.Errorf("got %q", got) } } func TestExpandPrompt_Star(t *testing.T) { got := ExpandPrompt("Search: {{2*}}", []string{"prefix", "foo", "bar", "baz"}) if got != "Search: foo bar baz" { t.Errorf("got %q", got) } } func TestExpandPrompt_Args(t *testing.T) { got := ExpandPrompt("Run with: {{args}}", []string{"a", "b", "c"}) if got != "Run with: a b c" { t.Errorf("got %q", got) } } func TestExpandPrompt_ArgsDefault(t *testing.T) { got := ExpandPrompt("Run with: {{args:defaults}}", nil) if got != "Run with: defaults" { t.Errorf("got %q", got) } } func TestMatchSubCommand(t *testing.T) { candidates := []string{"list", "add", "del", "delete"} tests := []struct { input string want string }{ {"list", "list"}, {"l", "list"}, {"a", "add"}, {"del", "del"}, {"delete", "delete"}, {"d", "d"}, // ambiguous: del, delete {"xyz", "xyz"}, } for _, tt := range tests { got := matchSubCommand(tt.input, candidates) if got != tt.want { t.Errorf("matchSubCommand(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestMatchPrefix(t *testing.T) { candidates := []struct { names []string id string }{ {[]string{"help"}, "help"}, {[]string{"provider", "pro"}, "provider"}, {[]string{"list", "ls"}, "list"}, {[]string{"new"}, "new"}, } tests := []struct { input string want string }{ {"help", "help"}, {"h", "help"}, {"provider", "provider"}, {"pro", "provider"}, {"p", "provider"}, {"list", "list"}, {"ls", "list"}, {"l", "list"}, {"new", "new"}, {"n", "new"}, {"xyz", ""}, } for _, tt := range tests { got := matchPrefix(tt.input, candidates) if got != tt.want { t.Errorf("matchPrefix(%q) = %q, want %q", tt.input, got, tt.want) } } } func TestAllowList(t *testing.T) { tests := []struct { allow string user string expect bool }{ {"", "anyone", true}, {"*", "anyone", true}, {"user1", "user1", true}, {"user1", "USER1", true}, {"user1,user2", "user2", true}, {"user1, user2", "user2", true}, {"user1", "user3", false}, {" user1 , user2 ", "user2", true}, } for _, tt := range tests { got := AllowList(tt.allow, tt.user) if got != tt.expect { t.Errorf("AllowList(%q, %q) = %v, want %v", tt.allow, tt.user, got, tt.expect) } } }