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>
106 lines
2.7 KiB
Go
106 lines
2.7 KiB
Go
package skillpack
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func sampleTree() Tree {
|
|
return Tree{
|
|
ManifestName: []byte(goodManifest),
|
|
"scripts/fill.py": []byte("print('hi')\n"),
|
|
"references/spec.md": []byte("# spec\n"),
|
|
}
|
|
}
|
|
|
|
func TestTreeDigest_StableAndContentSensitive(t *testing.T) {
|
|
a := sampleTree()
|
|
b := sampleTree()
|
|
if a.Digest() != b.Digest() {
|
|
t.Fatal("identical trees must share a digest")
|
|
}
|
|
b["scripts/fill.py"] = []byte("print('bye')\n")
|
|
if a.Digest() == b.Digest() {
|
|
t.Fatal("content change must change the digest")
|
|
}
|
|
// Adding a file changes the digest.
|
|
c := sampleTree()
|
|
c["extra.txt"] = []byte("x")
|
|
if a.Digest() == c.Digest() {
|
|
t.Fatal("added file must change the digest")
|
|
}
|
|
}
|
|
|
|
func TestLoadPack(t *testing.T) {
|
|
p, err := LoadPack(sampleTree())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.Manifest.Name != "pdf-processing" {
|
|
t.Errorf("name = %q", p.Manifest.Name)
|
|
}
|
|
if len(p.Bundled) != 2 || p.Bundled[0] != "references/spec.md" || p.Bundled[1] != "scripts/fill.py" {
|
|
t.Errorf("bundled = %v (want sorted, sans SKILL.md)", p.Bundled)
|
|
}
|
|
if p.Digest == "" {
|
|
t.Error("digest empty")
|
|
}
|
|
}
|
|
|
|
func TestLoadPack_NoManifest(t *testing.T) {
|
|
if _, err := LoadPack(Tree{"readme.md": []byte("x")}); err != ErrNoManifest {
|
|
t.Fatalf("want ErrNoManifest, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTreeWriteTo(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := sampleTree().WriteTo(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(filepath.Join(dir, "scripts", "fill.py"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "print('hi')\n" {
|
|
t.Errorf("staged content = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestReadTree_SkipsSymlinks(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, ManifestName), []byte(goodManifest), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A malicious pack pointing at a host file must NOT be read into the tree.
|
|
secret := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(secret, []byte("TOPSECRET"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(secret, filepath.Join(dir, "leak")); err != nil {
|
|
t.Skipf("symlink unsupported: %v", err)
|
|
}
|
|
tree, err := readTree(os.DirFS(dir))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := tree["leak"]; ok {
|
|
t.Fatal("symlink was followed into the tree — arbitrary host file read")
|
|
}
|
|
if _, ok := tree[ManifestName]; !ok {
|
|
t.Fatal("real file should still be read")
|
|
}
|
|
}
|
|
|
|
func TestTreeWriteTo_RejectsTraversal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
evil := Tree{"../escape.txt": []byte("nope")}
|
|
if err := evil.WriteTo(dir); err == nil {
|
|
t.Fatal("expected traversal rejection")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(filepath.Dir(dir), "escape.txt")); err == nil {
|
|
t.Fatal("traversal file was written outside dir")
|
|
}
|
|
}
|