package e2e import ( "os" "path/filepath" "testing" "github.com/m3tam3re/agent-lib/internal/lockfile" ) func bumpUpstream(h *harness, t *testing.T) { t.Helper() h.write(t, "skills/fresh-skill/SKILL.md", "---\nname: Fresh\n---\n# Fresh\n") h.write(t, "skills/good-skill/SKILL.md", "---\nname: Good Skill\ndescription: changed upstream\n---\n# Changed\n") os.RemoveAll(filepath.Join(h.upstreamDir, "skills/broken-skill")) os.Remove(filepath.Join(h.upstreamDir, "agents/scout.md")) h.commitUpstream(t, "fixture: add fresh, change good, remove broken+scout") } func TestVendorUpdateHappyPath(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir) oldLock := readFile(t, filepath.Join(h.workDir, lockfile.FileName)) bumpUpstream(h, t) out := h.mustRun(t, "vendor", "update", "superpowers") for _, want := range []string{"fresh-skill (added)", "good-skill (changed)", "broken-skill (removed)", "scout (removed)"} { if !contains(out, want) { t.Errorf("report missing %q:\n%s", want, out) } } newSkill := filepath.Join(h.workDir, "external/superpowers/skills/fresh-skill/SKILL.md") if _, err := os.Stat(newSkill); err != nil { t.Error("added skill must be materialized") } if _, err := os.Stat(filepath.Join(h.workDir, "external/superpowers/skills/broken-skill")); err == nil { t.Error("removed skill must be gone from the external area") } if _, err := os.Stat(filepath.Join(h.workDir, "external/superpowers/agents/scout.md")); err == nil { t.Error("removed agent must be gone from the external area") } newLock := readFile(t, filepath.Join(h.workDir, lockfile.FileName)) if newLock == oldLock { t.Fatal("lockfile must advance its rev") } if !contains(newLock, "fresh-skill") || contains(newLock, "broken-skill") { t.Errorf("inventory must reflect the new selection:\n%s", newLock) } if out := h.mustRun(t, "validate"); !contains(out, "valid") { t.Errorf("updated state must validate:\n%s", out) } } func TestVendorUpdateIncludeModeHoldsSelection(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "curated", h.upstreamDir, "--include", "good-skill") bumpUpstream(h, t) diffOut := h.mustRun(t, "vendor", "diff", "curated") if !contains(diffOut, "fresh-skill (available upstream, not selected)") { t.Errorf("diff must surface new upstream items as available:\n%s", diffOut) } out := h.mustRun(t, "vendor", "update", "curated") if contains(out, "fresh-skill (added)") { t.Errorf("include mode must not pull new upstream items:\n%s", out) } if _, err := os.Stat(filepath.Join(h.workDir, "external/curated/skills/fresh-skill")); err == nil { t.Error("fresh-skill must not be materialized in include mode") } if !contains(out, "good-skill (changed)") { t.Errorf("included item changes must still arrive:\n%s", out) } } func TestVendorUpdateVanishedIncludeFailsLoud(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "curated", h.upstreamDir, "--include", "good-skill") lockBefore := readFile(t, filepath.Join(h.workDir, lockfile.FileName)) os.RemoveAll(filepath.Join(h.upstreamDir, "skills/good-skill")) h.commitUpstream(t, "fixture: remove good-skill") out, err := h.run(t, "vendor", "update", "curated") if err == nil { t.Fatalf("vanished include entry must fail the update:\n%s", out) } if !contains(out, "good-skill") || !contains(out, "not found upstream") { t.Errorf("error must name the vanished entry:\n%s", out) } if got := readFile(t, filepath.Join(h.workDir, lockfile.FileName)); got != lockBefore { t.Error("failed update must leave the lockfile untouched") } } func TestVendorUpdateUpToDateIdempotent(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir) lockBefore := readFile(t, filepath.Join(h.workDir, lockfile.FileName)) out := h.mustRun(t, "vendor", "update", "superpowers") if !contains(out, "already up to date") { t.Errorf("unchanged upstream must report up-to-date:\n%s", out) } if got := readFile(t, filepath.Join(h.workDir, lockfile.FileName)); got != lockBefore { t.Error("up-to-date update must not touch the lockfile") } } func TestVendorDiffReadOnly(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir) lockBefore := readFile(t, filepath.Join(h.workDir, lockfile.FileName)) out := h.mustRun(t, "vendor", "diff", "superpowers") if !contains(out, "up to date") { t.Errorf("unchanged upstream diff:\n%s", out) } bumpUpstream(h, t) out = h.mustRun(t, "vendor", "diff", "superpowers") for _, want := range []string{"fresh-skill", "good-skill", "broken-skill"} { if !contains(out, want) { t.Errorf("diff must list %q:\n%s", want, out) } } if got := readFile(t, filepath.Join(h.workDir, lockfile.FileName)); got != lockBefore { t.Error("diff must not modify the lockfile") } if _, err := os.Stat(filepath.Join(h.workDir, "external/superpowers/skills/fresh-skill")); err == nil { t.Error("diff must not materialize anything") } if _, err := os.Stat(filepath.Join(h.workDir, "external/superpowers/skills/broken-skill")); err != nil { t.Error("diff must not remove anything") } } func TestVendorRemoveLeavesNoResidue(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir) out := h.mustRun(t, "vendor", "remove", "superpowers") if !contains(out, "removed") { t.Errorf("remove must confirm:\n%s", out) } if _, err := os.Stat(filepath.Join(h.workDir, "external/superpowers")); err == nil { t.Error("external area must be gone") } if _, err := os.Stat(filepath.Join(h.workDir, "external")); err == nil { t.Error("empty external root must be cleaned up") } lock := readFile(t, filepath.Join(h.workDir, lockfile.FileName)) if contains(lock, "superpowers") { t.Errorf("lockfile entry must be gone:\n%s", lock) } } func TestVendorUpdateAll(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "alpha", h.upstreamDir) h.mustRun(t, "vendor", "add", "beta", h.upstreamDir, "--include", "good-skill", "--rename", "good-skill=beta-good") bumpUpstream(h, t) out := h.mustRun(t, "vendor", "update", "--all") if !contains(out, "alpha") || !contains(out, "beta") { t.Errorf("update --all must report per source:\n%s", out) } if _, err := os.Stat(filepath.Join(h.workDir, "external/alpha/skills/fresh-skill/SKILL.md")); err != nil { t.Error("alpha (all mode) must receive fresh-skill") } if _, err := os.Stat(filepath.Join(h.workDir, "external/beta/skills/fresh-skill")); err == nil { t.Error("beta (include mode) must not receive fresh-skill") } if _, err := os.Stat(filepath.Join(h.workDir, "external/beta/skills/beta-good/SKILL.md")); err != nil { t.Error("beta rename must survive the update") } } func TestVendorUpdateCollisionWithOwn(t *testing.T) { h := newHarness(t) standardFixture(h, t) h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir) lockBefore := readFile(t, filepath.Join(h.workDir, lockfile.FileName)) ownDir := filepath.Join(h.workDir, "skills", "fresh-skill") if err := os.MkdirAll(ownDir, 0o755); err != nil { t.Fatal(err) } os.WriteFile(filepath.Join(ownDir, "SKILL.md"), []byte("---\nname: My Own Fresh\n---\n"), 0o644) h.write(t, "skills/fresh-skill/SKILL.md", "---\nname: Upstream Fresh\n---\n") h.commitUpstream(t, "fixture: add fresh-skill that collides") out, err := h.run(t, "vendor", "update", "superpowers") if err == nil { t.Fatalf("upstream collision with own item must fail:\n%s", out) } if !contains(out, "collides") || !contains(out, "own") { t.Errorf("error must name both parties:\n%s", out) } if got := readFile(t, filepath.Join(h.workDir, lockfile.FileName)); got != lockBefore { t.Error("failed update must not touch the lockfile") } if _, err := os.Stat(filepath.Join(h.workDir, "external/superpowers/skills/good-skill/SKILL.md")); err != nil { t.Error("previous external area must survive the failed update") } }