package executor import ( "os" "path/filepath" "strings" "testing" ) // --- helper: create a temp skill dir with SKILL.md --- func createTempSkill(t *testing.T, name, content string) string { t.Helper() home := t.TempDir() skillDir := filepath.Join(home, ".claude", "skills", name) if err := os.MkdirAll(skillDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(content), 0644); err != nil { t.Fatal(err) } return home } // --- ParseParallelConfig skills parsing tests --- func TestParseParallelConfig_SkillsField(t *testing.T) { tests := []struct { name string input string taskIdx int expectedSkills []string }{ { name: "single skill", input: `---TASK--- id: t1 workdir: . skills: golang-base-practices ---CONTENT--- Do something. `, taskIdx: 0, expectedSkills: []string{"golang-base-practices"}, }, { name: "multiple comma-separated skills", input: `---TASK--- id: t1 workdir: . skills: golang-base-practices, vercel-react-best-practices ---CONTENT--- Do something. `, taskIdx: 0, expectedSkills: []string{"golang-base-practices", "vercel-react-best-practices"}, }, { name: "no skills field", input: `---TASK--- id: t1 workdir: . ---CONTENT--- Do something. `, taskIdx: 0, expectedSkills: nil, }, { name: "empty skills value", input: `---TASK--- id: t1 workdir: . skills: ---CONTENT--- Do something. `, taskIdx: 0, expectedSkills: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg, err := ParseParallelConfig([]byte(tt.input)) if err != nil { t.Fatalf("ParseParallelConfig error: %v", err) } got := cfg.Tasks[tt.taskIdx].Skills if len(got) != len(tt.expectedSkills) { t.Fatalf("skills: got %v, want %v", got, tt.expectedSkills) } for i := range got { if got[i] != tt.expectedSkills[i] { t.Errorf("skills[%d]: got %q, want %q", i, got[i], tt.expectedSkills[i]) } } }) } } // --- stripYAMLFrontmatter tests --- func TestStripYAMLFrontmatter(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "with frontmatter", input: "---\nname: test\ndescription: foo\n---\n\n# Body\nContent here.", expected: "# Body\nContent here.", }, { name: "no frontmatter", input: "# Just a body\nNo frontmatter.", expected: "# Just a body\nNo frontmatter.", }, { name: "empty", input: "", expected: "", }, { name: "only frontmatter", input: "---\nname: test\n---", expected: "", }, { name: "frontmatter with allowed-tools", input: "---\nname: do\nallowed-tools: [\"Bash\"]\n---\n\n# Skill content", expected: "# Skill content", }, { name: "CRLF line endings", input: "---\r\nname: test\r\n---\r\n\r\n# Body\r\nContent.", expected: "# Body\nContent.", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := stripYAMLFrontmatter(tt.input) if got != tt.expected { t.Errorf("got %q, want %q", got, tt.expected) } }) } } // --- DetectProjectSkills tests --- func TestDetectProjectSkills_GoProject(t *testing.T) { tmpDir := t.TempDir() os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test"), 0644) skills := DetectProjectSkills(tmpDir) // Result depends on whether golang-base-practices is installed locally t.Logf("detected skills for Go project: %v", skills) } func TestDetectProjectSkills_NoFingerprints(t *testing.T) { tmpDir := t.TempDir() skills := DetectProjectSkills(tmpDir) if len(skills) != 0 { t.Errorf("expected no skills for empty dir, got %v", skills) } } func TestDetectProjectSkills_FullStack(t *testing.T) { tmpDir := t.TempDir() os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test"), 0644) os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`{"name":"test"}`), 0644) skills := DetectProjectSkills(tmpDir) t.Logf("detected skills for fullstack project: %v", skills) seen := make(map[string]bool) for _, s := range skills { if seen[s] { t.Errorf("duplicate skill detected: %s", s) } seen[s] = true } } func TestDetectProjectSkills_NonexistentDir(t *testing.T) { skills := DetectProjectSkills("/nonexistent/path/xyz") if len(skills) != 0 { t.Errorf("expected no skills for nonexistent dir, got %v", skills) } } // --- ResolveSkillContent tests (CI-friendly with temp dirs) --- func TestResolveSkillContent_ValidSkill(t *testing.T) { home := createTempSkill(t, "test-skill", "---\nname: test\n---\n\n# Test Skill\nBest practices here.") t.Setenv("HOME", home) result := ResolveSkillContent([]string{"test-skill"}, 0) if result == "" { t.Fatal("expected non-empty content") } if !strings.Contains(result, ``) { t.Error("missing opening tag") } if !strings.Contains(result, "") { t.Error("missing closing tag") } if !strings.Contains(result, "# Test Skill") { t.Error("missing skill body content") } if strings.Contains(result, "name: test") { t.Error("frontmatter was not stripped") } } func TestResolveSkillContent_NonexistentSkill(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) result := ResolveSkillContent([]string{"nonexistent-skill-xyz"}, 0) if result != "" { t.Errorf("expected empty for nonexistent skill, got %d bytes", len(result)) } } func TestResolveSkillContent_Empty(t *testing.T) { if result := ResolveSkillContent(nil, 0); result != "" { t.Errorf("expected empty for nil, got %q", result) } if result := ResolveSkillContent([]string{}, 0); result != "" { t.Errorf("expected empty for empty, got %q", result) } } func TestResolveSkillContent_Budget(t *testing.T) { longBody := strings.Repeat("x", 500) home := createTempSkill(t, "big-skill", "---\nname: big\n---\n\n"+longBody) t.Setenv("HOME", home) result := ResolveSkillContent([]string{"big-skill"}, 200) if result == "" { t.Fatal("expected non-empty even with small budget") } if len(result) > 200 { t.Errorf("result %d bytes exceeds budget 200", len(result)) } t.Logf("budget=200, result=%d bytes", len(result)) } func TestResolveSkillContent_MultipleSkills(t *testing.T) { home := t.TempDir() for _, name := range []string{"skill-a", "skill-b"} { skillDir := filepath.Join(home, ".claude", "skills", name) os.MkdirAll(skillDir, 0755) os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# "+name+"\nContent."), 0644) } t.Setenv("HOME", home) result := ResolveSkillContent([]string{"skill-a", "skill-b"}, 0) if result == "" { t.Fatal("expected non-empty for multiple skills") } if !strings.Contains(result, ``) { t.Error("missing skill-a tag") } if !strings.Contains(result, ``) { t.Error("missing skill-b tag") } } func TestResolveSkillContent_PathTraversal(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) result := ResolveSkillContent([]string{"../../../etc/passwd"}, 0) if result != "" { t.Errorf("expected empty for path traversal name, got %d bytes", len(result)) } } func TestResolveSkillContent_InvalidNames(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) tests := []string{"../bad", "foo/bar", "skill name", "skill.name", "a b"} for _, name := range tests { result := ResolveSkillContent([]string{name}, 0) if result != "" { t.Errorf("expected empty for invalid name %q, got %d bytes", name, len(result)) } } } func TestResolveSkillContent_ValidNamePattern(t *testing.T) { if !validSkillName.MatchString("golang-base-practices") { t.Error("golang-base-practices should be valid") } if !validSkillName.MatchString("my_skill_v2") { t.Error("my_skill_v2 should be valid") } if validSkillName.MatchString("../bad") { t.Error("../bad should be invalid") } if validSkillName.MatchString("") { t.Error("empty should be invalid") } } // --- Integration: skill injection format test --- func TestSkillInjectionFormat(t *testing.T) { home := createTempSkill(t, "test-go", "---\nname: go\n---\n\n# Go Best Practices\nUse gofmt.") t.Setenv("HOME", home) taskText := "Implement the feature." content := ResolveSkillContent([]string{"test-go"}, 0) injected := taskText + "\n\n# Domain Best Practices\n\n" + content if !strings.Contains(injected, "Implement the feature.") { t.Error("original task text lost") } if !strings.Contains(injected, "# Domain Best Practices") { t.Error("missing section header") } if !strings.Contains(injected, ``) { t.Error("missing tag") } if !strings.Contains(injected, "Use gofmt.") { t.Error("missing skill body") } }