@@ -13,6 +13,7 @@ import (
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/lsp"
15 "github.com/charmbracelet/crush/internal/lsp"
16+ "github.com/charmbracelet/crush/internal/skills"
16 "github.com/stretchr/testify/require"
17 "github.com/stretchr/testify/require"
@@ -22,7 +23,7 @@ func TestCrushInfo_MinimalConfig(t *testing.T) {
22 cfg := config.NewTestStore(&config.Config{
23 cfg := config.NewTestStore(&config.Config{
23 Providers: csync.NewMap[string, config.ProviderConfig](),
24 Providers: csync.NewMap[string, config.ProviderConfig](),
25− output := buildCrushInfo(cfg, nil)
26+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
26 require.NotContains(t, output, "[providers]")
27 require.NotContains(t, output, "[providers]")
27 require.NotContains(t, output, "[lsp]")
28 require.NotContains(t, output, "[lsp]")
28 require.NotContains(t, output, "[mcp]")
29 require.NotContains(t, output, "[mcp]")
@@ -38,7 +39,7 @@ func TestCrushInfo_ConfigFiles(t *testing.T) {
38 "/home/user/.config/crush/crush.json",
39 "/home/user/.config/crush/crush.json",
39 "/project/.crush/crush.json",
40 "/project/.crush/crush.json",
41− output := buildCrushInfo(cfg, nil)
42+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
42 require.Contains(t, output, "[config_files]")
43 require.Contains(t, output, "[config_files]")
43 require.Contains(t, output, "/home/user/.config/crush/crush.json")
44 require.Contains(t, output, "/home/user/.config/crush/crush.json")
44 require.Contains(t, output, "/project/.crush/crush.json")
45 require.Contains(t, output, "/project/.crush/crush.json")
@@ -54,7 +55,7 @@ func TestCrushInfo_Models(t *testing.T) {
55 Providers: csync.NewMap[string, config.ProviderConfig](),
56 Providers: csync.NewMap[string, config.ProviderConfig](),
57− output := buildCrushInfo(cfg, nil)
58+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
58 require.Contains(t, output, "[model]")
59 require.Contains(t, output, "[model]")
59 require.Contains(t, output, "large = claude-sonnet-4-20250514 (anthropic)")
60 require.Contains(t, output, "large = claude-sonnet-4-20250514 (anthropic)")
60 require.Contains(t, output, "small = claude-haiku-3-20250307 (anthropic)")
61 require.Contains(t, output, "small = claude-haiku-3-20250307 (anthropic)")
@@ -68,7 +69,7 @@ func TestCrushInfo_Providers(t *testing.T) {
68 providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
69 providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
70 cfg := config.NewTestStore(&config.Config{Providers: providers})
71 cfg := config.NewTestStore(&config.Config{Providers: providers})
71− output := buildCrushInfo(cfg, nil)
72+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
72 require.Contains(t, output, "[providers]")
73 require.Contains(t, output, "[providers]")
73 anthropicIdx := strings.Index(output, "anthropic = enabled")
74 anthropicIdx := strings.Index(output, "anthropic = enabled")
74 openaiIdx := strings.Index(output, "openai = enabled")
75 openaiIdx := strings.Index(output, "openai = enabled")
@@ -87,7 +88,7 @@ func TestCrushInfo_DisabledProvidersOmitted(t *testing.T) {
87 providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
88 providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
89 cfg := config.NewTestStore(&config.Config{Providers: providers})
90 cfg := config.NewTestStore(&config.Config{Providers: providers})
90− output := buildCrushInfo(cfg, nil)
91+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
91 require.Contains(t, output, "anthropic = enabled")
92 require.Contains(t, output, "anthropic = enabled")
92 require.NotContains(t, output, "openai")
93 require.NotContains(t, output, "openai")
@@ -107,7 +108,7 @@ func TestCrushInfo_LSPStates(t *testing.T) {
107 mgr.Clients().Set("pyright", errorClient)
108 mgr.Clients().Set("pyright", errorClient)
109 cfg := config.NewTestStore(&config.Config{Providers: csync.NewMap[string, config.ProviderConfig]()})
110 cfg := config.NewTestStore(&config.Config{Providers: csync.NewMap[string, config.ProviderConfig]()})
110− output := buildCrushInfo(cfg, mgr)
111+ output := buildCrushInfo(cfg, mgr, nil, nil, nil)
111 require.Contains(t, output, "[lsp]")
112 require.Contains(t, output, "[lsp]")
112 require.Contains(t, output, "gopls = ready")
113 require.Contains(t, output, "gopls = ready")
113 require.Contains(t, output, "pyright = error")
114 require.Contains(t, output, "pyright = error")
@@ -158,7 +159,7 @@ func TestCrushInfo_YoloMode(t *testing.T) {
159 cfg.Overrides().SkipPermissionRequests = true
160 cfg.Overrides().SkipPermissionRequests = true
161− output := buildCrushInfo(cfg, nil)
162+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
162 require.Contains(t, output, "[permissions]")
163 require.Contains(t, output, "[permissions]")
163 require.Contains(t, output, "mode = yolo")
164 require.Contains(t, output, "mode = yolo")
@@ -171,7 +172,7 @@ func TestCrushInfo_AllowedTools(t *testing.T) {
171 Permissions: &config.Permissions{AllowedTools: []string{"edit:write", "bash"}},
172 Permissions: &config.Permissions{AllowedTools: []string{"edit:write", "bash"}},
174− output := buildCrushInfo(cfg, nil)
175+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
175 require.Contains(t, output, "[permissions]")
176 require.Contains(t, output, "[permissions]")
176 require.Contains(t, output, "allowed_tools = bash, edit:write")
177 require.Contains(t, output, "allowed_tools = bash, edit:write")
@@ -184,7 +185,7 @@ func TestCrushInfo_DisabledTools(t *testing.T) {
184 Options: &config.Options{DisabledTools: []string{"sourcegraph", "agentic_fetch"}},
185 Options: &config.Options{DisabledTools: []string{"sourcegraph", "agentic_fetch"}},
187− output := buildCrushInfo(cfg, nil)
188+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
188 require.Contains(t, output, "[tools]")
189 require.Contains(t, output, "[tools]")
189 require.Contains(t, output, "disabled = agentic_fetch, sourcegraph")
190 require.Contains(t, output, "disabled = agentic_fetch, sourcegraph")
@@ -201,7 +202,7 @@ func TestCrushInfo_Options(t *testing.T) {
204− output := buildCrushInfo(cfg, nil)
205+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
205 require.Contains(t, output, "[options]")
206 require.Contains(t, output, "[options]")
206 require.Contains(t, output, "auto_lsp = true")
207 require.Contains(t, output, "auto_lsp = true")
207 require.Contains(t, output, "auto_summarize = false")
208 require.Contains(t, output, "auto_summarize = false")
@@ -216,14 +217,14 @@ func TestCrushInfo_AutoSummarizeInversion(t *testing.T) {
216 Providers: csync.NewMap[string, config.ProviderConfig](),
217 Providers: csync.NewMap[string, config.ProviderConfig](),
217 Options: &config.Options{DisableAutoSummarize: true},
218 Options: &config.Options{DisableAutoSummarize: true},
219− outputFalse := buildCrushInfo(cfgFalse, nil)
220+ outputFalse := buildCrushInfo(cfgFalse, nil, nil, nil, nil)
220 require.Contains(t, outputFalse, "auto_summarize = false")
221 require.Contains(t, outputFalse, "auto_summarize = false")
222 cfgTrue := config.NewTestStore(&config.Config{
223 cfgTrue := config.NewTestStore(&config.Config{
223 Providers: csync.NewMap[string, config.ProviderConfig](),
224 Providers: csync.NewMap[string, config.ProviderConfig](),
224 Options: &config.Options{DisableAutoSummarize: false},
225 Options: &config.Options{DisableAutoSummarize: false},
226− outputTrue := buildCrushInfo(cfgTrue, nil)
227+ outputTrue := buildCrushInfo(cfgTrue, nil, nil, nil, nil)
227 require.Contains(t, outputTrue, "auto_summarize = true")
228 require.Contains(t, outputTrue, "auto_summarize = true")
@@ -237,7 +238,7 @@ func TestCrushInfo_NoSecrets(t *testing.T) {
239 cfg := config.NewTestStore(&config.Config{Providers: providers})
240 cfg := config.NewTestStore(&config.Config{Providers: providers})
240− output := buildCrushInfo(cfg, nil)
241+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
241 require.NotContains(t, output, "sk-super-secret-key-12345")
242 require.NotContains(t, output, "sk-super-secret-key-12345")
242 require.NotContains(t, output, "secret")
243 require.NotContains(t, output, "secret")
243 require.Contains(t, output, "openai = enabled (8 models)")
244 require.Contains(t, output, "openai = enabled (8 models)")
@@ -273,7 +274,7 @@ func TestCrushInfo_DeterministicOrdering(t *testing.T) {
273 zMcpIdx := strings.Index(mcpOutput, "z-mcp = connected")
274 zMcpIdx := strings.Index(mcpOutput, "z-mcp = connected")
274 require.Less(t, aMcpIdx, zMcpIdx)
275 require.Less(t, aMcpIdx, zMcpIdx)
276− output := buildCrushInfo(cfg, nil)
277+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
278 alphaIdx := strings.Index(output, "alpha = enabled")
279 alphaIdx := strings.Index(output, "alpha = enabled")
279 middleIdx := strings.Index(output, "middle = enabled")
280 middleIdx := strings.Index(output, "middle = enabled")
@@ -294,11 +295,12 @@ func TestCrushInfo_EmptySectionsOmitted(t *testing.T) {
294 Options: &config.Options{},
295 Options: &config.Options{},
297− output := buildCrushInfo(cfg, nil)
298+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
298 require.NotContains(t, output, "[tools]")
299 require.NotContains(t, output, "[tools]")
299 require.NotContains(t, output, "[permissions]")
300 require.NotContains(t, output, "[permissions]")
300 require.NotContains(t, output, "[lsp]")
301 require.NotContains(t, output, "[lsp]")
301 require.NotContains(t, output, "[mcp]")
302 require.NotContains(t, output, "[mcp]")
303+ require.NotContains(t, output, "[skills]")
304func TestCrushInfo_ConfigStaleness_Clean(t *testing.T) {
306func TestCrushInfo_ConfigStaleness_Clean(t *testing.T) {
@@ -315,7 +317,7 @@ func TestCrushInfo_ConfigStaleness_Clean(t *testing.T) {
315 // Capture snapshot (normally done in Load)
317 // Capture snapshot (normally done in Load)
316 store.CaptureStalenessSnapshot([]string{configPath})
318 store.CaptureStalenessSnapshot([]string{configPath})
318− output := buildCrushInfo(store, nil)
320+ output := buildCrushInfo(store, nil, nil, nil, nil)
319 require.Contains(t, output, "[config]")
321 require.Contains(t, output, "[config]")
320 require.Contains(t, output, "dirty = false")
322 require.Contains(t, output, "dirty = false")
321 require.NotContains(t, output, "changed_paths")
323 require.NotContains(t, output, "changed_paths")
@@ -340,7 +342,7 @@ func TestCrushInfo_ConfigStaleness_Dirty(t *testing.T) {
340 time.Sleep(10 * time.Millisecond)
342 time.Sleep(10 * time.Millisecond)
341 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": true}`), 0o600))
343 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": true}`), 0o600))
343− output := buildCrushInfo(store, nil)
345+ output := buildCrushInfo(store, nil, nil, nil, nil)
344 require.Contains(t, output, "[config]")
346 require.Contains(t, output, "[config]")
345 require.Contains(t, output, "dirty = true")
347 require.Contains(t, output, "dirty = true")
346 require.Contains(t, output, "changed_paths")
348 require.Contains(t, output, "changed_paths")
@@ -364,9 +366,110 @@ func TestCrushInfo_ConfigStaleness_MissingPath(t *testing.T) {
364 // Delete file to trigger missing state
366 // Delete file to trigger missing state
365 require.NoError(t, os.Remove(configPath))
367 require.NoError(t, os.Remove(configPath))
367− output := buildCrushInfo(store, nil)
369+ output := buildCrushInfo(store, nil, nil, nil, nil)
368 require.Contains(t, output, "[config]")
370 require.Contains(t, output, "[config]")
369 require.Contains(t, output, "dirty = true")
371 require.Contains(t, output, "dirty = true")
370 require.Contains(t, output, "missing_paths")
372 require.Contains(t, output, "missing_paths")
371 require.Contains(t, output, configPath)
373 require.Contains(t, output, configPath)
376+func TestCrushInfo_Skills_NoSkills(t *testing.T) {
379+ cfg := config.NewTestStore(&config.Config{
380+ Providers: csync.NewMap[string, config.ProviderConfig](),
382+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
383+ require.NotContains(t, output, "[skills]")
386+func TestCrushInfo_Skills_MixedLoadedUnloaded(t *testing.T) {
389+ allSkills := []*skills.Skill{
390+ {Name: "go-doc", Builtin: false},
391+ {Name: "bash", Builtin: false},
392+ {Name: "crush-config", Builtin: true},
394+ activeSkills := allSkills
396+ tracker := skills.NewTracker(activeSkills)
397+ tracker.MarkLoaded("bash")
398+ tracker.MarkLoaded("crush-config")
400+ cfg := config.NewTestStore(&config.Config{
401+ Providers: csync.NewMap[string, config.ProviderConfig](),
403+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
404+ require.Contains(t, output, "[skills]")
405+ require.Contains(t, output, "bash = user, loaded")
406+ require.Contains(t, output, "crush-config = builtin, loaded")
407+ require.Contains(t, output, "go-doc = user, unloaded")
410+func TestCrushInfo_Skills_DisabledSkills(t *testing.T) {
413+ allSkills := []*skills.Skill{
414+ {Name: "bash", Builtin: false},
415+ {Name: "crush-config", Builtin: true},
416+ {Name: "image-convert", Builtin: false},
418+ activeSkills := []*skills.Skill{
419+ {Name: "bash", Builtin: false},
420+ {Name: "crush-config", Builtin: true},
423+ tracker := skills.NewTracker(activeSkills)
425+ cfg := config.NewTestStore(&config.Config{
426+ Providers: csync.NewMap[string, config.ProviderConfig](),
427+ Options: &config.Options{DisabledSkills: []string{"image-convert"}},
429+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
430+ require.Contains(t, output, "[skills]")
431+ require.Contains(t, output, "bash = user, unloaded")
432+ require.Contains(t, output, "crush-config = builtin, unloaded")
433+ require.Contains(t, output, "image-convert = user, disabled")
436+func TestCrushInfo_Skills_Ordering(t *testing.T) {
439+ allSkills := []*skills.Skill{
440+ {Name: "z-skill", Builtin: false},
441+ {Name: "a-skill", Builtin: true},
442+ {Name: "m-skill", Builtin: false},
444+ activeSkills := allSkills
445+ tracker := skills.NewTracker(activeSkills)
447+ cfg := config.NewTestStore(&config.Config{
448+ Providers: csync.NewMap[string, config.ProviderConfig](),
450+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
452+ aIdx := strings.Index(output, "a-skill")
453+ mIdx := strings.Index(output, "m-skill")
454+ zIdx := strings.Index(output, "z-skill")
455+ require.Less(t, aIdx, mIdx)
456+ require.Less(t, mIdx, zIdx)
459+func TestCrushInfo_Skills_BuiltinOrigin(t *testing.T) {
462+ allSkills := []*skills.Skill{
463+ {Name: "crush-config", Builtin: true},
464+ {Name: "my-skill", Builtin: false},
466+ activeSkills := allSkills
467+ tracker := skills.NewTracker(activeSkills)
469+ cfg := config.NewTestStore(&config.Config{
470+ Providers: csync.NewMap[string, config.ProviderConfig](),
472+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
473+ require.Contains(t, output, "crush-config = builtin, unloaded")
474+ require.Contains(t, output, "my-skill = user, unloaded")