Files
chenhg5-cc-connect/core/command_test.go
chenhg5 8f630a0538 feat(commands): add support for exec commands with shell execution
Add ability to define custom slash commands that execute shell commands directly:
- Extend CommandConfig with Exec and WorkDir fields
- Add cmdCommandsAddExec handler for exec commands
- Implement executeShellCommand with timeout and output handling
- Add work_dir support for exec commands
- Update documentation and i18n messages
- Add tests and error handling

generated by llmgit

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-07 07:55:24 +08:00

273 lines
6.1 KiB
Go

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)
}
}
}