WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
package skills
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSkillFile(t *testing.T) {
|
||||
content := `---
|
||||
name: test-skill
|
||||
description: A test skill for unit testing purposes.
|
||||
---
|
||||
# Test Skill
|
||||
|
||||
This is the content of the test skill.
|
||||
|
||||
## Usage
|
||||
|
||||
Use this skill when testing.
|
||||
`
|
||||
|
||||
skill, err := ParseSkillFile(content)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse skill file: %v", err)
|
||||
}
|
||||
|
||||
if skill.Name != "test-skill" {
|
||||
t.Errorf("Expected name 'test-skill', got '%s'", skill.Name)
|
||||
}
|
||||
|
||||
if skill.Description != "A test skill for unit testing purposes." {
|
||||
t.Errorf("Expected description 'A test skill for unit testing purposes.', got '%s'", skill.Description)
|
||||
}
|
||||
|
||||
if skill.Instructions == "" {
|
||||
t.Error("Expected instructions to be non-empty")
|
||||
}
|
||||
|
||||
if !skill.Loaded {
|
||||
t.Error("Expected Loaded to be true after parsing")
|
||||
}
|
||||
if skill.FrontmatterRepaired {
|
||||
t.Error("valid frontmatter should not be marked repaired")
|
||||
}
|
||||
|
||||
t.Logf("Parsed skill: name=%s, description=%s, instructions_len=%d",
|
||||
skill.Name, skill.Description, len(skill.Instructions))
|
||||
}
|
||||
|
||||
func TestSkillValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
skillName string
|
||||
description string
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid skill",
|
||||
skillName: "my-skill",
|
||||
description: "A valid skill",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
skillName: "",
|
||||
description: "A skill",
|
||||
wantErr: true,
|
||||
errContains: "name is required",
|
||||
},
|
||||
{
|
||||
name: "invalid characters in name",
|
||||
skillName: "My Skill",
|
||||
description: "A skill",
|
||||
wantErr: true,
|
||||
errContains: "letters, numbers, hyphens, and underscores",
|
||||
},
|
||||
{
|
||||
name: "underscore is allowed",
|
||||
skillName: "word_docx",
|
||||
description: "A skill",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unicode letters are allowed",
|
||||
skillName: "律师助手",
|
||||
description: "A skill",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "reserved word in name",
|
||||
skillName: "my-claude-skill",
|
||||
description: "A skill",
|
||||
wantErr: true,
|
||||
errContains: "reserved word",
|
||||
},
|
||||
{
|
||||
name: "empty description",
|
||||
skillName: "my-skill",
|
||||
description: "",
|
||||
wantErr: true,
|
||||
errContains: "description is required",
|
||||
},
|
||||
{
|
||||
name: "name too long",
|
||||
skillName: "this-is-a-very-long-skill-name-that-exceeds-the-maximum-allowed-length-of-64-characters",
|
||||
description: "A skill",
|
||||
wantErr: true,
|
||||
errContains: "maximum is 64",
|
||||
},
|
||||
{
|
||||
name: "description at 1024 CJK runes is allowed even though it is over 1024 bytes",
|
||||
skillName: "my-skill",
|
||||
description: strings.Repeat("描", 1024),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "description over 1024 CJK runes is rejected",
|
||||
skillName: "my-skill",
|
||||
description: strings.Repeat("描", 1025),
|
||||
wantErr: true,
|
||||
errContains: "maximum is 1024",
|
||||
},
|
||||
{
|
||||
name: "description over 1024 ASCII runes is rejected",
|
||||
skillName: "my-skill",
|
||||
description: strings.Repeat("a", 1025),
|
||||
wantErr: true,
|
||||
errContains: "maximum is 1024",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
skill := &Skill{
|
||||
Name: tt.skillName,
|
||||
Description: tt.description,
|
||||
}
|
||||
|
||||
err := skill.Validate()
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error containing '%s', got nil", tt.errContains)
|
||||
} else if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
|
||||
t.Errorf("Expected error containing '%s', got '%s'", tt.errContains, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func containsString(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr))
|
||||
}
|
||||
|
||||
func containsSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestLoaderDiscoverSkills(t *testing.T) {
|
||||
// Create a temporary skills directory
|
||||
tmpDir, err := os.MkdirTemp("", "skills-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a test skill directory
|
||||
skillDir := filepath.Join(tmpDir, "test-skill")
|
||||
if err := os.MkdirAll(skillDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create skill dir: %v", err)
|
||||
}
|
||||
|
||||
// Write SKILL.md
|
||||
skillContent := `---
|
||||
name: test-skill
|
||||
description: A test skill for loader testing.
|
||||
---
|
||||
# Test Skill
|
||||
|
||||
This is the test skill content.
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(skillContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write SKILL.md: %v", err)
|
||||
}
|
||||
|
||||
// Create loader and discover skills
|
||||
loader := NewLoader([]string{tmpDir})
|
||||
metadata, err := loader.DiscoverSkills()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to discover skills: %v", err)
|
||||
}
|
||||
|
||||
if len(metadata) != 1 {
|
||||
t.Fatalf("Expected 1 skill, got %d", len(metadata))
|
||||
}
|
||||
|
||||
if metadata[0].Name != "test-skill" {
|
||||
t.Errorf("Expected skill name 'test-skill', got '%s'", metadata[0].Name)
|
||||
}
|
||||
|
||||
t.Logf("Discovered %d skills: %v", len(metadata), metadata[0].Name)
|
||||
}
|
||||
|
||||
func TestLoaderLoadSkillInstructions(t *testing.T) {
|
||||
// Create a temporary skills directory
|
||||
tmpDir, err := os.MkdirTemp("", "skills-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a test skill directory
|
||||
skillDir := filepath.Join(tmpDir, "test-skill")
|
||||
if err := os.MkdirAll(skillDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create skill dir: %v", err)
|
||||
}
|
||||
|
||||
// Write SKILL.md
|
||||
skillContent := `---
|
||||
name: test-skill
|
||||
description: A test skill for content loading.
|
||||
---
|
||||
# Test Skill
|
||||
|
||||
This is the main content.
|
||||
|
||||
## Section 1
|
||||
|
||||
More content here.
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(skillContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write SKILL.md: %v", err)
|
||||
}
|
||||
|
||||
// Create loader and load skill instructions
|
||||
loader := NewLoader([]string{tmpDir})
|
||||
skill, err := loader.LoadSkillInstructions("test-skill")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load skill instructions: %v", err)
|
||||
}
|
||||
|
||||
if skill.Name != "test-skill" {
|
||||
t.Errorf("Expected skill name 'test-skill', got '%s'", skill.Name)
|
||||
}
|
||||
|
||||
if skill.Instructions == "" {
|
||||
t.Error("Expected instructions to be non-empty")
|
||||
}
|
||||
|
||||
if !skill.Loaded {
|
||||
t.Error("Expected Loaded to be true")
|
||||
}
|
||||
|
||||
t.Logf("Loaded skill: name=%s, instructions_len=%d", skill.Name, len(skill.Instructions))
|
||||
}
|
||||
|
||||
func TestLoaderLoadSkillFile(t *testing.T) {
|
||||
// Create a temporary skills directory
|
||||
tmpDir, err := os.MkdirTemp("", "skills-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a test skill directory with additional files
|
||||
skillDir := filepath.Join(tmpDir, "test-skill")
|
||||
scriptsDir := filepath.Join(skillDir, "scripts")
|
||||
if err := os.MkdirAll(scriptsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create skill dir: %v", err)
|
||||
}
|
||||
|
||||
// Write SKILL.md
|
||||
skillContent := `---
|
||||
name: test-skill
|
||||
description: A test skill with additional files.
|
||||
---
|
||||
# Test Skill
|
||||
|
||||
See [GUIDE.md](GUIDE.md) for more info.
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(skillContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write SKILL.md: %v", err)
|
||||
}
|
||||
|
||||
// Write additional file
|
||||
guideContent := "# Guide\n\nThis is the guide content."
|
||||
if err := os.WriteFile(filepath.Join(skillDir, "GUIDE.md"), []byte(guideContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write GUIDE.md: %v", err)
|
||||
}
|
||||
|
||||
// Write a script
|
||||
scriptContent := "#!/usr/bin/env python3\nprint('Hello from script')"
|
||||
if err := os.WriteFile(filepath.Join(scriptsDir, "hello.py"), []byte(scriptContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write script: %v", err)
|
||||
}
|
||||
|
||||
// Create loader and discover skills first
|
||||
loader := NewLoader([]string{tmpDir})
|
||||
_, err = loader.DiscoverSkills()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to discover skills: %v", err)
|
||||
}
|
||||
|
||||
// Load additional file
|
||||
file, err := loader.LoadSkillFile("test-skill", "GUIDE.md")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load skill file: %v", err)
|
||||
}
|
||||
|
||||
if file.Content != guideContent {
|
||||
t.Errorf("Expected guide content, got '%s'", file.Content)
|
||||
}
|
||||
|
||||
if file.IsScript {
|
||||
t.Error("GUIDE.md should not be marked as script")
|
||||
}
|
||||
|
||||
// Load script file
|
||||
scriptFile, err := loader.LoadSkillFile("test-skill", "scripts/hello.py")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load script file: %v", err)
|
||||
}
|
||||
|
||||
if !scriptFile.IsScript {
|
||||
t.Error("hello.py should be marked as script")
|
||||
}
|
||||
|
||||
t.Logf("Loaded files: GUIDE.md=%d bytes, hello.py=%d bytes (isScript=%v)",
|
||||
len(file.Content), len(scriptFile.Content), scriptFile.IsScript)
|
||||
}
|
||||
|
||||
func TestManagerIntegration(t *testing.T) {
|
||||
// Create a temporary skills directory
|
||||
tmpDir, err := os.MkdirTemp("", "skills-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create a test skill directory
|
||||
skillDir := filepath.Join(tmpDir, "test-skill")
|
||||
if err := os.MkdirAll(skillDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create skill dir: %v", err)
|
||||
}
|
||||
|
||||
// Write SKILL.md
|
||||
skillContent := `---
|
||||
name: test-skill
|
||||
description: A test skill for manager integration testing.
|
||||
---
|
||||
# Test Skill
|
||||
|
||||
Integration test content.
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(skillContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write SKILL.md: %v", err)
|
||||
}
|
||||
|
||||
// Create manager with config
|
||||
config := &ManagerConfig{
|
||||
SkillDirs: []string{tmpDir},
|
||||
AllowedSkills: []string{}, // Allow all
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
manager := NewManager(config, nil) // No sandbox for this test
|
||||
|
||||
// Initialize
|
||||
ctx := context.Background()
|
||||
if err := manager.Initialize(ctx); err != nil {
|
||||
t.Fatalf("Failed to initialize manager: %v", err)
|
||||
}
|
||||
|
||||
// Get all metadata
|
||||
metadata := manager.GetAllMetadata()
|
||||
if len(metadata) != 1 {
|
||||
t.Fatalf("Expected 1 skill, got %d", len(metadata))
|
||||
}
|
||||
|
||||
// Load skill
|
||||
skill, err := manager.LoadSkill(ctx, "test-skill")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load skill: %v", err)
|
||||
}
|
||||
|
||||
if skill.Name != "test-skill" {
|
||||
t.Errorf("Expected skill name 'test-skill', got '%s'", skill.Name)
|
||||
}
|
||||
|
||||
t.Logf("Manager integration test passed: %d skills discovered", len(metadata))
|
||||
}
|
||||
|
||||
func TestIsScript(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
{"script.py", true},
|
||||
{"script.sh", true},
|
||||
{"script.bash", true},
|
||||
{"script.js", true},
|
||||
{"script.mjs", true},
|
||||
{"script.cjs", true},
|
||||
{"script.ts", true},
|
||||
{"script.rb", true},
|
||||
{"README.md", false},
|
||||
{"data.json", false},
|
||||
{"config.yaml", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := IsScript(tt.path)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsScript(%s) = %v, expected %v", tt.path, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOnDemandInstallerPath(t *testing.T) {
|
||||
for _, path := range []string{
|
||||
"scripts/install_deps.py",
|
||||
"scripts/install_dep.sh",
|
||||
"setup_deps.py",
|
||||
"bootstrap_deps.py",
|
||||
} {
|
||||
if !IsOnDemandInstallerPath(path) {
|
||||
t.Errorf("IsOnDemandInstallerPath(%s) = false, want true", path)
|
||||
}
|
||||
}
|
||||
if IsOnDemandInstallerPath("scripts/generate_ppt.py") {
|
||||
t.Error("a real skill script must not look like an on-demand installer")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user