mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
340 lines
8.6 KiB
Go
340 lines
8.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
func TestResolveTool(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
wantName string
|
|
wantNil bool
|
|
}{
|
|
{"claude", "claude", false},
|
|
{"cursor", "agents", false},
|
|
{"codex", "agents", false},
|
|
{"windsurf", "agents", false},
|
|
{"opencode", "agents", false},
|
|
{"agents", "agents", false},
|
|
{"copilot", "copilot", false},
|
|
{"amazon-q", "amazon-q", false},
|
|
{"amazonq", "amazon-q", false},
|
|
{"junie", "junie", false},
|
|
{"CLAUDE", "claude", false},
|
|
{"Cursor", "agents", false},
|
|
{"nonexistent", "", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
tool := ResolveTool(tt.input)
|
|
if tt.wantNil {
|
|
if tool != nil {
|
|
t.Errorf("ResolveTool(%q) = %v, want nil", tt.input, tool)
|
|
}
|
|
return
|
|
}
|
|
if tool == nil {
|
|
t.Fatalf("ResolveTool(%q) = nil, want %q", tt.input, tt.wantName)
|
|
}
|
|
if tool.Name != tt.wantName {
|
|
t.Errorf("ResolveTool(%q).Name = %q, want %q", tt.input, tool.Name, tt.wantName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStripFrontmatter(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{
|
|
name: "with frontmatter",
|
|
input: "---\nname: pad\ndescription: test\n---\n\n# Body\nContent here",
|
|
want: "# Body\nContent here",
|
|
},
|
|
{
|
|
name: "no frontmatter",
|
|
input: "# Just a body\nNo frontmatter here",
|
|
want: "# Just a body\nNo frontmatter here",
|
|
},
|
|
{
|
|
name: "complex frontmatter",
|
|
input: "---\nname: pad\nallowed-tools:\n - Bash\n - Read\n---\n\nBody text",
|
|
want: "Body text",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := string(StripFrontmatter([]byte(tt.input)))
|
|
if got != tt.want {
|
|
t.Errorf("StripFrontmatter() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatForTool(t *testing.T) {
|
|
embedded := []byte(`---
|
|
name: pad
|
|
description: "Test skill"
|
|
allowed-tools:
|
|
- Bash
|
|
---
|
|
|
|
# Pad Skill
|
|
|
|
Body content here.
|
|
`)
|
|
|
|
t.Run("claude returns original", func(t *testing.T) {
|
|
tool := *ResolveTool("claude")
|
|
got := FormatForTool(tool, embedded)
|
|
if string(got) != string(embedded) {
|
|
t.Error("Claude format should return embedded content unchanged")
|
|
}
|
|
})
|
|
|
|
t.Run("agents has name+description frontmatter", func(t *testing.T) {
|
|
tool := *ResolveTool("agents")
|
|
got := string(FormatForTool(tool, embedded))
|
|
if got[:4] != "---\n" {
|
|
t.Error("Agents format should start with frontmatter")
|
|
}
|
|
if !contains(got, "name: pad") {
|
|
t.Error("Agents format should contain name: pad")
|
|
}
|
|
if !contains(got, "description:") {
|
|
t.Error("Agents format should contain description")
|
|
}
|
|
if contains(got, "allowed-tools") {
|
|
t.Error("Agents format should NOT contain allowed-tools")
|
|
}
|
|
if !contains(got, "# Pad Skill") {
|
|
t.Error("Agents format should contain body")
|
|
}
|
|
})
|
|
|
|
t.Run("copilot has applyTo frontmatter", func(t *testing.T) {
|
|
tool := *ResolveTool("copilot")
|
|
got := string(FormatForTool(tool, embedded))
|
|
if !contains(got, "applyTo:") {
|
|
t.Error("Copilot format should contain applyTo")
|
|
}
|
|
if contains(got, "allowed-tools") {
|
|
t.Error("Copilot format should NOT contain allowed-tools")
|
|
}
|
|
})
|
|
|
|
t.Run("junie has no frontmatter", func(t *testing.T) {
|
|
tool := *ResolveTool("junie")
|
|
got := string(FormatForTool(tool, embedded))
|
|
if contains(got, "---") {
|
|
t.Error("Junie format should NOT contain frontmatter delimiters")
|
|
}
|
|
if !contains(got, "# Pad Skill") {
|
|
t.Error("Junie format should contain body")
|
|
}
|
|
})
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr))
|
|
}
|
|
|
|
func containsStr(s, sub string) bool {
|
|
for i := 0; i <= len(s)-len(sub); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isolateEnv points HOME and PATH at empty temp directories and chdirs into
|
|
// an empty temp directory, so DetectTools sees none of this machine's real
|
|
// signals (this dev machine has ~/.codex, ~/.claude, etc. — a naive test
|
|
// would false-pass without this).
|
|
func isolateEnv(t *testing.T) (home, bin string) {
|
|
t.Helper()
|
|
home = t.TempDir()
|
|
bin = t.TempDir()
|
|
cwd := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
t.Setenv("PATH", bin)
|
|
t.Chdir(cwd)
|
|
return home, bin
|
|
}
|
|
|
|
// writeStubBinary creates an executable file named `name` in dir, so
|
|
// exec.LookPath(name) succeeds against it.
|
|
func writeStubBinary(t *testing.T, dir, name string) {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
|
t.Fatalf("write stub binary %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func detectedNames(tools []AgentTool) map[string]bool {
|
|
names := map[string]bool{}
|
|
for _, tool := range tools {
|
|
names[tool.Name] = true
|
|
}
|
|
return names
|
|
}
|
|
|
|
func TestDetectTools(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("stub binaries and HOME semantics differ on windows")
|
|
}
|
|
|
|
t.Run("no signal miss", func(t *testing.T) {
|
|
isolateEnv(t)
|
|
got := DetectTools()
|
|
if len(got) != 0 {
|
|
t.Errorf("DetectTools() = %v, want empty", got)
|
|
}
|
|
})
|
|
|
|
t.Run("home dir hit for agents via ~/.codex", func(t *testing.T) {
|
|
home, _ := isolateEnv(t)
|
|
if err := os.Mkdir(filepath.Join(home, ".codex"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := detectedNames(DetectTools())
|
|
if !names["agents"] {
|
|
t.Errorf("expected agents detected via ~/.codex, got %v", names)
|
|
}
|
|
if names["claude"] {
|
|
t.Errorf("claude should not be detected, got %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("home dir hit for agents via ~/.opencode", func(t *testing.T) {
|
|
home, _ := isolateEnv(t)
|
|
if err := os.Mkdir(filepath.Join(home, ".opencode"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := detectedNames(DetectTools())
|
|
if !names["agents"] {
|
|
t.Errorf("expected agents detected via ~/.opencode, got %v", names)
|
|
}
|
|
if names["claude"] {
|
|
t.Errorf("claude should not be detected, got %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("home dir hit for claude via ~/.claude", func(t *testing.T) {
|
|
home, _ := isolateEnv(t)
|
|
if err := os.Mkdir(filepath.Join(home, ".claude"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := detectedNames(DetectTools())
|
|
if !names["claude"] {
|
|
t.Errorf("expected claude detected via ~/.claude, got %v", names)
|
|
}
|
|
if names["agents"] {
|
|
t.Errorf("agents should not be detected, got %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("binary hit for agents via codex on PATH", func(t *testing.T) {
|
|
_, bin := isolateEnv(t)
|
|
writeStubBinary(t, bin, "codex")
|
|
names := detectedNames(DetectTools())
|
|
if !names["agents"] {
|
|
t.Errorf("expected agents detected via codex binary, got %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("binary hit for agents via opencode on PATH", func(t *testing.T) {
|
|
_, bin := isolateEnv(t)
|
|
writeStubBinary(t, bin, "opencode")
|
|
names := detectedNames(DetectTools())
|
|
if !names["agents"] {
|
|
t.Errorf("expected agents detected via opencode binary, got %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("binary hit for claude via claude on PATH", func(t *testing.T) {
|
|
_, bin := isolateEnv(t)
|
|
writeStubBinary(t, bin, "claude")
|
|
names := detectedNames(DetectTools())
|
|
if !names["claude"] {
|
|
t.Errorf("expected claude detected via claude binary, got %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("project-local dir still works for tools with no machine signal", func(t *testing.T) {
|
|
isolateEnv(t)
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(cwd, ".junie"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := detectedNames(DetectTools())
|
|
if !names["junie"] {
|
|
t.Errorf("expected junie detected via project-local .junie, got %v", names)
|
|
}
|
|
if len(names) != 1 {
|
|
t.Errorf("expected only junie detected, got %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("project-local and home dir both present dedupes to one entry", func(t *testing.T) {
|
|
home, _ := isolateEnv(t)
|
|
if err := os.Mkdir(filepath.Join(home, ".claude"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(cwd, ".claude"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := DetectTools()
|
|
count := 0
|
|
for _, tool := range got {
|
|
if tool.Name == "claude" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected claude detected exactly once, got %d (in %v)", count, got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAllToolNames(t *testing.T) {
|
|
names := AllToolNames()
|
|
// Should include both canonical names and aliases
|
|
expected := map[string]bool{
|
|
"claude": true,
|
|
"agents": true,
|
|
"cursor": true,
|
|
"codex": true,
|
|
"opencode": true,
|
|
"copilot": true,
|
|
"junie": true,
|
|
}
|
|
nameMap := map[string]bool{}
|
|
for _, n := range names {
|
|
nameMap[n] = true
|
|
}
|
|
for name := range expected {
|
|
if !nameMap[name] {
|
|
t.Errorf("AllToolNames() missing %q", name)
|
|
}
|
|
}
|
|
}
|