// skill_paths_test.go - 14.P1-A Paths 激活过滤测试. // // 覆盖场景: // - Paths 为空时全局激活 // - 精确目录匹配 // - 父子路径匹配(前缀匹配) // - Glob 通配符匹配 // - 不匹配场景 // - SkillRegistry.ListForCwd 过滤逻辑 package engine import ( "runtime" "testing" ) // skipOnWindows 在 Windows 跳过测试(路径分隔符差异). func skipOnWindows(t *testing.T) { t.Helper() if runtime.GOOS == "windows" { t.Skip("path separator differs on Windows") } } // ---- Skill.MatchesCwd 正常路径 ---- // TestSkillMatchesCwd_NoPaths 路径为空时任何 CWD 都应激活. func TestSkillMatchesCwd_NoPaths(t *testing.T) { s := &Skill{Name: "global", Paths: nil} cases := []string{"/any/dir", "/tmp", "/home/user/project", ""} for _, cwd := range cases { if !s.MatchesCwd(cwd) { t.Errorf("空 Paths 应全局激活,CWD=%q 返回了 false", cwd) } } } // TestSkillMatchesCwd_ExactDir 精确目录路径匹配. func TestSkillMatchesCwd_ExactDir(t *testing.T) { skipOnWindows(t) s := &Skill{Name: "frontend", Paths: []string{"/project/src"}} // 精确匹配应激活 if !s.MatchesCwd("/project/src") { t.Error("精确路径 /project/src 应匹配 /project/src") } // 子目录也应激活(前缀匹配) if !s.MatchesCwd("/project/src/components") { t.Error("子目录 /project/src/components 应匹配 /project/src") } // 不相关目录不应激活 if s.MatchesCwd("/project/tests") { t.Error("/project/tests 不应匹配 /project/src") } // 防止局部前缀误匹配(src != src-extra) if s.MatchesCwd("/project/src-extra") { t.Error("/project/src-extra 不应匹配 /project/src(路径段精确匹配)") } } // TestSkillMatchesCwd_TrailingSlash 路径末尾有斜杠时也应正确匹配. func TestSkillMatchesCwd_TrailingSlash(t *testing.T) { skipOnWindows(t) s := &Skill{Name: "api", Paths: []string{"/project/api/"}} if !s.MatchesCwd("/project/api") { t.Error("末尾斜杠不影响匹配,/project/api 应匹配") } if !s.MatchesCwd("/project/api/handlers") { t.Error("子目录 /project/api/handlers 应匹配 /project/api/") } } // TestSkillMatchesCwd_GlobPattern glob 通配符匹配. func TestSkillMatchesCwd_GlobPattern(t *testing.T) { skipOnWindows(t) // 精妙之处(CLEVER): filepath.Match 只匹配单段(* 不匹配 /), // 所以 "*/frontend" 只匹配"一层父目录/frontend",不匹配多层路径. s := &Skill{Name: "ui", Paths: []string{"/project/*/frontend"}} // 匹配"一层目录" if !s.MatchesCwd("/project/app/frontend") { t.Error("/project/app/frontend 应匹配 /project/*/frontend") } // 不匹配多层目录(filepath.Match 的 * 不跨分隔符) if s.MatchesCwd("/project/app/ui/frontend") { t.Error("/project/app/ui/frontend 不应匹配 /project/*/frontend(* 不跨路径分隔符)") } } // TestSkillMatchesCwd_MultiPaths 多个 Paths 时 OR 关系. func TestSkillMatchesCwd_MultiPaths(t *testing.T) { skipOnWindows(t) s := &Skill{Name: "fullstack", Paths: []string{ "/project/frontend", "/project/backend", }} if !s.MatchesCwd("/project/frontend") { t.Error("/project/frontend 应匹配(第一个路径)") } if !s.MatchesCwd("/project/backend/api") { t.Error("/project/backend/api 应匹配(第二个路径的子目录)") } if s.MatchesCwd("/project/infra") { t.Error("/project/infra 不应匹配任何路径") } } // TestSkillMatchesCwd_NoMatch 无匹配场景. func TestSkillMatchesCwd_NoMatch(t *testing.T) { skipOnWindows(t) s := &Skill{Name: "restricted", Paths: []string{"/secure/zone"}} if s.MatchesCwd("/other/dir") { t.Error("/other/dir 不应匹配 /secure/zone") } if s.MatchesCwd("/") { t.Error("根目录不应匹配 /secure/zone") } // 确保父路径不会误匹配子路径 if s.MatchesCwd("/secure") { t.Error("/secure 不应匹配 /secure/zone(父路径不激活子 Skill)") } } // ---- SkillRegistry.ListForCwd ---- // TestSkillRegistry_ListForCwd_Empty 无匹配时返回空列表. func TestSkillRegistry_ListForCwd_Empty(t *testing.T) { skipOnWindows(t) r := newSkillRegistry(nil) r.RegisterAll([]*Skill{ {Name: "frontend", Paths: []string{"/project/src"}}, {Name: "api", Paths: []string{"/project/api"}}, }) result := r.ListForCwd("/other/dir") if len(result) != 0 { t.Errorf("无匹配时应返回空列表,got %d skills", len(result)) } } // TestSkillRegistry_ListForCwd_MatchesSome 只返回匹配的 Skill. func TestSkillRegistry_ListForCwd_MatchesSome(t *testing.T) { skipOnWindows(t) r := newSkillRegistry(nil) r.RegisterAll([]*Skill{ {Name: "frontend", Paths: []string{"/project/src"}}, {Name: "api", Paths: []string{"/project/api"}}, {Name: "global", Paths: nil}, // 全局 Skill(始终激活) }) result := r.ListForCwd("/project/src/components") names := skillNames(result) if !containsStr(names, "frontend") { t.Error("前端 Skill 应被激活(/project/src 前缀匹配)") } if containsStr(names, "api") { t.Error("API Skill 不应被激活(不在 /project/api 下)") } if !containsStr(names, "global") { t.Error("全局 Skill(Paths=nil)应始终激活") } } // TestSkillRegistry_ListForCwd_GlobalSkillsAlwaysIncluded 全局 Skill 任何目录都包含. func TestSkillRegistry_ListForCwd_GlobalSkillsAlwaysIncluded(t *testing.T) { r := newSkillRegistry(nil) r.RegisterAll([]*Skill{ {Name: "commit", Paths: nil}, // 全局 {Name: "review", Paths: nil}, // 全局 {Name: "frontend", Paths: []string{"/project/src"}}, }) result := r.ListForCwd("/totally/different/dir") names := skillNames(result) if !containsStr(names, "commit") || !containsStr(names, "review") { t.Error("全局 Skill(Paths=nil)应在任何目录下激活") } if containsStr(names, "frontend") { t.Error("frontend Skill 不应在非 /project/src 下激活") } } // TestSkillRegistry_ListForCwd_WithAdditionalFilter 结合其他过滤器. func TestSkillRegistry_ListForCwd_WithAdditionalFilter(t *testing.T) { skipOnWindows(t) r := newSkillRegistry(nil) r.RegisterAll([]*Skill{ {Name: "user-cmd", Paths: []string{"/project"}, UserInvocable: true}, {Name: "internal", Paths: []string{"/project"}, UserInvocable: false}, }) // 在 /project 目录下,且 UserInvocable=true result := r.ListForCwd("/project/src", func(s *Skill) bool { return s.UserInvocable }) if len(result) != 1 || result[0].Name != "user-cmd" { t.Errorf("应只返回 user-cmd,got %v", skillNames(result)) } } // ---- 辅助函数 ---- func skillNames(skills []*Skill) []string { names := make([]string, len(skills)) for i, s := range skills { names[i] = s.Name } return names } func containsStr(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false }