feat: collision hard errors + rename resolution
- flat per-type deployed namespace enforced across own items and all sources - own-vs-vendored and vendored-vs-vendored collisions abort with both parties named - --rename upstream=deployed resolves collisions; on-disk folder always equals deployed name; rename targets are validated against new collisions - checks run before any tree mutation; validate re-checks the namespace offline
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/m3tam3re/agent-lib/internal/lockfile"
|
||||
)
|
||||
|
||||
func ownSkillFixture(h *harness, t *testing.T) {
|
||||
t.Helper()
|
||||
dir := filepath.Join(h.workDir, "skills", "good-skill")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("---\nname: Own Good\n---\n"), 0o644)
|
||||
}
|
||||
|
||||
func TestCollisionOwnVsVendored(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
ownSkillFixture(h, t)
|
||||
standardFixture(h, t)
|
||||
|
||||
out, err := h.run(t, "vendor", "add", "superpowers", h.upstreamDir)
|
||||
if err == nil {
|
||||
t.Fatalf("own-vs-vendored collision must fail:\n%s", out)
|
||||
}
|
||||
for _, want := range []string{"collides", "own", "good-skill"} {
|
||||
if !contains(out, want) {
|
||||
t.Errorf("error must contain %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(h.workDir, "external")); err == nil {
|
||||
t.Error("collision must abort before any tree mutation")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(h.workDir, lockfile.FileName)); err == nil {
|
||||
t.Error("collision must not write a lockfile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollisionVendoredVsVendored(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
standardFixture(h, t)
|
||||
h.mustRun(t, "vendor", "add", "first", h.upstreamDir)
|
||||
|
||||
out, err := h.run(t, "vendor", "add", "second", h.upstreamDir)
|
||||
if err == nil {
|
||||
t.Fatalf("vendored-vs-vendored collision must fail:\n%s", out)
|
||||
}
|
||||
for _, want := range []string{"collides", "first"} {
|
||||
if !contains(out, want) {
|
||||
t.Errorf("error must name both parties (%q missing):\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollisionResolvedByRename(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
ownSkillFixture(h, t)
|
||||
standardFixture(h, t)
|
||||
|
||||
out := h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir,
|
||||
"--rename", "good-skill=superpowers-good-skill")
|
||||
if !contains(out, "renamed") && !contains(out, "materialized") {
|
||||
t.Errorf("rename resolution should report success:\n%s", out)
|
||||
}
|
||||
|
||||
renamed := filepath.Join(h.workDir, "external/superpowers/skills/superpowers-good-skill/SKILL.md")
|
||||
if _, err := os.Stat(renamed); err != nil {
|
||||
t.Fatal("renamed folder must carry the deployed name")
|
||||
}
|
||||
original := filepath.Join(h.workDir, "external/superpowers/skills/good-skill")
|
||||
if _, err := os.Stat(original); err == nil {
|
||||
t.Error("upstream folder name must not appear after rename")
|
||||
}
|
||||
|
||||
lock := readFile(t, filepath.Join(h.workDir, lockfile.FileName))
|
||||
if !contains(lock, `"good-skill": "superpowers-good-skill"`) {
|
||||
t.Errorf("rename map must be pinned in the lockfile:\n%s", lock)
|
||||
}
|
||||
|
||||
if out := h.mustRun(t, "validate"); !contains(out, "valid") {
|
||||
t.Errorf("renamed state must validate:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollisionRenameToAnotherCollision(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
ownSkillFixture(h, t)
|
||||
standardFixture(h, t)
|
||||
|
||||
out, err := h.run(t, "vendor", "add", "superpowers", h.upstreamDir,
|
||||
"--rename", "good-skill=broken-skill")
|
||||
if err == nil {
|
||||
t.Fatalf("renaming onto an existing deployed name must fail:\n%s", out)
|
||||
}
|
||||
if !contains(out, "collides") {
|
||||
t.Errorf("error must report the collision:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDetectsHandEditedCollision(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
standardFixture(h, t)
|
||||
h.mustRun(t, "vendor", "add", "superpowers", h.upstreamDir)
|
||||
h.mustRun(t, "vendor", "add", "second-source", h.upstreamDir, "--include", "good-skill",
|
||||
"--rename", "good-skill=second-good")
|
||||
|
||||
mutateLockfile(t, h, func(lf *lockfile.Lockfile) {
|
||||
lf.Sources["second-source"].Renames["good-skill"] = "broken-skill"
|
||||
})
|
||||
|
||||
out, err := h.run(t, "validate")
|
||||
if err == nil {
|
||||
t.Fatalf("hand-edited rename collision must fail validation:\n%s", out)
|
||||
}
|
||||
if !contains(out, "collision") {
|
||||
t.Errorf("rule must be reported as collision:\n%s", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user