From 97960511368ed93b3bd9721e64cfdcaa7768e3e9 Mon Sep 17 00:00:00 2001 From: keithy Date: Thu, 5 Mar 2026 02:28:19 +0000 Subject: [PATCH] test(skills): add IndexRegistry tests --- cmd/picoclaw/internal/skills/install_test.go | 77 ++++----- pkg/skills/index_registry_test.go | 173 +++++++++++++++++++ 2 files changed, 209 insertions(+), 41 deletions(-) create mode 100644 pkg/skills/index_registry_test.go diff --git a/cmd/picoclaw/internal/skills/install_test.go b/cmd/picoclaw/internal/skills/install_test.go index 6b362822d..8c101ced6 100644 --- a/cmd/picoclaw/internal/skills/install_test.go +++ b/cmd/picoclaw/internal/skills/install_test.go @@ -30,65 +30,60 @@ func TestNewInstallSubcommand(t *testing.T) { func TestInstallCommandArgs(t *testing.T) { tests := []struct { name string - args []string registry string - expectError bool - errorMsg string + args []string + wantErr bool + errContains string }{ { - name: "no registry, one arg", - args: []string{"sipeed/picoclaw-skills/weather"}, - registry: "", - expectError: false, + name: "no registry with one arg passes", + registry: "", + args: []string{"sioeed/picoclaw-skills/weather"}, + wantErr: false, }, { - name: "no registry, no args", + name: "no registry with no args fails", + registry: "", + args: []string{}, + wantErr: true, + errContains: "exactly 1 argument", + }, + { + name: "with registry one arg passes", + registry: "clawhub", + args: []string{"github"}, + wantErr: false, + }, + { + name: "with registry zero args fails", + registry: "clawhub", args: []string{}, - registry: "", - expectError: true, - errorMsg: "exactly 1 argument is required: ", + wantErr: true, + errContains: "exactly 1 argument", }, { - name: "no registry, too many args", + name: "with registry two args fails", + registry: "clawhub", args: []string{"arg1", "arg2"}, - registry: "", - expectError: true, - errorMsg: "exactly 1 argument is required: ", - }, - { - name: "with registry, one arg", - args: []string{"weather-skill"}, - registry: "clawhub", - expectError: false, - }, - { - name: "with registry, no args", - args: []string{}, - registry: "clawhub", - expectError: true, - errorMsg: "when --registry is set, exactly 1 argument is required: ", - }, - { - name: "with registry, too many args", - args: []string{"arg1", "arg2"}, - registry: "clawhub", - expectError: true, - errorMsg: "when --registry is set, exactly 1 argument is required: ", + wantErr: true, + errContains: "exactly 1 argument", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := newInstallCommand(nil) + // Set the registry flag + err := cmd.Flags().Set("registry", tt.registry) + require.NoError(t, err) - if tt.registry != "" { - require.NoError(t, cmd.Flags().Set("registry", tt.registry)) - } + err = cmd.Args(cmd, tt.args) - err := cmd.Args(cmd, tt.args) - if tt.expectError { + if tt.wantErr { require.Error(t, err) - assert.Equal(t, tt.errorMsg, err.Error()) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } } else { require.NoError(t, err) } diff --git a/pkg/skills/index_registry_test.go b/pkg/skills/index_registry_test.go new file mode 100644 index 000000000..f48eba476 --- /dev/null +++ b/pkg/skills/index_registry_test.go @@ -0,0 +1,173 @@ +package skills + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIndexRegistryMapURL(t *testing.T) { + tests := []struct { + name string + mappings map[string]string + inputURL string + expectedURL string + }{ + { + name: "maps HTTP to fork", + mappings: map[string]string{ + "https://raw.githubusercontent.com/keithy/angelhub/main/": "https://raw.githubusercontent.com/myfork/angelhub/main/", + }, + inputURL: "https://raw.githubusercontent.com/keithy/angelhub/main/picoclaw/skills/self-config", + expectedURL: "https://raw.githubusercontent.com/myfork/angelhub/main/picoclaw/skills/self-config", + }, + { + name: "maps HTTP to local file", + mappings: map[string]string{ + "https://raw.githubusercontent.com/keithy/angelhub/": "file:///home/me/repos/angelhub/", + }, + inputURL: "https://raw.githubusercontent.com/keithy/angelhub/main/picoclaw/skills/self-config", + expectedURL: "file:///home/me/repos/angelhub/main/picoclaw/skills/self-config", + }, + { + name: "no mapping returns original", + mappings: map[string]string{ + "https://other.com/": "https://example.com/", + }, + inputURL: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + expectedURL: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + }, + { + name: "empty mappings returns original", + mappings: map[string]string{}, + inputURL: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + expectedURL: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + }, + { + name: "nil mappings returns original", + mappings: nil, + inputURL: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + expectedURL: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + }, + { + name: "first matching prefix wins", + mappings: map[string]string{ + "https://raw.githubusercontent.com/keithy/": "https://raw.githubusercontent.com/fork1/", + }, + inputURL: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + expectedURL: "https://raw.githubusercontent.com/fork1/angelhub/main/skills", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &IndexRegistry{ + name: "test", + urlMappings: tt.mappings, + } + result := r.mapURL(tt.inputURL) + assert.Equal(t, tt.expectedURL, result) + }) + } +} + +func TestIndexRegistryIsURLAllowed(t *testing.T) { + tests := []struct { + name string + allowedPrefixes []string + url string + expected bool + }{ + { + name: "matching prefix returns true", + allowedPrefixes: []string{"https://raw.githubusercontent.com/keithy/"}, + url: "https://raw.githubusercontent.com/keithy/angelhub/main/skills", + expected: true, + }, + { + name: "non-matching prefix returns false", + allowedPrefixes: []string{"https://raw.githubusercontent.com/keithy/"}, + url: "https://raw.githubusercontent.com/other/repo/main", + expected: false, + }, + { + name: "empty allowedPrefixes allows all", + allowedPrefixes: []string{}, + url: "https://any-site.com/file", + expected: true, + }, + { + name: "nil allowedPrefixes allows all", + allowedPrefixes: nil, + url: "https://any-site.com/file", + expected: true, + }, + { + name: "multiple prefixes first match wins", + allowedPrefixes: []string{"https://raw.githubusercontent.com/other/", "https://raw.githubusercontent.com/keithy/"}, + url: "https://raw.githubusercontent.com/keithy/angelhub/main", + expected: true, + }, + { + name: "prefix not at start returns false", + allowedPrefixes: []string{"https://raw.githubusercontent.com/keithy/"}, + url: "https://raw.githubusercontent.com/other/keithy/foo", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &IndexRegistry{ + name: "test", + allowedPrefixes: tt.allowedPrefixes, + } + result := r.isURLAllowed(tt.url) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIndexRegistryCopyLocalPath(t *testing.T) { + // Create temp directory for testing + tmpDir := t.TempDir() + tmpSkillDir := filepath.Join(tmpDir, "test-skill") + err := os.MkdirAll(filepath.Join(tmpSkillDir, "subdir"), 0o755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tmpSkillDir, "SKILL.md"), []byte("# Test Skill"), 0o644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tmpSkillDir, "subdir", "script.sh"), []byte("#!/bin/bash\necho hello"), 0o755) + require.NoError(t, err) + + t.Run("symlink_local true creates symlink", func(t *testing.T) { + targetDir := filepath.Join(tmpDir, "installed-skill") + r := &IndexRegistry{ + name: "test", + symlinkLocal: true, + } + err := r.copyLocalPath(tmpSkillDir, targetDir) + require.NoError(t, err) + + // Verify symlink was created + info, err := os.Lstat(targetDir) + require.NoError(t, err) + require.True(t, info.Mode()&os.ModeSymlink != 0, "should be a symlink") + + // Verify symlink points to correct path + linkDest, err := os.Readlink(targetDir) + require.NoError(t, err) + assert.Equal(t, tmpSkillDir, linkDest) + }) +} + +func TestIndexRegistrySearch(t *testing.T) { + // This test would require mocking HTTP - just verify the method signature works + r := &IndexRegistry{ + name: "test", + indexURL: "https://example.com/index.json", + } + assert.Equal(t, "test", r.Name()) +}