Files
MuyueWorkspace/internal/skills/skills_test.go
Augustin 485e085bb0 feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard
Major changes:
- Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version)
- Add LSP registry with health checks, auto-install, and editor config generation
- Add MCP registry with editor detection, status tracking, and per-editor configuration
- Add workflow engine with planner and step execution for automated task chains
- Add conversation search, export (Markdown/JSON), and detailed token counting
- Add streaming shell chat handler with tool call/result events
- Add skill validation, dry-run testing, and export endpoints
- Enrich dashboard with Tools/Activity/Status tabs and tool cards grid
- Add PRD documentation
- Complete i18n for both EN and FR

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-23 19:47:00 +02:00

440 lines
9.3 KiB
Go

package skills
import (
"os"
"path/filepath"
"testing"
)
func TestParseSkillWithYAML(t *testing.T) {
data := []byte(`---
name: test-skill
description: A test skill
author: test
version: "1.0"
target: both
tags:
- test
- demo
---
# Test Skill Content
This is the body.
`)
skill, err := parseSkill(data)
if err != nil {
t.Fatalf("parseSkill failed: %v", err)
}
if skill.Description != "A test skill" {
t.Errorf("Expected 'A test skill', got %s", skill.Description)
}
if skill.Author != "test" {
t.Errorf("Expected 'test', got %s", skill.Author)
}
if skill.Version != "1.0" {
t.Errorf("Expected '1.0', got %s", skill.Version)
}
if skill.Target != "both" {
t.Errorf("Expected 'both', got %s", skill.Target)
}
if len(skill.Tags) != 2 {
t.Errorf("Expected 2 tags, got %d", len(skill.Tags))
}
if skill.Content == "" {
t.Error("Content should not be empty")
}
}
func TestParseSkillNoFrontmatter(t *testing.T) {
data := []byte("Just plain content here")
skill, err := parseSkill(data)
if err != nil {
t.Fatalf("parseSkill failed: %v", err)
}
if skill.Content != "Just plain content here" {
t.Errorf("Unexpected content: %s", skill.Content)
}
}
func TestParseSkillIncompleteFrontmatter(t *testing.T) {
data := []byte("---\nname: incomplete\n---\nBody content")
skill, err := parseSkill(data)
if err != nil {
t.Fatalf("parseSkill failed: %v", err)
}
if skill.Content != "Body content" {
t.Errorf("Expected 'Body content', got %s", skill.Content)
}
}
func TestRenderSkill(t *testing.T) {
skill := &Skill{
Name: "test",
Description: "A test",
Author: "author",
Version: "1.0",
Target: "both",
Tags: []string{"a", "b"},
Content: "Body",
}
rendered := renderSkill(skill)
if rendered == "" {
t.Error("Rendered skill should not be empty")
}
if len(rendered) < 20 {
t.Error("Rendered skill seems too short")
}
}
func TestListEmpty(t *testing.T) {
tmpDir := t.TempDir()
origHome := os.Getenv("HOME")
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", origHome)
skills, err := List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(skills) != 0 {
t.Errorf("Expected 0 skills, got %d", len(skills))
}
}
func TestCreateAndGet(t *testing.T) {
tmpDir := t.TempDir()
origHome := os.Getenv("HOME")
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", origHome)
skill := &Skill{
Name: "test-skill",
Description: "Test description",
Content: "Test content body",
Author: "tester",
Version: "1.0.0",
Target: "both",
}
if err := Create(skill); err != nil {
t.Fatalf("Create failed: %v", err)
}
dir, _ := SkillsDir()
skillPath := filepath.Join(dir, "test-skill", "SKILL.md")
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
t.Error("Skill file should exist")
}
got, err := Get("test-skill")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got.Name != "test-skill" {
t.Errorf("Expected test-skill, got %s", got.Name)
}
}
func TestDelete(t *testing.T) {
tmpDir := t.TempDir()
origHome := os.Getenv("HOME")
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", origHome)
skill := &Skill{
Name: "to-delete",
Description: "Will be deleted",
Content: "content",
Target: "both",
}
Create(skill)
if err := Delete("to-delete"); err != nil {
t.Fatalf("Delete failed: %v", err)
}
_, err := Get("to-delete")
if err == nil {
t.Error("Skill should be deleted")
}
}
func TestBuildAIGeneratePrompt(t *testing.T) {
prompt := BuildAIGeneratePrompt("docker", "Set up Docker", "both")
if prompt == "" {
t.Error("Prompt should not be empty")
}
if len(prompt) < 50 {
t.Error("Prompt seems too short")
}
}
func TestInstallBuiltinSkills(t *testing.T) {
tmpDir := t.TempDir()
origHome := os.Getenv("HOME")
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", origHome)
if err := InstallBuiltinSkills(); err != nil {
t.Fatalf("InstallBuiltinSkills failed: %v", err)
}
skills, err := List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(skills) == 0 {
t.Error("Expected at least one builtin skill")
}
found := false
for _, s := range skills {
if s.Name == "env-setup" {
found = true
}
}
if !found {
t.Error("Expected env-setup skill")
}
}
func TestValidate(t *testing.T) {
skill := &Skill{
Name: "valid-skill",
Description: "A valid skill",
Content: "## Steps\nDo things",
Version: "1.0.0",
Target: "both",
}
errs := Validate(skill)
if len(errs) != 0 {
t.Errorf("Valid skill should have no errors, got %v", errs)
}
}
func TestValidateMissingFields(t *testing.T) {
skill := &Skill{}
errs := Validate(skill)
if len(errs) == 0 {
t.Error("Empty skill should have validation errors")
}
fields := map[string]bool{}
for _, e := range errs {
fields[e.Field] = true
}
if !fields["name"] {
t.Error("Should require name")
}
if !fields["description"] {
t.Error("Should require description")
}
if !fields["content"] {
t.Error("Should require content")
}
}
func TestValidateBadVersion(t *testing.T) {
skill := &Skill{
Name: "test-skill",
Description: "desc",
Content: "content",
Version: "not-semver",
}
errs := Validate(skill)
hasVersionErr := false
for _, e := range errs {
if e.Field == "version" {
hasVersionErr = true
}
}
if !hasVersionErr {
t.Error("Should reject non-semver version")
}
}
func TestValidateBadTarget(t *testing.T) {
skill := &Skill{
Name: "test",
Description: "desc",
Content: "content",
Target: "invalid",
}
errs := Validate(skill)
hasTargetErr := false
for _, e := range errs {
if e.Field == "target" {
hasTargetErr = true
}
}
if !hasTargetErr {
t.Error("Should reject invalid target")
}
}
func TestValidateBadName(t *testing.T) {
skill := &Skill{
Name: "INVALID",
Description: "desc",
Content: "content",
}
errs := Validate(skill)
hasNameErr := false
for _, e := range errs {
if e.Field == "name" {
hasNameErr = true
}
}
if !hasNameErr {
t.Error("Should reject uppercase name")
}
}
func TestValidateDependencies(t *testing.T) {
skill := &Skill{
Name: "test",
Description: "desc",
Content: "content",
Dependencies: []SkillDependency{
{Type: "mcp_server", Name: "github", Required: true},
{Type: "invalid_type", Name: "test"},
},
}
errs := Validate(skill)
hasDepErr := false
for _, e := range errs {
if e.Field == "dependencies[1].type" {
hasDepErr = true
}
}
if !hasDepErr {
t.Error("Should reject invalid dependency type")
}
}
func TestExportImport(t *testing.T) {
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", tmpDir)
skill := &Skill{
Name: "export-test",
Description: "Export test skill",
Content: "## Content",
Author: "tester",
Version: "1.0.0",
Target: "both",
Tags: []string{"test"},
}
Create(skill)
exportPath := filepath.Join(tmpDir, "export", "export-test.md")
if err := Export("export-test", exportPath); err != nil {
t.Fatalf("Export failed: %v", err)
}
if _, err := os.Stat(exportPath); os.IsNotExist(err) {
t.Error("Export file should exist")
}
imported, err := Import(exportPath)
if err != nil {
t.Fatalf("Import failed: %v", err)
}
if imported.Description != "Export test skill" {
t.Errorf("Expected 'Export test skill', got %s", imported.Description)
}
}
func TestDryRun(t *testing.T) {
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", tmpDir)
skill := &Skill{
Name: "dry-run-test",
Description: "Dry run test",
Content: "## Steps\nDo something",
Version: "1.0.0",
Target: "both",
Tags: []string{"test"},
}
Create(skill)
result := DryRun("dry-run-test", "test something")
if !result.Passed {
t.Errorf("DryRun should pass, got: %s", result.Message)
}
}
func TestDryRunMissing(t *testing.T) {
result := DryRun("nonexistent", "")
if result.Passed {
t.Error("DryRun of nonexistent skill should fail")
}
}
func TestUpdate(t *testing.T) {
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", tmpDir)
skill := &Skill{
Name: "update-test",
Description: "Original",
Content: "Original content",
Version: "1.0.0",
Target: "both",
}
Create(skill)
skill.Description = "Updated"
skill.Content = "Updated content"
skill.Version = "2.0.0"
if err := Update(skill); err != nil {
t.Fatalf("Update failed: %v", err)
}
got, err := Get("update-test")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got.Description != "Updated" {
t.Errorf("Expected 'Updated', got %s", got.Description)
}
}
func TestBuiltinSkillCount(t *testing.T) {
if len(builtinSkills) < 5 {
t.Errorf("Expected at least 5 builtin skills, got %d", len(builtinSkills))
}
expectedSkills := []string{"env-setup", "git-workflow", "api-design", "debug-assist", "code-review", "docker-setup", "security-audit", "mcp-setup", "lsp-setup", "workflow-design"}
for _, name := range expectedSkills {
found := false
for _, s := range builtinSkills {
if s.Name == name {
found = true
break
}
}
if !found {
t.Errorf("Expected builtin skill: %s", name)
}
}
}
func TestBuiltinSkillsHaveDependencies(t *testing.T) {
hasDeps := 0
for _, s := range builtinSkills {
if len(s.Dependencies) > 0 {
hasDeps++
}
}
if hasDeps == 0 {
t.Error("At least some builtin skills should declare dependencies")
}
}