206 lines
6.9 KiB
Go
206 lines
6.9 KiB
Go
package pkgmgr
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// writePkg writes a package.json (and optional extra files) into a fresh temp
|
|
// dir and returns the dir. extraFiles maps a relative path to its contents; a
|
|
// path ending in "/" is created as a directory.
|
|
func writePkg(t *testing.T, packageJSON string, extraFiles map[string]string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, body := range extraFiles {
|
|
p := filepath.Join(dir, name)
|
|
if body == "" && name[len(name)-1] == '/' {
|
|
if err := os.MkdirAll(p, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return dir
|
|
}
|
|
|
|
// TestClassifyExplicitType verifies a single explicit pi.type is recognized.
|
|
func TestClassifyExplicitType(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"pi-web","version":"1.0.0","pi":{"type":"skill"}}`, nil)
|
|
name, version, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
if name != "pi-web" || version != "1.0.0" {
|
|
t.Errorf("name/version = %q/%q, want pi-web/1.0.0", name, version)
|
|
}
|
|
if !reflect.DeepEqual(types, []PackageType{TypeSkill}) {
|
|
t.Errorf("types = %v, want [skill]", types)
|
|
}
|
|
}
|
|
|
|
// TestClassifyMultiType verifies a package declaring several types via pi.types
|
|
// returns them all, sorted.
|
|
func TestClassifyMultiType(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"combo","version":"2.0.0","pi":{"types":["extension","skill"]}}`, nil)
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
want := []PackageType{TypeExtension, TypeSkill}
|
|
if !reflect.DeepEqual(types, want) {
|
|
t.Errorf("types = %v, want %v", types, want)
|
|
}
|
|
}
|
|
|
|
// TestClassifyPerCapabilityKeys verifies pi.extension + pi.theme blocks both
|
|
// register their types.
|
|
func TestClassifyPerCapabilityKeys(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"x","version":"0.1.0","pi":{"extension":{"cmd":"x"},"theme":{}}}`, nil)
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
want := []PackageType{TypeExtension, TypeTheme}
|
|
if !reflect.DeepEqual(types, want) {
|
|
t.Errorf("types = %v, want %v", types, want)
|
|
}
|
|
}
|
|
|
|
// TestClassifyPluralCapabilityKeys verifies the pi-ecosystem convention of
|
|
// plural path arrays (pi.extensions, pi.skills, ...) registers each type. This
|
|
// is the shape published packages actually use (pi-simplify, pi-ask-user, ...).
|
|
func TestClassifyPluralCapabilityKeys(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"pi-ask-user","version":"1.0.0","pi":{"extensions":["./index.ts"],"skills":["./skills"]}}`, nil)
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
want := []PackageType{TypeExtension, TypeSkill}
|
|
if !reflect.DeepEqual(types, want) {
|
|
t.Errorf("types = %v, want %v", types, want)
|
|
}
|
|
}
|
|
|
|
// TestClassifyPluralExtensionsOnly verifies a pure extension declared via
|
|
// pi.extensions (no bin) classifies as an extension — the pi-simplify case.
|
|
func TestClassifyPluralExtensionsOnly(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"pi-simplify","version":"0.2.3","pi":{"extensions":["dist/index.js"]}}`, nil)
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(types, []PackageType{TypeExtension}) {
|
|
t.Errorf("types = %v, want [extension]", types)
|
|
}
|
|
}
|
|
|
|
// TestClassifyStructuralBin verifies a bare package with a bin entry is an
|
|
// extension even without pi metadata.
|
|
func TestClassifyStructuralBin(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"pi-mcp-adapter","version":"1.0.0","bin":{"pi-mcp-adapter":"./index.js"}}`, nil)
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(types, []PackageType{TypeExtension}) {
|
|
t.Errorf("types = %v, want [extension]", types)
|
|
}
|
|
}
|
|
|
|
// TestClassifyStructuralSkillMd verifies a SKILL.md file signals a skill.
|
|
func TestClassifyStructuralSkillMd(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"pi-skill","version":"1.0.0"}`, map[string]string{
|
|
"SKILL.md": "# a skill",
|
|
})
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(types, []PackageType{TypeSkill}) {
|
|
t.Errorf("types = %v, want [skill]", types)
|
|
}
|
|
}
|
|
|
|
// TestClassifyStructuralCommandsDir verifies a commands/ dir signals a prompt.
|
|
func TestClassifyStructuralCommandsDir(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"pi-cmds","version":"1.0.0"}`, map[string]string{
|
|
"commands/hello.md": "hi",
|
|
})
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(types, []PackageType{TypePrompt}) {
|
|
t.Errorf("types = %v, want [prompt]", types)
|
|
}
|
|
}
|
|
|
|
// TestClassifyExplicitAndStructural verifies explicit metadata and structural
|
|
// signals union together (skill via SKILL.md, extension via pi.type).
|
|
func TestClassifyExplicitAndStructural(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"both","version":"1.0.0","pi":{"type":"extension"}}`, map[string]string{
|
|
"SKILL.md": "# skill",
|
|
})
|
|
_, _, types, err := Classify(dir)
|
|
if err != nil {
|
|
t.Fatalf("Classify: %v", err)
|
|
}
|
|
want := []PackageType{TypeExtension, TypeSkill}
|
|
if !reflect.DeepEqual(types, want) {
|
|
t.Errorf("types = %v, want %v", types, want)
|
|
}
|
|
}
|
|
|
|
// TestClassifyUnknown verifies a plain npm package with no pi signals errors.
|
|
func TestClassifyUnknown(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"lodash","version":"4.17.21"}`, nil)
|
|
_, _, _, err := Classify(dir)
|
|
if err == nil {
|
|
t.Fatal("Classify of non-pi package = nil error, want error")
|
|
}
|
|
if !contains(err.Error(), "unrecognized pi package") {
|
|
t.Errorf("error = %q, want to mention 'unrecognized pi package'", err)
|
|
}
|
|
}
|
|
|
|
// TestClassifyMissingPackageJSON verifies a missing package.json errors.
|
|
func TestClassifyMissingPackageJSON(t *testing.T) {
|
|
if _, _, _, err := Classify(t.TempDir()); err == nil {
|
|
t.Fatal("Classify with no package.json = nil error, want error")
|
|
}
|
|
}
|
|
|
|
// TestClassifyCorruptPackageJSON verifies malformed JSON errors clearly.
|
|
func TestClassifyCorruptPackageJSON(t *testing.T) {
|
|
dir := writePkg(t, `{not valid json`, nil)
|
|
_, _, _, err := Classify(dir)
|
|
if err == nil {
|
|
t.Fatal("Classify with corrupt package.json = nil error, want error")
|
|
}
|
|
if !contains(err.Error(), "parse package.json") {
|
|
t.Errorf("error = %q, want to mention 'parse package.json'", err)
|
|
}
|
|
}
|
|
|
|
// TestClassifyUnknownTypeStringIgnored verifies an unrecognized pi.type string
|
|
// is ignored (not fatal) but leaves the package unclassified if nothing else
|
|
// matches.
|
|
func TestClassifyUnknownTypeStringIgnored(t *testing.T) {
|
|
dir := writePkg(t, `{"name":"x","version":"1.0.0","pi":{"type":"widget"}}`, nil)
|
|
_, _, _, err := Classify(dir)
|
|
if err == nil {
|
|
t.Fatal("Classify with only unknown pi.type = nil error, want error")
|
|
}
|
|
}
|