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") } }