155 lines
4.9 KiB
Go
155 lines
4.9 KiB
Go
|
|
package skills
|
||
|
|
|
||
|
|
import (
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestInstallUninstallIdempotent(t *testing.T) {
|
||
|
|
project := t.TempDir()
|
||
|
|
|
||
|
|
if err := Install(project, "fd-engineering"); err != nil {
|
||
|
|
t.Fatalf("install: %v", err)
|
||
|
|
}
|
||
|
|
// 幂等:重复安装不报错。
|
||
|
|
if err := Install(project, "fd-engineering"); err != nil {
|
||
|
|
t.Fatalf("reinstall: %v", err)
|
||
|
|
}
|
||
|
|
if !IsInstalled(project, "fd-engineering") {
|
||
|
|
t.Fatal("expected installed after Install")
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(filepath.Join(project, ".cursor", "skills", "fd-engineering", "SKILL.md")); err != nil {
|
||
|
|
t.Fatalf("SKILL.md missing: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(filepath.Join(project, ".cursor", "skills", "fd-engineering", "references", "execute.md")); err != nil {
|
||
|
|
t.Fatalf("references missing: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 命令文件应分发到 .cursor/commands/。
|
||
|
|
commandPath := filepath.Join(project, ".cursor", "commands", "FD-commit.md")
|
||
|
|
if _, err := os.Stat(commandPath); err != nil {
|
||
|
|
t.Fatalf("command file missing: %v", err)
|
||
|
|
}
|
||
|
|
// 命令模板不应进入 skill 目录。
|
||
|
|
if _, err := os.Stat(filepath.Join(project, ".cursor", "skills", "fd-engineering", "commands")); !os.IsNotExist(err) {
|
||
|
|
t.Fatal("commands template must not be copied into skill dir")
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := Uninstall(project, "fd-engineering"); err != nil {
|
||
|
|
t.Fatalf("uninstall: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(commandPath); !os.IsNotExist(err) {
|
||
|
|
t.Fatalf("command file should be removed: %v", err)
|
||
|
|
}
|
||
|
|
if IsInstalled(project, "fd-engineering") {
|
||
|
|
t.Fatal("expected not installed after Uninstall")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInstallRefreshesBuiltinAndRetiredFiles(t *testing.T) {
|
||
|
|
project := t.TempDir()
|
||
|
|
if err := Install(project, "fd-engineering"); err != nil {
|
||
|
|
t.Fatalf("install: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
target := filepath.Join(project, ".cursor", "skills", "fd-engineering")
|
||
|
|
skillPath := filepath.Join(target, "SKILL.md")
|
||
|
|
if err := os.WriteFile(skillPath, []byte("old skill\n"), 0o644); err != nil {
|
||
|
|
t.Fatalf("write stale skill: %v", err)
|
||
|
|
}
|
||
|
|
workflowPath := filepath.Join(target, "references", "workflow.md")
|
||
|
|
if err := os.Remove(workflowPath); err != nil {
|
||
|
|
t.Fatalf("remove workflow: %v", err)
|
||
|
|
}
|
||
|
|
for _, name := range []string{"native-tools.md", "mode-switch.md"} {
|
||
|
|
path := filepath.Join(target, "references", name)
|
||
|
|
if err := os.WriteFile(path, []byte("retired\n"), 0o644); err != nil {
|
||
|
|
t.Fatalf("write retired reference: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
customPath := filepath.Join(target, "references", "custom.md")
|
||
|
|
if err := os.WriteFile(customPath, []byte("user-owned\n"), 0o644); err != nil {
|
||
|
|
t.Fatalf("write custom reference: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := Install(project, "fd-engineering"); err != nil {
|
||
|
|
t.Fatalf("refresh install: %v", err)
|
||
|
|
}
|
||
|
|
got, err := os.ReadFile(skillPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("read refreshed skill: %v", err)
|
||
|
|
}
|
||
|
|
want, err := builtinFS.ReadFile("builtin/fd-engineering/SKILL.md")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("read embedded skill: %v", err)
|
||
|
|
}
|
||
|
|
if string(got) != string(want) {
|
||
|
|
t.Fatal("existing skill was not refreshed from builtin content")
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(workflowPath); err != nil {
|
||
|
|
t.Fatalf("missing workflow was not restored: %v", err)
|
||
|
|
}
|
||
|
|
for _, name := range []string{"native-tools.md", "mode-switch.md"} {
|
||
|
|
if _, err := os.Stat(filepath.Join(target, "references", name)); !os.IsNotExist(err) {
|
||
|
|
t.Fatalf("retired reference %s was not removed", name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(customPath); err != nil {
|
||
|
|
t.Fatalf("user-owned reference must survive refresh: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUninstallKeepsUserCommands(t *testing.T) {
|
||
|
|
project := t.TempDir()
|
||
|
|
if err := Install(project, "fd-engineering"); err != nil {
|
||
|
|
t.Fatalf("install: %v", err)
|
||
|
|
}
|
||
|
|
// 用户自建命令不受影响。
|
||
|
|
userCommand := filepath.Join(project, ".cursor", "commands", "my-own.md")
|
||
|
|
if err := os.WriteFile(userCommand, []byte("# own\n"), 0o644); err != nil {
|
||
|
|
t.Fatalf("write user command: %v", err)
|
||
|
|
}
|
||
|
|
if err := Uninstall(project, "fd-engineering"); err != nil {
|
||
|
|
t.Fatalf("uninstall: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(userCommand); err != nil {
|
||
|
|
t.Fatalf("user command must survive: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInstallRejectsUnknownSkillAndBadProject(t *testing.T) {
|
||
|
|
if err := Install(t.TempDir(), "not-exist"); err == nil {
|
||
|
|
t.Fatal("expected error for unknown skill")
|
||
|
|
}
|
||
|
|
if err := Install(filepath.Join(t.TempDir(), "missing"), "fd-engineering"); err == nil {
|
||
|
|
t.Fatal("expected error for missing project dir")
|
||
|
|
}
|
||
|
|
// 路径穿越防护。
|
||
|
|
if err := Install(t.TempDir(), "../evil"); err == nil {
|
||
|
|
t.Fatal("expected error for path traversal")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestListExposesCommands(t *testing.T) {
|
||
|
|
list := List()
|
||
|
|
if len(list) == 0 {
|
||
|
|
t.Fatal("expected builtin skills")
|
||
|
|
}
|
||
|
|
var found *Skill
|
||
|
|
for i := range list {
|
||
|
|
if list[i].Name == "fd-engineering" {
|
||
|
|
found = &list[i]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if found == nil {
|
||
|
|
t.Fatal("fd-engineering not listed")
|
||
|
|
}
|
||
|
|
if len(found.Commands) == 0 {
|
||
|
|
t.Fatal("expected commands to be listed")
|
||
|
|
}
|
||
|
|
if found.Commands[0].Description == "" {
|
||
|
|
t.Fatal("expected command description parsed from frontmatter")
|
||
|
|
}
|
||
|
|
}
|