bf0b67f9af
New additive, nil-safe battery for subscribing to skill packages in the Anthropic agent-skills format (SKILL.md manifest + bundled files): - Manifest/ParseManifest: SKILL.md frontmatter+body parse & validation (name/description required, allowed-tools passthrough, kebab/length limits) - Tree/Pack/LoadPack: self-contained file set, order-independent content digest (the pin identity + change signal), bundled-file listing, traversal- safe staging - Source (DirSource, GitSource): Fetch returns tree + resolved ref; git clones to temp, reads subpath into memory, cleans up (self-contained tree) - Subscription + Store + content-addressed PackCache, with Memory defaults - Syncer: Subscribe pins; Check records a PENDING update but never moves the pin; Apply is the only re-pin (supply-chain guard — upstream can't silently change what an agent runs) - Activate: resolved packs -> majordomo agent.Skill (catalog instructions + one skill_use tool) for progressive disclosure; Stage materializes files Third distinct 'skill' concept, deliberately separate from executus/skill (saved-agent noun) and majordomo/skill (eager capability bundle). Mort-side wiring (convars, .skillpack commands, Agent.SkillPacks, allowed-tools shim) is a later, separate step. Full unit + hermetic local-git tests; gofmt/vet clean; race-tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
2.3 KiB
Go
83 lines
2.3 KiB
Go
package skillpack
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const goodManifest = `---
|
|
name: pdf-processing
|
|
description: Extract text and tables from PDF files and fill forms.
|
|
license: MIT
|
|
allowed-tools: [Read, Bash]
|
|
metadata:
|
|
version: 1.2.0
|
|
---
|
|
# PDF Processing
|
|
|
|
Use pdfplumber for extraction.
|
|
`
|
|
|
|
func TestParseManifest_Good(t *testing.T) {
|
|
m, err := ParseManifest([]byte(goodManifest))
|
|
if err != nil {
|
|
t.Fatalf("ParseManifest: %v", err)
|
|
}
|
|
if m.Name != "pdf-processing" {
|
|
t.Errorf("name = %q", m.Name)
|
|
}
|
|
if !strings.HasPrefix(m.Description, "Extract text") {
|
|
t.Errorf("description = %q", m.Description)
|
|
}
|
|
if m.License != "MIT" {
|
|
t.Errorf("license = %q", m.License)
|
|
}
|
|
if len(m.AllowedTools) != 2 || m.AllowedTools[0] != "Read" || m.AllowedTools[1] != "Bash" {
|
|
t.Errorf("allowed-tools = %v", m.AllowedTools)
|
|
}
|
|
if m.Metadata["version"] != "1.2.0" {
|
|
t.Errorf("metadata version = %q", m.Metadata["version"])
|
|
}
|
|
if !strings.Contains(m.Body, "pdfplumber") || strings.Contains(m.Body, "---") {
|
|
t.Errorf("body not cleanly extracted: %q", m.Body)
|
|
}
|
|
}
|
|
|
|
func TestParseManifest_AllowedToolsScalar(t *testing.T) {
|
|
m, err := ParseManifest([]byte("---\nname: n\ndescription: d\nallowed-tools: \"Read, Bash , Grep\"\n---\nbody\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(m.AllowedTools) != 3 || m.AllowedTools[2] != "Grep" {
|
|
t.Errorf("scalar allowed-tools = %v", m.AllowedTools)
|
|
}
|
|
}
|
|
|
|
func TestParseManifest_Errors(t *testing.T) {
|
|
cases := map[string]string{
|
|
"no frontmatter": "# just a heading\n",
|
|
"unterminated": "---\nname: x\ndescription: y\n",
|
|
"missing name": "---\ndescription: y\n---\nb\n",
|
|
"missing desc": "---\nname: x\n---\nb\n",
|
|
"bad name uppercase": "---\nname: PdfProcessing\ndescription: d\n---\nb\n",
|
|
"bad name space": "---\nname: pdf processing\ndescription: d\n---\nb\n",
|
|
"bad yaml": "---\nname: [unclosed\n---\nb\n",
|
|
}
|
|
for label, in := range cases {
|
|
if _, err := ParseManifest([]byte(in)); err == nil {
|
|
t.Errorf("%s: expected error, got nil", label)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseManifest_LeadingBlanksAndCRLF(t *testing.T) {
|
|
in := "\r\n\n---\r\nname: ok-name\r\ndescription: fine\r\n---\r\nbody line\r\n"
|
|
m, err := ParseManifest([]byte(in))
|
|
if err != nil {
|
|
t.Fatalf("tolerant parse: %v", err)
|
|
}
|
|
if m.Name != "ok-name" || m.Body != "body line" {
|
|
t.Errorf("got name=%q body=%q", m.Name, m.Body)
|
|
}
|
|
}
|