package integrationtools import ( "context" "encoding/json" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/sipeed/picoclaw/pkg/skills" ) type mockInstallRegistry struct{} const validSkillMarkdown = "---\nname: pr-review\ndescription: Review pull requests\n---\n# PR Review\n" func (m *mockInstallRegistry) Name() string { return "clawhub" } func (m *mockInstallRegistry) ResolveInstallDirName(target string) (string, error) { return target, nil } func (m *mockInstallRegistry) SkillURL(slug, _ string) string { return slug } func (m *mockInstallRegistry) Search(context.Context, string, int) ([]skills.SearchResult, error) { return nil, nil } func (m *mockInstallRegistry) GetSkillMeta(context.Context, string) (*skills.SkillMeta, error) { return nil, nil } func (m *mockInstallRegistry) DownloadAndInstall( _ context.Context, _ string, _ string, targetDir string, ) (*skills.InstallResult, error) { if err := os.MkdirAll(targetDir, 0o755); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(targetDir, "SKILL.md"), []byte(validSkillMarkdown), 0o600); err != nil { return nil, err } return &skills.InstallResult{Version: "test"}, nil } type mockGitHubInstallRegistry struct{} func (m *mockGitHubInstallRegistry) Name() string { return "github" } func (m *mockGitHubInstallRegistry) ResolveInstallDirName(target string) (string, error) { return "pr-review", nil } func (m *mockGitHubInstallRegistry) SkillURL(slug, _ string) string { return slug } func (m *mockGitHubInstallRegistry) Search(context.Context, string, int) ([]skills.SearchResult, error) { return nil, nil } func (m *mockGitHubInstallRegistry) GetSkillMeta(context.Context, string) (*skills.SkillMeta, error) { return nil, nil } func (m *mockGitHubInstallRegistry) DownloadAndInstall( _ context.Context, _ string, _ string, targetDir string, ) (*skills.InstallResult, error) { if err := os.MkdirAll(targetDir, 0o755); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(targetDir, "SKILL.md"), []byte(validSkillMarkdown), 0o600); err != nil { return nil, err } return &skills.InstallResult{Version: "main"}, nil } type stubGitHubInstallRegistry struct { *skills.GitHubRegistry } func (m *stubGitHubInstallRegistry) DownloadAndInstall( _ context.Context, _ string, _ string, targetDir string, ) (*skills.InstallResult, error) { if err := os.MkdirAll(targetDir, 0o755); err != nil { return nil, err } if err := os.WriteFile(filepath.Join(targetDir, "SKILL.md"), []byte(validSkillMarkdown), 0o600); err != nil { return nil, err } return &skills.InstallResult{Version: "main"}, nil } type mockInvalidInstallRegistry struct{} type mockFailingInstallRegistry struct{} func (m *mockInvalidInstallRegistry) Name() string { return "clawhub" } func (m *mockInvalidInstallRegistry) ResolveInstallDirName(target string) (string, error) { return target, nil } func (m *mockInvalidInstallRegistry) SkillURL(slug, _ string) string { return slug } func (m *mockInvalidInstallRegistry) Search(context.Context, string, int) ([]skills.SearchResult, error) { return nil, nil } func (m *mockInvalidInstallRegistry) GetSkillMeta(context.Context, string) (*skills.SkillMeta, error) { return nil, nil } func (m *mockInvalidInstallRegistry) DownloadAndInstall( _ context.Context, _ string, _ string, targetDir string, ) (*skills.InstallResult, error) { if err := os.MkdirAll(targetDir, 0o755); err != nil { return nil, err } if err := os.WriteFile( filepath.Join(targetDir, "SKILL.md"), []byte("---\nname: bad_skill\ndescription: invalid name\n---\n# Invalid\n"), 0o600, ); err != nil { return nil, err } return &skills.InstallResult{Version: "test"}, nil } func (m *mockFailingInstallRegistry) Name() string { return "clawhub" } func (m *mockFailingInstallRegistry) ResolveInstallDirName(target string) (string, error) { return target, nil } func (m *mockFailingInstallRegistry) SkillURL(slug, _ string) string { return slug } func (m *mockFailingInstallRegistry) Search(context.Context, string, int) ([]skills.SearchResult, error) { return nil, nil } func (m *mockFailingInstallRegistry) GetSkillMeta(context.Context, string) (*skills.SkillMeta, error) { return nil, nil } func (m *mockFailingInstallRegistry) DownloadAndInstall( _ context.Context, _ string, _ string, _ string, ) (*skills.InstallResult, error) { return nil, assert.AnError } func TestInstallSkillToolName(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) assert.Equal(t, "install_skill", tool.Name()) } func TestInstallSkillToolMissingSlug(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) result := tool.Execute(context.Background(), map[string]any{}) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "identifier is required and must be a non-empty string") } func TestInstallSkillToolEmptySlug(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) result := tool.Execute(context.Background(), map[string]any{ "slug": " ", }) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "identifier is required and must be a non-empty string") } func TestInstallSkillToolUnsafeSlug(t *testing.T) { registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(skills.NewClawHubRegistry(skills.ClawHubConfig{Enabled: true})) tool := NewInstallSkillTool(registryMgr, t.TempDir()) cases := []string{ "../etc/passwd", "path/traversal", "path\\traversal", } for _, slug := range cases { result := tool.Execute(context.Background(), map[string]any{ "slug": slug, "registry": "clawhub", }) assert.True(t, result.IsError, "slug %q should be rejected", slug) assert.Contains(t, result.ForLLM, "invalid slug") } } func TestInstallSkillToolAlreadyExists(t *testing.T) { workspace := t.TempDir() skillDir := filepath.Join(workspace, "skills", "existing-skill") require.NoError(t, os.MkdirAll(skillDir, 0o755)) registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&mockInstallRegistry{}) tool := NewInstallSkillTool(registryMgr, workspace) result := tool.Execute(context.Background(), map[string]any{ "slug": "existing-skill", "registry": "clawhub", }) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "already installed") } func TestInstallSkillToolRegistryNotFound(t *testing.T) { workspace := t.TempDir() tool := NewInstallSkillTool(skills.NewRegistryManager(), workspace) result := tool.Execute(context.Background(), map[string]any{ "slug": "some-skill", "registry": "nonexistent", }) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "registry") assert.Contains(t, result.ForLLM, "not found") } func TestInstallSkillToolParameters(t *testing.T) { tool := NewInstallSkillTool(skills.NewRegistryManager(), t.TempDir()) params := tool.Parameters() props, ok := params["properties"].(map[string]any) assert.True(t, ok) assert.Contains(t, props, "slug") assert.Contains(t, props, "version") assert.Contains(t, props, "registry") assert.Contains(t, props, "force") required, ok := params["required"].([]string) assert.True(t, ok) assert.Contains(t, required, "slug") assert.NotContains(t, required, "registry") } func TestInstallSkillToolMissingRegistry(t *testing.T) { registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&mockGitHubInstallRegistry{}) tool := NewInstallSkillTool(registryMgr, t.TempDir()) result := tool.Execute(context.Background(), map[string]any{ "slug": "some-skill", }) assert.False(t, result.IsError) assert.Contains(t, result.ForLLM, `Successfully installed skill`) } func TestInstallSkillToolAllowsGitHubURLSlug(t *testing.T) { registry := skills.GitHubRegistryConfig{Enabled: true, BaseURL: "https://github.com"}.BuildRegistry() githubRegistry, ok := registry.(*skills.GitHubRegistry) require.True(t, ok) registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&stubGitHubInstallRegistry{GitHubRegistry: githubRegistry}) workspace := t.TempDir() tool := NewInstallSkillTool(registryMgr, workspace) slug := "https://github.com/synthetic-lab/octofriend/tree/main/.agents/skills/pr-review" result := tool.Execute(context.Background(), map[string]any{ "slug": slug, "registry": "github", }) assert.False(t, result.IsError) assert.Contains(t, result.ForLLM, `Successfully installed skill`) data, err := os.ReadFile(filepath.Join(workspace, "skills", "pr-review", ".skill-origin.json")) require.NoError(t, err) var meta originMeta require.NoError(t, json.Unmarshal(data, &meta)) assert.Equal(t, "third_party", meta.OriginKind) assert.Equal(t, "github", meta.Registry) assert.Equal(t, "synthetic-lab/octofriend/.agents/skills/pr-review", meta.Slug) assert.Equal(t, slug, meta.RegistryURL) assert.Equal(t, "main", meta.InstalledVersion) assert.NotZero(t, meta.InstalledAt) } func TestInstallSkillToolPreservesGitHubSourceURLWithEnterpriseRegistry(t *testing.T) { registry := skills.GitHubRegistryConfig{Enabled: true, BaseURL: "https://ghe.example.com/git"}.BuildRegistry() githubRegistry, ok := registry.(*skills.GitHubRegistry) require.True(t, ok) registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&stubGitHubInstallRegistry{GitHubRegistry: githubRegistry}) workspace := t.TempDir() tool := NewInstallSkillTool(registryMgr, workspace) slug := "https://github.com/synthetic-lab/octofriend/tree/main/.agents/skills/pr-review" result := tool.Execute(context.Background(), map[string]any{ "slug": slug, "registry": "github", }) assert.False(t, result.IsError) data, err := os.ReadFile(filepath.Join(workspace, "skills", "pr-review", ".skill-origin.json")) require.NoError(t, err) var meta originMeta require.NoError(t, json.Unmarshal(data, &meta)) assert.Equal(t, "synthetic-lab/octofriend/.agents/skills/pr-review", meta.Slug) assert.Equal(t, slug, meta.RegistryURL) assert.Equal(t, "main", meta.InstalledVersion) } func TestInstallSkillToolRejectsInvalidInstalledSkill(t *testing.T) { workspace := t.TempDir() registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&mockInvalidInstallRegistry{}) tool := NewInstallSkillTool(registryMgr, workspace) result := tool.Execute(context.Background(), map[string]any{ "slug": "broken-skill", "registry": "clawhub", }) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "not a valid skill") _, err := os.Stat(filepath.Join(workspace, "skills", "broken-skill")) assert.True(t, os.IsNotExist(err)) } func TestInstallSkillToolRollsBackOnOriginMetadataWriteFailure(t *testing.T) { workspace := t.TempDir() registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&mockInstallRegistry{}) tool := NewInstallSkillTool(registryMgr, workspace) previousPersist := persistInstalledSkillOriginMeta persistInstalledSkillOriginMeta = func(string, skills.SkillRegistry, string, string) error { return assert.AnError } defer func() { persistInstalledSkillOriginMeta = previousPersist }() result := tool.Execute(context.Background(), map[string]any{ "slug": "rollback-skill", "registry": "clawhub", }) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "failed to persist skill metadata") _, err := os.Stat(filepath.Join(workspace, "skills", "rollback-skill")) assert.True(t, os.IsNotExist(err)) } func TestInstallSkillToolForceReinstallRestoresPreviousSkillAfterDownloadFailure(t *testing.T) { workspace := t.TempDir() skillDir := filepath.Join(workspace, "skills", "existing-skill") require.NoError(t, os.MkdirAll(skillDir, 0o755)) oldContent := []byte("---\nname: existing-skill\ndescription: Existing skill\n---\n# Existing\n") require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), oldContent, 0o600)) registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&mockFailingInstallRegistry{}) tool := NewInstallSkillTool(registryMgr, workspace) result := tool.Execute(context.Background(), map[string]any{ "slug": "existing-skill", "registry": "clawhub", "force": true, }) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "failed to install") gotContent, err := os.ReadFile(filepath.Join(skillDir, "SKILL.md")) require.NoError(t, err) assert.Equal(t, oldContent, gotContent) } func TestInstallSkillToolForceReinstallRestoresPreviousSkillAfterMetadataFailure(t *testing.T) { workspace := t.TempDir() skillDir := filepath.Join(workspace, "skills", "existing-skill") require.NoError(t, os.MkdirAll(skillDir, 0o755)) oldContent := []byte("---\nname: existing-skill\ndescription: Existing skill\n---\n# Existing\n") require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), oldContent, 0o600)) registryMgr := skills.NewRegistryManager() registryMgr.AddRegistry(&mockInstallRegistry{}) tool := NewInstallSkillTool(registryMgr, workspace) previousPersist := persistInstalledSkillOriginMeta persistInstalledSkillOriginMeta = func(string, skills.SkillRegistry, string, string) error { return assert.AnError } defer func() { persistInstalledSkillOriginMeta = previousPersist }() result := tool.Execute(context.Background(), map[string]any{ "slug": "existing-skill", "registry": "clawhub", "force": true, }) assert.True(t, result.IsError) assert.Contains(t, result.ForLLM, "failed to persist skill metadata") gotContent, err := os.ReadFile(filepath.Join(skillDir, "SKILL.md")) require.NoError(t, err) assert.Equal(t, oldContent, gotContent) }