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") } }