1package commands
2
3import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "testing"
8
9 "github.com/charmbracelet/crush/internal/skills"
10 "github.com/stretchr/testify/require"
11)
12
13func TestLoadFromSource_NonExistentDir(t *testing.T) {
14 t.Parallel()
15
16 dir := filepath.Join(t.TempDir(), "does-not-exist")
17
18 cmds, err := loadFromSource(commandSource{path: dir, prefix: userCommandPrefix})
19 require.NoError(t, err)
20 require.Empty(t, cmds)
21
22 // directory must NOT have been created
23 _, statErr := os.Stat(dir)
24 require.True(t, os.IsNotExist(statErr))
25}
26
27func TestLoadFromSource_ExistingDir(t *testing.T) {
28 t.Parallel()
29
30 dir := t.TempDir()
31 require.NoError(t, os.WriteFile(filepath.Join(dir, "hello.md"), []byte("say hello"), 0o644))
32
33 cmds, err := loadFromSource(commandSource{path: dir, prefix: userCommandPrefix})
34 require.NoError(t, err)
35 require.Len(t, cmds, 1)
36 require.Equal(t, "user:hello", cmds[0].ID)
37 require.Equal(t, "say hello", cmds[0].Content)
38}
39
40func TestLoadAll_MixedSources(t *testing.T) {
41 t.Parallel()
42
43 existing := t.TempDir()
44 require.NoError(t, os.WriteFile(filepath.Join(existing, "cmd.md"), []byte("content"), 0o644))
45
46 missing := filepath.Join(t.TempDir(), "nope")
47
48 cmds, err := loadAll([]commandSource{
49 {path: existing, prefix: userCommandPrefix},
50 {path: missing, prefix: projectCommandPrefix},
51 })
52 require.NoError(t, err)
53 require.Len(t, cmds, 1)
54 require.Equal(t, "user:cmd", cmds[0].ID)
55}
56
57func TestFromSkillCatalog_UserInvocableOnly(t *testing.T) {
58 t.Parallel()
59
60 cmds := FromSkillCatalog([]skills.CatalogEntry{
61 {
62 ID: "/skills/on/SKILL.md",
63 Name: "on",
64 Description: "Enabled.",
65 Label: "user:on",
66 UserInvocable: true,
67 },
68 {
69 ID: "/skills/off/SKILL.md",
70 Name: "off",
71 Description: "Not invocable.",
72 Label: "user:off",
73 UserInvocable: false,
74 },
75 })
76
77 require.Len(t, cmds, 1)
78 require.Equal(t, "user:on", cmds[0].ID)
79 require.Equal(t, "user:on", cmds[0].Name)
80 require.Equal(t, "on", cmds[0].Skill.Name)
81 require.Equal(t, "Enabled.", cmds[0].Skill.Description)
82 require.Equal(t, "/skills/on/SKILL.md", cmds[0].Skill.SkillFilePath)
83}
84
85func TestFromSkillCatalog_UsesDiscoveredSymlinkedSkills(t *testing.T) {
86 if runtime.GOOS == "windows" {
87 t.Skip("symlink creation requires special privileges on Windows")
88 }
89 t.Parallel()
90
91 root := t.TempDir()
92 targetParent := t.TempDir()
93 targetSkillDir := filepath.Join(targetParent, "linked-skill")
94 require.NoError(t, os.MkdirAll(targetSkillDir, 0o755))
95 require.NoError(t, os.WriteFile(
96 filepath.Join(targetSkillDir, skills.SkillFileName),
97 []byte("---\nname: linked-skill\ndescription: Symlinked.\nuser-invocable: true\n---\nUse me.\n"),
98 0o644,
99 ))
100
101 link := filepath.Join(root, "linked-skill")
102 require.NoError(t, os.Symlink(targetSkillDir, link))
103
104 _, activeSkills, _ := skills.DiscoverFromConfig(skills.DiscoveryConfig{
105 SkillsPaths: []string{root},
106 })
107 entries := skills.Catalog(activeSkills, []string{root}, "")
108 cmds := FromSkillCatalog(entries)
109
110 require.Len(t, cmds, 1)
111 require.Equal(t, "user:linked-skill", cmds[0].ID)
112 require.Equal(t, "linked-skill", cmds[0].Skill.Name)
113 require.Equal(t, filepath.Join(link, skills.SkillFileName), cmds[0].Skill.SkillFilePath)
114}