9bb5d143f7
executus CI / test (pull_request) Successful in 3m30s
Real issues from the PR review: - security: readTree now skips symlinks (a pack with SKILL.md -> /etc/passwd or scripts/x -> ../../.ssh/id_rsa could read host files); covers file and dir symlinks, incl. within a git subpath - security: GitSource rejects url/ref beginning with '-' (git arg injection) and clones with '--' separator; --filter=blob:none (blobless partial clone) instead of full-history clone - correctness: Subscribe no longer swallows a non-ErrNotFound store error from GetByName (would create a duplicate subscription); handles *GitSource as well as GitSource in the URL/subpath extraction - correctness: pinTo no longer renames a subscription, so Apply can't silently collide two subscriptions when an upstream pack changes its name - validation: isKebab rejects leading/trailing/consecutive hyphens; BOM- prefixed SKILL.md now parses (matches the doc comment) - robustness: Catalog/Activate/renderPackBody/Stage guard nil/malformed packs - test cleanup: Syncer.Store field renamed Cache (collided with the Store interface); test NewID returns distinct ids - tests: symlink-skip, BOM, strict-kebab, nil-pack-safety Deferred (advisory perf, documented): PackCache stores raw trees so activation re-parses; CheckAll is serial. Both fine at expected scale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
package skillpack
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func mustPack(t *testing.T, name, body string, extra map[string]string) *Pack {
|
|
t.Helper()
|
|
tr := packTree(name, body)
|
|
for k, v := range extra {
|
|
tr[k] = []byte(v)
|
|
}
|
|
p, err := LoadPack(tr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestCatalog(t *testing.T) {
|
|
packs := []*Pack{
|
|
mustPack(t, "zebra", "z", nil),
|
|
mustPack(t, "alpha", "a", nil),
|
|
}
|
|
cat := Catalog(packs)
|
|
if !strings.Contains(cat, "skill_use") {
|
|
t.Error("catalog should tell the model how to load a skill")
|
|
}
|
|
ai := strings.Index(cat, "alpha")
|
|
zi := strings.Index(cat, "zebra")
|
|
if ai < 0 || zi < 0 || ai > zi {
|
|
t.Errorf("catalog should list packs sorted by name:\n%s", cat)
|
|
}
|
|
if Catalog(nil) != "" {
|
|
t.Error("empty catalog should be empty string")
|
|
}
|
|
}
|
|
|
|
func TestActivate_SkillUseTool(t *testing.T) {
|
|
ctx := context.Background()
|
|
packs := []*Pack{
|
|
mustPack(t, "pdf", "Use pdfplumber.", map[string]string{"scripts/x.py": "print()"}),
|
|
}
|
|
sk := Activate(packs, "/stage")
|
|
if sk == nil {
|
|
t.Fatal("expected a non-nil skill")
|
|
}
|
|
if sk.Instructions() != Catalog(packs) {
|
|
t.Error("skill instructions should be the catalog")
|
|
}
|
|
tb := sk.Tools()
|
|
tool, ok := tb.Get("skill_use")
|
|
if !ok {
|
|
t.Fatal("skill_use tool missing from toolbox")
|
|
}
|
|
|
|
// load an existing pack
|
|
out, err := tool.Handler(ctx, json.RawMessage(`{"name":"pdf"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := out.(string)
|
|
if !strings.Contains(body, "Use pdfplumber.") {
|
|
t.Errorf("skill_use body missing instructions: %q", body)
|
|
}
|
|
if !strings.Contains(body, "scripts/x.py") || !strings.Contains(body, "/stage/pdf") {
|
|
t.Errorf("skill_use should list bundled files under the staged dir: %q", body)
|
|
}
|
|
|
|
// unknown pack returns guidance, not an error
|
|
out, err = tool.Handler(ctx, json.RawMessage(`{"name":"nope"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s, _ := out.(string); !strings.Contains(s, "No skill named") {
|
|
t.Errorf("unknown skill should return guidance: %q", s)
|
|
}
|
|
}
|
|
|
|
func TestActivate_Empty(t *testing.T) {
|
|
if Activate(nil, "") != nil {
|
|
t.Error("no packs should activate to a nil skill")
|
|
}
|
|
}
|
|
|
|
func TestNilPackElementsAreSafe(t *testing.T) {
|
|
packs := []*Pack{nil, mustPack(t, "real", "b", nil), {Manifest: nil}}
|
|
// Neither Catalog nor Activate may panic on nil / malformed elements.
|
|
if got := Catalog(packs); !strings.Contains(got, "real") {
|
|
t.Errorf("catalog should include the valid pack and skip nils: %q", got)
|
|
}
|
|
sk := Activate(packs, "")
|
|
if sk == nil {
|
|
t.Fatal("a valid pack among nils should still activate")
|
|
}
|
|
if _, ok := sk.Tools().Get("skill_use"); !ok {
|
|
t.Error("skill_use missing")
|
|
}
|
|
// All-nil activates to nothing rather than panicking.
|
|
if Activate([]*Pack{nil, {Manifest: nil}}, "") != nil {
|
|
t.Error("only-nil packs should activate to nil")
|
|
}
|
|
}
|
|
|
|
func TestResolveFromCache(t *testing.T) {
|
|
ctx := context.Background()
|
|
cache := NewMemoryPackCache()
|
|
p := mustPack(t, "alpha", "a", nil)
|
|
cache.Put(ctx, p.Digest, p.Tree)
|
|
|
|
subs := []Subscription{
|
|
{Name: "alpha", PinnedDigest: p.Digest, Enabled: true},
|
|
{Name: "disabled", PinnedDigest: p.Digest, Enabled: false},
|
|
}
|
|
packs, err := Resolve(ctx, cache, subs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(packs) != 1 || packs[0].Manifest.Name != "alpha" {
|
|
t.Fatalf("resolve should skip disabled subs; got %d packs", len(packs))
|
|
}
|
|
|
|
// missing from cache is an error
|
|
subs = []Subscription{{Name: "ghost", PinnedDigest: "deadbeef", Enabled: true}}
|
|
if _, err := Resolve(ctx, cache, subs); err == nil {
|
|
t.Fatal("expected error resolving an uncached pin")
|
|
}
|
|
}
|
|
|
|
func TestStage(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := mustPack(t, "pdf", "b", map[string]string{"scripts/x.py": "print()"})
|
|
staged, err := Stage(p, dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasSuffix(staged, "/pdf") {
|
|
t.Errorf("staged dir = %q", staged)
|
|
}
|
|
}
|