garden/lock.go: Lock() and Unlock() toggle the locked flag on existing tracked entries. Errors on untracked paths. Persists to manifest. cmd/sgard/lock.go: sgard lock <path>..., sgard unlock <path>... 6 tests: lock/unlock existing entry, persistence, error on untracked, checkpoint behavior changes after lock, status changes between drifted and modified after unlock. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
198 lines
4.4 KiB
Go
198 lines
4.4 KiB
Go
package garden
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLockExistingEntry(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoDir := filepath.Join(root, "repo")
|
|
|
|
g, err := Init(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
|
|
testFile := filepath.Join(root, "testfile")
|
|
if err := os.WriteFile(testFile, []byte("data"), 0o644); err != nil {
|
|
t.Fatalf("writing: %v", err)
|
|
}
|
|
|
|
// Add without lock.
|
|
if err := g.Add([]string{testFile}); err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
|
|
if g.manifest.Files[0].Locked {
|
|
t.Fatal("should not be locked initially")
|
|
}
|
|
|
|
// Lock it.
|
|
if err := g.Lock([]string{testFile}); err != nil {
|
|
t.Fatalf("Lock: %v", err)
|
|
}
|
|
|
|
if !g.manifest.Files[0].Locked {
|
|
t.Error("should be locked after Lock()")
|
|
}
|
|
|
|
// Verify persisted.
|
|
g2, err := Open(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
if !g2.manifest.Files[0].Locked {
|
|
t.Error("locked state should persist")
|
|
}
|
|
}
|
|
|
|
func TestUnlockExistingEntry(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoDir := filepath.Join(root, "repo")
|
|
|
|
g, err := Init(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
|
|
testFile := filepath.Join(root, "testfile")
|
|
if err := os.WriteFile(testFile, []byte("data"), 0o644); err != nil {
|
|
t.Fatalf("writing: %v", err)
|
|
}
|
|
|
|
if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
|
|
if !g.manifest.Files[0].Locked {
|
|
t.Fatal("should be locked")
|
|
}
|
|
|
|
if err := g.Unlock([]string{testFile}); err != nil {
|
|
t.Fatalf("Unlock: %v", err)
|
|
}
|
|
|
|
if g.manifest.Files[0].Locked {
|
|
t.Error("should not be locked after Unlock()")
|
|
}
|
|
}
|
|
|
|
func TestLockUntrackedErrors(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoDir := filepath.Join(root, "repo")
|
|
|
|
g, err := Init(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
|
|
testFile := filepath.Join(root, "nottracked")
|
|
if err := os.WriteFile(testFile, []byte("data"), 0o644); err != nil {
|
|
t.Fatalf("writing: %v", err)
|
|
}
|
|
|
|
if err := g.Lock([]string{testFile}); err == nil {
|
|
t.Fatal("Lock on untracked path should error")
|
|
}
|
|
}
|
|
|
|
func TestLockChangesCheckpointBehavior(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoDir := filepath.Join(root, "repo")
|
|
|
|
g, err := Init(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
|
|
testFile := filepath.Join(root, "testfile")
|
|
if err := os.WriteFile(testFile, []byte("original"), 0o644); err != nil {
|
|
t.Fatalf("writing: %v", err)
|
|
}
|
|
|
|
// Add unlocked, checkpoint picks up changes.
|
|
if err := g.Add([]string{testFile}); err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
|
|
origHash := g.manifest.Files[0].Hash
|
|
|
|
if err := os.WriteFile(testFile, []byte("changed"), 0o644); err != nil {
|
|
t.Fatalf("modifying: %v", err)
|
|
}
|
|
|
|
if err := g.Checkpoint(""); err != nil {
|
|
t.Fatalf("Checkpoint: %v", err)
|
|
}
|
|
|
|
if g.manifest.Files[0].Hash == origHash {
|
|
t.Fatal("unlocked file: checkpoint should update hash")
|
|
}
|
|
|
|
newHash := g.manifest.Files[0].Hash
|
|
|
|
// Now lock it and modify again — checkpoint should NOT update.
|
|
if err := g.Lock([]string{testFile}); err != nil {
|
|
t.Fatalf("Lock: %v", err)
|
|
}
|
|
|
|
if err := os.WriteFile(testFile, []byte("system overwrote"), 0o644); err != nil {
|
|
t.Fatalf("overwriting: %v", err)
|
|
}
|
|
|
|
if err := g.Checkpoint(""); err != nil {
|
|
t.Fatalf("Checkpoint: %v", err)
|
|
}
|
|
|
|
if g.manifest.Files[0].Hash != newHash {
|
|
t.Error("locked file: checkpoint should not update hash")
|
|
}
|
|
}
|
|
|
|
func TestUnlockChangesStatusBehavior(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoDir := filepath.Join(root, "repo")
|
|
|
|
g, err := Init(repoDir)
|
|
if err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
|
|
testFile := filepath.Join(root, "testfile")
|
|
if err := os.WriteFile(testFile, []byte("original"), 0o644); err != nil {
|
|
t.Fatalf("writing: %v", err)
|
|
}
|
|
|
|
if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil {
|
|
t.Fatalf("Add: %v", err)
|
|
}
|
|
|
|
if err := os.WriteFile(testFile, []byte("changed"), 0o644); err != nil {
|
|
t.Fatalf("modifying: %v", err)
|
|
}
|
|
|
|
// Locked: should be "drifted".
|
|
statuses, err := g.Status()
|
|
if err != nil {
|
|
t.Fatalf("Status: %v", err)
|
|
}
|
|
if statuses[0].State != "drifted" {
|
|
t.Errorf("locked: expected drifted, got %s", statuses[0].State)
|
|
}
|
|
|
|
// Unlock: should now be "modified".
|
|
if err := g.Unlock([]string{testFile}); err != nil {
|
|
t.Fatalf("Unlock: %v", err)
|
|
}
|
|
|
|
statuses, err = g.Status()
|
|
if err != nil {
|
|
t.Fatalf("Status: %v", err)
|
|
}
|
|
if statuses[0].State != "modified" {
|
|
t.Errorf("unlocked: expected modified, got %s", statuses[0].State)
|
|
}
|
|
}
|