2026-08-22 21:54:32 +02:00
|
|
|
package e2e
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/m3tam3re/agent-lib/internal/lockfile"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func healthySource(t *testing.T, h *harness) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
standardFixture(h, t)
|
|
|
|
|
h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func mutateLockfile(t *testing.T, h *harness, fn func(*lockfile.Lockfile)) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
path := filepath.Join(h.workDir, lockfile.FileName)
|
|
|
|
|
lf, err := lockfile.Load(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
fn(lf)
|
|
|
|
|
if err := lf.Save(path); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidateHealthy(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
|
|
|
|
|
|
|
|
|
out := h.mustRun(t, "validate")
|
|
|
|
|
if !contains(out, "valid") {
|
|
|
|
|
t.Errorf("healthy state must validate:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonOut := h.mustRun(t, "validate", "--json")
|
|
|
|
|
var result struct {
|
|
|
|
|
Valid bool `json:"valid"`
|
|
|
|
|
Errors []struct {
|
|
|
|
|
Source string `json:"source"`
|
|
|
|
|
Rule string `json:"rule"`
|
|
|
|
|
} `json:"errors"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal([]byte(jsonOut), &result); err != nil {
|
|
|
|
|
t.Fatalf("validate --json not parseable: %v\n%s", err, jsonOut)
|
|
|
|
|
}
|
|
|
|
|
if !result.Valid || len(result.Errors) != 0 {
|
|
|
|
|
t.Errorf("healthy state must be valid in json: %+v", result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidateAllWithInclude(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
|
|
|
|
mutateLockfile(t, h, func(lf *lockfile.Lockfile) {
|
|
|
|
|
lf.Sources["superpowers"].Selection.Include = []string{"good-skill"}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
out, err := h.run(t, "validate")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("all+include must fail:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
if !contains(out, "superpowers") || !contains(out, "include list is set") {
|
|
|
|
|
t.Errorf("error must name source and rule:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidateExcludeWithoutAll(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
|
|
|
|
mutateLockfile(t, h, func(lf *lockfile.Lockfile) {
|
|
|
|
|
lf.Sources["superpowers"].Selection.Mode = lockfile.ModeInclude
|
|
|
|
|
lf.Sources["superpowers"].Selection.Include = []string{"good-skill"}
|
|
|
|
|
lf.Sources["superpowers"].Selection.Exclude = []string{"scout"}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
out, err := h.run(t, "validate")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("exclude without all must fail:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
if !contains(out, "exclude list is only valid in all mode") {
|
|
|
|
|
t.Errorf("error must name the rule:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidateOrphanedInclude(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
|
|
|
|
mutateLockfile(t, h, func(lf *lockfile.Lockfile) {
|
|
|
|
|
lf.Sources["superpowers"].Selection.Mode = lockfile.ModeInclude
|
|
|
|
|
lf.Sources["superpowers"].Selection.Include = []string{"good-skill", "vanished-item"}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
out, err := h.run(t, "validate")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("orphaned include must fail:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
if !contains(out, "vanished-item") || !contains(out, "pinned inventory") {
|
|
|
|
|
t.Errorf("error must name source and item:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonOut, _ := h.run(t, "validate", "--json")
|
|
|
|
|
var result struct {
|
|
|
|
|
Valid bool `json:"valid"`
|
|
|
|
|
Errors []struct {
|
|
|
|
|
Source string `json:"source"`
|
|
|
|
|
} `json:"errors"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal([]byte(jsonOut), &result); err != nil {
|
|
|
|
|
t.Fatalf("json output not parseable: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if result.Valid || len(result.Errors) == 0 || result.Errors[0].Source != "superpowers" {
|
|
|
|
|
t.Errorf("json must carry the error with source: %+v", result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestValidateTreeLockfileDivergence(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
|
|
|
|
|
|
|
|
|
os.Remove(filepath.Join(h.workDir, "external/superpowers/agents/scout.md"))
|
|
|
|
|
out, err := h.run(t, "validate")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("missing tree item must fail:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
if !contains(out, "agents") || !contains(out, "scout") {
|
|
|
|
|
t.Errorf("error must name type and item:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stray := filepath.Join(h.workDir, "external/superpowers/commands/ghost.md")
|
|
|
|
|
os.WriteFile(stray, []byte("# ghost\n"), 0o644)
|
|
|
|
|
out, err = h.run(t, "validate")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("stray tree item must fail:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
if !contains(out, "ghost") {
|
|
|
|
|
t.Errorf("error must name the stray item:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestVendorList(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
2026-08-22 21:56:50 +02:00
|
|
|
h.mustRun(t, "vendor", "add", "curated", h.upstreamDir, "--include", "good-skill",
|
|
|
|
|
"--rename", "good-skill=curated-good")
|
2026-08-22 21:54:32 +02:00
|
|
|
|
|
|
|
|
out := h.mustRun(t, "vendor", "list")
|
|
|
|
|
for _, want := range []string{"superpowers", "curated", "all", "include", "main"} {
|
|
|
|
|
if !contains(out, want) {
|
|
|
|
|
t.Errorf("list output missing %q:\n%s", want, out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonOut := h.mustRun(t, "vendor", "list", "--json")
|
|
|
|
|
var parsed struct {
|
|
|
|
|
Sources []struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
URL string `json:"url"`
|
|
|
|
|
Ref string `json:"ref"`
|
|
|
|
|
Rev string `json:"rev"`
|
|
|
|
|
Mode string `json:"mode"`
|
|
|
|
|
Counts map[string]int `json:"counts"`
|
|
|
|
|
Total int `json:"total"`
|
|
|
|
|
} `json:"sources"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal([]byte(jsonOut), &parsed); err != nil {
|
|
|
|
|
t.Fatalf("list --json not parseable: %v\n%s", err, jsonOut)
|
|
|
|
|
}
|
|
|
|
|
if len(parsed.Sources) != 2 {
|
|
|
|
|
t.Fatalf("want 2 sources, got %d", len(parsed.Sources))
|
|
|
|
|
}
|
|
|
|
|
super := parsed.Sources[1]
|
|
|
|
|
if super.Name != "superpowers" || super.Mode != "all" || super.Total != 5 {
|
|
|
|
|
t.Errorf("superpowers summary wrong: %+v", super)
|
|
|
|
|
}
|
|
|
|
|
if super.Counts["skills"] != 2 || super.Counts["mcp"] != 1 {
|
|
|
|
|
t.Errorf("counts wrong: %+v", super.Counts)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestVendorInspect(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
|
|
|
|
|
|
|
|
|
out := h.mustRun(t, "vendor", "inspect", "superpowers")
|
|
|
|
|
for _, want := range []string{"good-skill", "broken-skill", "scout", "search", "name=Good Skill", "warning"} {
|
|
|
|
|
if !contains(out, want) {
|
|
|
|
|
t.Errorf("inspect output missing %q:\n%s", want, out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsonOut := h.mustRun(t, "vendor", "inspect", "superpowers", "--json")
|
|
|
|
|
var report struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Ref string `json:"ref"`
|
|
|
|
|
Items map[string][]struct {
|
|
|
|
|
UpstreamID string `json:"upstream_id"`
|
|
|
|
|
DeployedID string `json:"deployed_id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
Warnings []string `json:"warnings"`
|
|
|
|
|
} `json:"items"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal([]byte(jsonOut), &report); err != nil {
|
|
|
|
|
t.Fatalf("inspect --json not parseable: %v\n%s", err, jsonOut)
|
|
|
|
|
}
|
|
|
|
|
if report.Name != "superpowers" || len(report.Items["skills"]) != 2 {
|
|
|
|
|
t.Errorf("inspect json wrong: %+v", report)
|
|
|
|
|
}
|
|
|
|
|
for _, it := range report.Items["skills"] {
|
|
|
|
|
if it.UpstreamID == "good-skill" && it.Name != "Good Skill" {
|
|
|
|
|
t.Errorf("frontmatter metadata missing: %+v", it)
|
|
|
|
|
}
|
|
|
|
|
if it.UpstreamID == "broken-skill" && len(it.Warnings) == 0 {
|
|
|
|
|
t.Errorf("recorded warnings missing: %+v", it)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestVendorInspectUnknownSource(t *testing.T) {
|
|
|
|
|
h := newHarness(t)
|
|
|
|
|
healthySource(t, h)
|
|
|
|
|
|
|
|
|
|
out, err := h.run(t, "vendor", "inspect", "nope")
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("unknown source must fail:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
if !contains(out, "not in the lockfile") {
|
|
|
|
|
t.Errorf("error must be explicit:\n%s", out)
|
|
|
|
|
}
|
|
|
|
|
}
|