package skills import ( "context" "os" "path/filepath" "strings" "testing" ) func TestLoadDirMissingReturnsEmpty(t *testing.T) { dir := filepath.Join(t.TempDir(), "missing") skills, err := LoadDir(dir) if err != nil { t.Fatalf("LoadDir(missing) returned error: %v", err) } if len(skills) != 0 { t.Fatalf("expected 0 skills, got %d", len(skills)) } } func TestLoadDirRejectsFilePath(t *testing.T) { file := filepath.Join(t.TempDir(), "not-a-dir") if err := os.WriteFile(file, []byte("x"), 0o644); err != nil { t.Fatal(err) } _, err := LoadDir(file) if err == nil { t.Fatal("expected error for file path") } if !strings.Contains(err.Error(), "skills path is not a directory") { t.Fatalf("unexpected error: %v", err) } } func TestLoadDirLoadsDirectorySkillsSorted(t *testing.T) { dir := t.TempDir() // b-skill loads second by name. bDir := filepath.Join(dir, "b-skill") if err := os.MkdirAll(bDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(bDir, SkillFileName), []byte("---\nname: b-skill\ndescription: Beta skill\n---\n\nBeta content.\n"), 0o644); err != nil { t.Fatal(err) } // a-skill loads first by name. aDir := filepath.Join(dir, "a-skill") if err := os.MkdirAll(aDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(aDir, SkillFileName), []byte("---\nname: a-skill\ndescription: Alpha skill\n---\n\nAlpha content.\n"), 0o644); err != nil { t.Fatal(err) } // Visible directory without SKILL.md — ignored. noSkillDir := filepath.Join(dir, "no-skill-here") if err := os.MkdirAll(noSkillDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(noSkillDir, "README.md"), []byte("not a skill"), 0o644); err != nil { t.Fatal(err) } skills, err := LoadDir(dir) if err != nil { t.Fatalf("LoadDir: %v", err) } if len(skills) != 2 { t.Fatalf("expected 2 skills, got %d: %+v", len(skills), skills) } if skills[0].Name != "a-skill" { t.Fatalf("skills[0].Name = %q", skills[0].Name) } if skills[0].Description != "Alpha skill" { t.Fatalf("skills[0].Description = %q", skills[0].Description) } if skills[0].Content != "Alpha content." { t.Fatalf("skills[0].Content = %q", skills[0].Content) } if !strings.HasSuffix(skills[0].Path, SkillFileName) { t.Fatalf("skills[0].Path = %q, want suffix %s", skills[0].Path, SkillFileName) } if skills[1].Name != "b-skill" { t.Fatalf("skills[1].Name = %q", skills[1].Name) } if skills[1].Description != "Beta skill" { t.Fatalf("skills[1].Description = %q", skills[1].Description) } } func TestLoadDirKeepsLegacyMarkdownSkills(t *testing.T) { dir := t.TempDir() write := func(name, content string) { if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { t.Fatal(err) } } write("legacy.md", " legacy content ") write(".hidden.md", "hidden") write("notes.txt", "not a skill") write("empty.md", " \n ") skills, err := LoadDir(dir) if err != nil { t.Fatalf("LoadDir: %v", err) } if len(skills) != 1 { t.Fatalf("expected 1 skill, got %d: %+v", len(skills), skills) } if skills[0].Name != "legacy" { t.Fatalf("skill.Name = %q", skills[0].Name) } if skills[0].Content != "legacy content" { t.Fatalf("skill.Content = %q", skills[0].Content) } } func TestInstallDirInstallsMarkdownFileAsSkillDirectory(t *testing.T) { sourceDir := t.TempDir() targetRoot := filepath.Join(t.TempDir(), "skills") sourcePath := filepath.Join(sourceDir, "source.md") sourceContent := "---\nname: uiux-pro-max\ndescription: UI workflow\n---\n\nReview the UI carefully.\n" if err := os.WriteFile(sourcePath, []byte(sourceContent), 0o644); err != nil { t.Fatal(err) } result, err := InstallDir(context.Background(), targetRoot, sourcePath) if err != nil { t.Fatalf("InstallDir: %v", err) } absRoot, _ := filepath.Abs(targetRoot) wantDir := filepath.Join(absRoot, "uiux-pro-max") if result.TargetDir != wantDir { t.Fatalf("TargetDir = %q, want %q", result.TargetDir, wantDir) } skillFile := filepath.Join(wantDir, SkillFileName) if _, err := os.Stat(skillFile); err != nil { t.Fatalf("SKILL.md not created: %v", err) } loaded, err := LoadDir(targetRoot) if err != nil { t.Fatalf("LoadDir: %v", err) } if len(loaded) != 1 { t.Fatalf("expected 1 loaded skill, got %d", len(loaded)) } if loaded[0].Name != "uiux-pro-max" { t.Fatalf("loaded name = %q", loaded[0].Name) } if loaded[0].Content != "Review the UI carefully." { t.Fatalf("loaded content = %q", loaded[0].Content) } } func TestInstallDirCopiesDirectorySkill(t *testing.T) { sourceDir := t.TempDir() targetRoot := filepath.Join(t.TempDir(), "skills") skillSrc := filepath.Join(sourceDir, "grill-me") if err := os.MkdirAll(filepath.Join(skillSrc, "references"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(skillSrc, SkillFileName), []byte("---\nname: grill-me\ndescription: Grill skill\n---\n\nGrill content.\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(skillSrc, "references", "guide.md"), []byte("# Guide\n"), 0o644); err != nil { t.Fatal(err) } result, err := InstallDir(context.Background(), targetRoot, skillSrc) if err != nil { t.Fatalf("InstallDir: %v", err) } skillFile := filepath.Join(result.TargetDir, SkillFileName) if _, err := os.Stat(skillFile); err != nil { t.Fatalf("SKILL.md not copied: %v", err) } guideFile := filepath.Join(result.TargetDir, "references", "guide.md") if _, err := os.Stat(guideFile); err != nil { t.Fatalf("references/guide.md not copied: %v", err) } loaded, err := LoadDir(targetRoot) if err != nil { t.Fatalf("LoadDir: %v", err) } if len(loaded) != 1 || loaded[0].Name != "grill-me" { t.Fatalf("loaded = %+v", loaded) } } func TestInstallDirRejectsExistingSkill(t *testing.T) { targetRoot := filepath.Join(t.TempDir(), "skills") existingDir := filepath.Join(targetRoot, "grill-me") if err := os.MkdirAll(existingDir, 0o755); err != nil { t.Fatal(err) } sourceDir := t.TempDir() sourcePath := filepath.Join(sourceDir, "source.md") if err := os.WriteFile(sourcePath, []byte("---\nname: grill-me\ndescription: d\n---\n\nbody\n"), 0o644); err != nil { t.Fatal(err) } _, err := InstallDir(context.Background(), targetRoot, sourcePath) if err == nil { t.Fatal("expected error for existing skill") } if !strings.Contains(err.Error(), "skill already exists") { t.Fatalf("unexpected error: %v", err) } } func TestInstallDirRejectsInvalidName(t *testing.T) { targetRoot := filepath.Join(t.TempDir(), "skills") sourceDir := t.TempDir() sourcePath := filepath.Join(sourceDir, "source.md") if err := os.WriteFile(sourcePath, []byte("---\nname: ../bad\ndescription: d\n---\n\nbody\n"), 0o644); err != nil { t.Fatal(err) } _, err := InstallDir(context.Background(), targetRoot, sourcePath) if err == nil { t.Fatal("expected error for invalid name") } if !strings.Contains(err.Error(), "invalid skill name") { t.Fatalf("unexpected error: %v", err) } } func TestInstallDirRequiresSource(t *testing.T) { targetRoot := filepath.Join(t.TempDir(), "skills") _, err := InstallDir(context.Background(), targetRoot, "") if err == nil { t.Fatal("expected error for empty source") } if !strings.Contains(err.Error(), "skill source is required") { t.Fatalf("unexpected error: %v", err) } } func TestInstallDirRejectsBadSourceType(t *testing.T) { targetRoot := filepath.Join(t.TempDir(), "skills") sourceDir := t.TempDir() sourcePath := filepath.Join(sourceDir, "something.yaml") if err := os.WriteFile(sourcePath, []byte("key: value\n"), 0o644); err != nil { t.Fatal(err) } _, err := InstallDir(context.Background(), targetRoot, sourcePath) if err == nil { t.Fatal("expected error for non-.md non-dir source") } if !strings.Contains(err.Error(), "skill source must be a directory") { t.Fatalf("unexpected error: %v", err) } } func TestInstallDirWithoutFrontmatterCreatesNormalizedHeader(t *testing.T) { targetRoot := filepath.Join(t.TempDir(), "skills") sourceDir := t.TempDir() sourcePath := filepath.Join(sourceDir, "my-skill.md") if err := os.WriteFile(sourcePath, []byte("Just body content.\n"), 0o644); err != nil { t.Fatal(err) } result, err := InstallDir(context.Background(), targetRoot, sourcePath) if err != nil { t.Fatalf("InstallDir: %v", err) } if result.Skill.Name != "my-skill" { t.Fatalf("name = %q", result.Skill.Name) } installed, err := os.ReadFile(filepath.Join(result.TargetDir, SkillFileName)) if err != nil { t.Fatal(err) } content := string(installed) if !strings.HasPrefix(content, "---\nname: my-skill\n") { t.Fatalf("installed SKILL.md missing normalized header: %q", content) } if !strings.Contains(content, "Just body content.") { t.Fatalf("installed SKILL.md missing body: %q", content) } } func TestFormatForSystemPromptIncludesDescription(t *testing.T) { items := []Skill{ {Name: "first", Path: "/tmp/first.md", Content: "skill one"}, {Name: "second", Description: "Described", Path: "/tmp/second.md", Content: "skill two"}, } out := FormatForSystemPrompt(items) if out == "" { t.Fatal("FormatForSystemPrompt returned empty string") } if !strings.Contains(out, "Global skills:") { t.Fatal("missing 'Global skills:' header") } if !strings.Contains(out, "### first") { t.Fatal("missing '### first'") } if !strings.Contains(out, "### second") { t.Fatal("missing '### second'") } if !strings.Contains(out, "Source: /tmp/first.md") { t.Fatal("missing source for first") } if !strings.Contains(out, "Source: /tmp/second.md") { t.Fatal("missing source for second") } if !strings.Contains(out, "skill one") { t.Fatal("missing content for first") } if !strings.Contains(out, "skill two") { t.Fatal("missing content for second") } // First skill has no description — the Description: line must be absent. firstBlock := out[:strings.Index(out, "### second")] if strings.Contains(firstBlock, "Description:") { t.Fatal("first skill should not have Description line") } // Second skill has description — the line must appear. if !strings.Contains(out, "Description: Described") { t.Fatal("missing Description line for second skill") } } func TestFormatForSystemPromptEmptyReturnsEmpty(t *testing.T) { if got := FormatForSystemPrompt(nil); got != "" { t.Fatalf("expected empty string, got %q", got) } } func TestFormatListNonEmpty(t *testing.T) { items := []Skill{ {Name: "alpha", Description: "Alpha desc", Path: "/tmp/alpha/SKILL.md", Content: "..."}, {Name: "beta", Path: "/tmp/beta/SKILL.md", Content: "..."}, } out := FormatList(items) if !strings.Contains(out, "Global skills:") { t.Fatal("missing header") } if !strings.Contains(out, "- alpha — Alpha desc") { t.Fatal("missing alpha with description") } if !strings.Contains(out, " Source: /tmp/alpha/SKILL.md") { t.Fatal("missing alpha source") } if !strings.Contains(out, "- beta\n") { t.Fatal("missing beta without description") } // Ensure beta has no em-dash line. if strings.Contains(out, "- beta —") { t.Fatal("beta should not have description separator") } } func TestFormatListEmpty(t *testing.T) { out := FormatList(nil) if out != "No global skills installed." { t.Fatalf("unexpected: %q", out) } } func TestLoadDirRejectsInvalidFrontmatterName(t *testing.T) { dir := t.TempDir() skillDir := filepath.Join(dir, "bad-skill") if err := os.MkdirAll(skillDir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(skillDir, SkillFileName), []byte("---\nname: ../bad\n---\n\nbody\n"), 0o644); err != nil { t.Fatal(err) } _, err := LoadDir(dir) if err == nil { t.Fatal("expected error for invalid frontmatter name") } if !strings.Contains(err.Error(), "invalid skill name") { t.Fatalf("unexpected error: %v", err) } } func TestParseSkillFileMissingClosingDelimiter(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "bad.md") _, _, _, _, err := parseSkillFile(path, []byte("---\nname: foo\nbody\n")) if err == nil { t.Fatal("expected error for missing closing ---") } if !strings.Contains(err.Error(), "missing closing ---") { t.Fatalf("unexpected error: %v", err) } }