29598df814
executus CI / test (pull_request) Successful in 2m19s
Replace Activate's stagedDir string with a BundleStager callback invoked lazily inside skill_use: when the model loads a pack with bundled files, the host stages them (mort: into run-scoped file storage) and the returned note is appended to the body so the model knows how to reach them. A nil stager (or a stager error) degrades gracefully to just listing the file names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
4.1 KiB
Go
155 lines
4.1 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()"}),
|
|
}
|
|
staged := 0
|
|
stager := func(_ context.Context, p *Pack) (string, error) {
|
|
staged++
|
|
return "staged " + p.Manifest.Name + " (file_id=abc)", nil
|
|
}
|
|
sk := Activate(packs, stager)
|
|
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")
|
|
}
|
|
if staged != 0 {
|
|
t.Error("stager must be lazy — not called until skill_use runs")
|
|
}
|
|
|
|
// 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") {
|
|
t.Errorf("skill_use should list bundled files: %q", body)
|
|
}
|
|
if staged != 1 || !strings.Contains(body, "file_id=abc") {
|
|
t.Errorf("stager should run on load and its note append to the body: staged=%d body=%q", staged, 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) != 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, nil)
|
|
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) != 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)
|
|
}
|
|
}
|