Files
agent-lib/internal/e2e/sync_force_test.go
m3ta-chiron 1ab97bff43 feat(client): sync --force to escape the locally-modified skip
Managed items with local modifications were kept (skip + warn) with no
escape hatch other than deleting the deployed file by hand — after an
upstream change the deployed content silently stayed old, which reads
exactly like a stale cache.

--force turns the two skip cases into update (repository version wins)
and remove (upstream deleted + modified). Unmanaged items and never-
managed path occupancies stay untouched even with --force; the default
behavior is unchanged.
2026-10-02 12:34:57 +02:00

84 lines
2.9 KiB
Go

package e2e
import (
"os"
"path/filepath"
"testing"
)
// TestSyncForceOverwritesLocallyModified pins the --force escape hatch:
// locally modified managed items are overwritten with the repository
// version, upstream-deleted modified items are removed, and unmanaged
// local items are never touched.
func TestSyncForceOverwritesLocallyModified(t *testing.T) {
h := newHarness(t)
env := newSyncEnv(t, h)
buildWorkRepo(t, h, env.workRepo)
env.writeConfig(t, env.workRepo, "")
env.run(t)
// local drift on a managed item + upstream change to the same item
deployed := filepath.Join(env.home, ".config/opencode/commands/deploy.md")
os.WriteFile(deployed, []byte("---\nname: Deploy\n---\nlocal drift\n"), 0o644)
upstream := filepath.Join(env.workRepo, "commands/deploy.md")
os.WriteFile(upstream, []byte("---\nname: Deploy\ndescription: updated\n---\nnew body\n"), 0o644)
// an unmanaged local item that must survive --force untouched
unmanaged := filepath.Join(env.home, ".config/opencode/commands/mine.md")
os.MkdirAll(filepath.Dir(unmanaged), 0o755)
os.WriteFile(unmanaged, []byte("---\nname: Mine\n---\nprecious\n"), 0o644)
h.git(t, env.workRepo, "add", "-A")
h.git(t, env.workRepo, "commit", "-m", "update deploy command")
out, err := env.run(t)
if err != nil {
t.Fatalf("sync failed: %v\n%s", err, out)
}
if !contains(out, "skipped (local modifications kept): 1") {
t.Errorf("default sync must keep the local modification:\n%s", out)
}
out, err = env.run(t, "--force")
if err != nil {
t.Fatalf("sync --force failed: %v\n%s", err, out)
}
if !contains(out, "updated: 1") {
t.Errorf("force must update the locally modified item:\n%s", out)
}
if got := readFile(t, deployed); !contains(got, "new body") {
t.Error("force must overwrite the local drift with the repository version")
}
if got := readFile(t, unmanaged); !contains(got, "precious") {
t.Error("force must never touch unmanaged local items")
}
// upstream deletion of a locally modified item: force removes it
os.WriteFile(deployed, []byte("---\nname: Deploy\n---\nmore drift\n"), 0o644)
os.Remove(upstream)
h.git(t, env.workRepo, "add", "-A")
h.git(t, env.workRepo, "commit", "-m", "delete deploy command")
out, err = env.run(t)
if err != nil {
t.Fatalf("sync failed: %v\n%s", err, out)
}
if !contains(out, "skipped (local modifications kept): 1") {
t.Errorf("default sync must keep the modified item after upstream deletion:\n%s", out)
}
if _, err := os.Stat(deployed); err != nil {
t.Fatal("item must still exist before force")
}
out, err = env.run(t, "--force")
if err != nil {
t.Fatalf("sync --force failed: %v\n%s", err, out)
}
if !contains(out, "removed: 1") {
t.Errorf("force must remove the locally modified item after upstream deletion:\n%s", out)
}
if _, err := os.Stat(deployed); !os.IsNotExist(err) {
t.Error("forced removal must delete the deployed file")
}
}