Files
agent-lib/internal/e2e/warn_test.go
T
m3ta-chiron 63619b90a4 feat: three-state sync planner with warning semantics
- Plan is a pure function (inventory, manifest, disk facts, injected path
  mapping) emitting exactly six actions: deploy, update, skip+warn,
  leave+warn, remove, keep — deterministic order, no filesystem access
- full 3x3 matrix (disk unmodified/user-modified/never-managed x upstream
  added/changed/removed) table-tested plus missing-disk restore cases,
  a mixed fixture and a purity/determinism guarantee test
- executor applies plans item by item (atomic deploy/remove per item),
  rewrites the manifest once, appends timestamped SKIP/LEAVE lines with
  item name and reason to the sync log
- sync now plans before mutating: unmanaged occupants block new deploys,
  upstream deletions remove only unmodified copies, local modifications
  always win and are warned about
2026-08-23 10:36:44 +02:00

181 lines
5.9 KiB
Go

package e2e
import (
"os"
"path/filepath"
"testing"
)
func syncOnce(t *testing.T, env *syncEnv) {
t.Helper()
if out, err := env.run(t); err != nil {
t.Fatalf("sync failed: %v\n%s", err, out)
}
}
func commitWorkRepo(t *testing.T, h *harness, dir, msg string) {
t.Helper()
h.git(t, dir, "add", "-A")
h.git(t, dir, "commit", "-m", msg)
}
func TestSyncSkipsLocallyModifiedItem(t *testing.T) {
h := newHarness(t)
env := newSyncEnv(t, h)
buildWorkRepo(t, h, env.workRepo)
env.writeConfig(t, env.workRepo, "")
syncOnce(t, env)
skillPath := filepath.Join(env.home, ".agents/skills/own-skill/SKILL.md")
os.WriteFile(skillPath, []byte("---\nname: Own Skill\n---\n# MY local edits\n"), 0o644)
p := filepath.Join(env.workRepo, "skills/own-skill/SKILL.md")
os.WriteFile(p, []byte("---\nname: Own Skill\ndescription: changed upstream\n---\n# Upstream v2\n"), 0o644)
commitWorkRepo(t, h, env.workRepo, "change own-skill upstream")
out, err := env.run(t)
if err != nil {
t.Fatalf("sync with local mods must not fail: %v\n%s", err, out)
}
if !contains(out, "skipped (local modifications kept): 1") {
t.Errorf("modified item must be skipped with a warning:\n%s", out)
}
kept := readFile(t, skillPath)
if !contains(kept, "MY local edits") {
t.Error("local version must be kept, not overwritten")
}
log := readFile(t, filepath.Join(env.stateDir, "sync.log"))
if !contains(log, "SKIP skills/own-skill") || !contains(log, "locally modified") {
t.Errorf("warning log must record item and reason:\n%s", log)
}
manifest := readFile(t, filepath.Join(env.stateDir, "manifest.json"))
if !contains(manifest, "own-skill") {
t.Error("skipped item must keep its manifest record for future syncs")
}
}
func TestSyncLeavesSelfCreatedItemsUntouched(t *testing.T) {
h := newHarness(t)
env := newSyncEnv(t, h)
buildWorkRepo(t, h, env.workRepo)
env.writeConfig(t, env.workRepo, "")
syncOnce(t, env)
mineDir := filepath.Join(env.home, ".agents/skills/my-secret-skill")
os.MkdirAll(mineDir, 0o755)
os.WriteFile(filepath.Join(mineDir, "SKILL.md"), []byte("---\nname: Mine\n---\n# private\n"), 0o644)
out, err := env.run(t)
if err != nil {
t.Fatalf("sync must not fail on unmanaged items: %v\n%s", err, out)
}
if !contains(out, "unmanaged items left untouched: 1") {
t.Errorf("self-created item must be reported as left:\n%s", out)
}
if _, err := os.Stat(filepath.Join(mineDir, "SKILL.md")); err != nil {
t.Error("self-created item must survive untouched")
}
log := readFile(t, filepath.Join(env.stateDir, "sync.log"))
if !contains(log, "LEAVE skills/my-secret-skill") || !contains(log, "unmanaged local item") {
t.Errorf("leave warning must be logged with reason:\n%s", log)
}
manifest := readFile(t, filepath.Join(env.stateDir, "manifest.json"))
if contains(manifest, "my-secret-skill") {
t.Error("unmanaged items must never enter the manifest")
}
}
func TestSyncRemovesUpstreamDeletedItems(t *testing.T) {
h := newHarness(t)
env := newSyncEnv(t, h)
buildWorkRepo(t, h, env.workRepo)
env.writeConfig(t, env.workRepo, "")
syncOnce(t, env)
os.RemoveAll(filepath.Join(env.workRepo, "skills/own-skill"))
os.Remove(filepath.Join(env.workRepo, "agents/helper.md"))
os.Remove(filepath.Join(env.workRepo, "commands/deploy.md"))
commitWorkRepo(t, h, env.workRepo, "remove everything deployable")
modifiedAgent := filepath.Join(env.home, ".config/opencode/agents/helper.md")
os.WriteFile(modifiedAgent, []byte("---\nname: Helper\n---\n# my tweaks\n"), 0o644)
out, err := env.run(t)
if err != nil {
t.Fatalf("sync with removals failed: %v\n%s", err, out)
}
if !contains(out, "removed: 2") {
t.Errorf("unmodified deleted items must be removed:\n%s", out)
}
if _, err := os.Stat(filepath.Join(env.home, ".agents/skills/own-skill")); err == nil {
t.Error("unmodified skill must be removed after upstream deletion")
}
if _, err := os.Stat(modifiedAgent); err != nil {
t.Error("locally modified agent must survive the upstream deletion")
}
if !contains(out, "skipped (local modifications kept): 1") {
t.Errorf("modified+deleted item must be skipped with warning:\n%s", out)
}
log := readFile(t, filepath.Join(env.stateDir, "sync.log"))
if !contains(log, "upstream deleted but the local copy was modified") {
t.Errorf("log must explain the kept deletion:\n%s", log)
}
manifest := readFile(t, filepath.Join(env.stateDir, "manifest.json"))
if contains(manifest, "own-skill") || contains(manifest, "deploy") {
t.Errorf("removed items must leave the manifest:\n%s", manifest)
}
if !contains(manifest, "helper") {
t.Error("kept modified item must keep its manifest record")
}
}
func TestSyncRestoresDeletedManagedItem(t *testing.T) {
h := newHarness(t)
env := newSyncEnv(t, h)
buildWorkRepo(t, h, env.workRepo)
env.writeConfig(t, env.workRepo, "")
syncOnce(t, env)
os.RemoveAll(filepath.Join(env.home, ".agents/skills/own-skill"))
out, err := env.run(t)
if err != nil {
t.Fatalf("restore sync failed: %v\n%s", err, out)
}
if !contains(out, "updated: 1") {
t.Errorf("locally deleted managed item must be restored:\n%s", out)
}
if _, err := os.Stat(filepath.Join(env.home, ".agents/skills/own-skill/SKILL.md")); err != nil {
t.Error("item must be back on disk")
}
}
func TestSyncNewUpstreamItemBlockedByUnmanagedOccupant(t *testing.T) {
h := newHarness(t)
env := newSyncEnv(t, h)
buildWorkRepo(t, h, env.workRepo)
mineDir := filepath.Join(env.home, ".agents/skills/own-skill")
os.MkdirAll(mineDir, 0o755)
os.WriteFile(filepath.Join(mineDir, "SKILL.md"), []byte("---\nname: My Own\n---\n# private version\n"), 0o644)
env.writeConfig(t, env.workRepo, "")
out, err := env.run(t)
if err != nil {
t.Fatalf("sync must not fail on occupant conflict: %v\n%s", err, out)
}
if !contains(out, "skipped (local modifications kept): 1") {
t.Errorf("new item blocked by unmanaged occupant must be skipped:\n%s", out)
}
kept := readFile(t, filepath.Join(mineDir, "SKILL.md"))
if !contains(kept, "private version") {
t.Error("occupant content must be preserved")
}
}