Files
sgard/garden/locked_test.go
Kyle Isom 0929d77e90 Add locked files and directory-only entries.
Locked files (--lock): repo-authoritative entries. Checkpoint skips
them (preserves repo version). Status reports "drifted" instead of
"modified". Restore always overwrites if hash differs, no prompt.
Use case: system-managed files the OS overwrites.

Directory-only entries (--dir): track directory itself without
recursing. Restore ensures directory exists with correct permissions.
Use case: directories that must exist but contents are managed
elsewhere.

Add refactored to use AddOptions struct (Encrypt, Lock, DirOnly)
instead of variadic bools.

Proto: ManifestEntry gains locked field. convert.go updated.
7 new tests. ARCHITECTURE.md and README.md updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:56:57 -07:00

230 lines
5.6 KiB
Go

package garden
import (
"os"
"path/filepath"
"testing"
)
func TestAddLocked(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("locked content\n"), 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.Error("entry should be locked")
}
}
func TestCheckpointSkipsLocked(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)
}
origHash := g.manifest.Files[0].Hash
// Modify the file — checkpoint should NOT update the hash.
if err := os.WriteFile(testFile, []byte("system overwrote this"), 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.Error("checkpoint should skip locked files — hash should not change")
}
}
func TestStatusReportsDrifted(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)
}
// Modify — status should report "drifted" not "modified".
if err := os.WriteFile(testFile, []byte("system changed this"), 0o644); err != nil {
t.Fatalf("modifying: %v", err)
}
statuses, err := g.Status()
if err != nil {
t.Fatalf("Status: %v", err)
}
if len(statuses) != 1 || statuses[0].State != "drifted" {
t.Errorf("expected drifted, got %v", statuses)
}
}
func TestRestoreAlwaysRestoresLocked(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("correct content"), 0o644); err != nil {
t.Fatalf("writing: %v", err)
}
if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil {
t.Fatalf("Add: %v", err)
}
// System overwrites the file.
if err := os.WriteFile(testFile, []byte("system garbage"), 0o644); err != nil {
t.Fatalf("overwriting: %v", err)
}
// Restore without --force — locked files should still be restored.
if err := g.Restore(nil, false, nil); err != nil {
t.Fatalf("Restore: %v", err)
}
got, err := os.ReadFile(testFile)
if err != nil {
t.Fatalf("reading: %v", err)
}
if string(got) != "correct content" {
t.Errorf("content = %q, want %q", got, "correct content")
}
}
func TestRestoreSkipsLockedWhenHashMatches(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("content"), 0o644); err != nil {
t.Fatalf("writing: %v", err)
}
if err := g.Add([]string{testFile}, AddOptions{Lock: true}); err != nil {
t.Fatalf("Add: %v", err)
}
// File is unchanged — restore should skip it (no unnecessary writes).
if err := g.Restore(nil, false, nil); err != nil {
t.Fatalf("Restore: %v", err)
}
// If we got here without error, it means it didn't try to overwrite
// an identical file, which is correct.
}
func TestAddDirOnly(t *testing.T) {
root := t.TempDir()
repoDir := filepath.Join(root, "repo")
g, err := Init(repoDir)
if err != nil {
t.Fatalf("Init: %v", err)
}
// Create a directory with a file inside.
testDir := filepath.Join(root, "testdir")
if err := os.MkdirAll(testDir, 0o755); err != nil {
t.Fatalf("creating dir: %v", err)
}
if err := os.WriteFile(filepath.Join(testDir, "file"), []byte("data"), 0o644); err != nil {
t.Fatalf("writing: %v", err)
}
// Add with --dir — should NOT recurse.
if err := g.Add([]string{testDir}, AddOptions{DirOnly: true}); err != nil {
t.Fatalf("Add: %v", err)
}
if len(g.manifest.Files) != 1 {
t.Fatalf("expected 1 entry (directory), got %d", len(g.manifest.Files))
}
if g.manifest.Files[0].Type != "directory" {
t.Errorf("type = %s, want directory", g.manifest.Files[0].Type)
}
if g.manifest.Files[0].Hash != "" {
t.Error("directory entry should have no hash")
}
}
func TestDirOnlyRestoreCreatesDirectory(t *testing.T) {
root := t.TempDir()
repoDir := filepath.Join(root, "repo")
g, err := Init(repoDir)
if err != nil {
t.Fatalf("Init: %v", err)
}
testDir := filepath.Join(root, "testdir")
if err := os.MkdirAll(testDir, 0o755); err != nil {
t.Fatalf("creating dir: %v", err)
}
if err := g.Add([]string{testDir}, AddOptions{DirOnly: true}); err != nil {
t.Fatalf("Add: %v", err)
}
// Remove directory.
_ = os.RemoveAll(testDir)
// Restore should recreate it.
if err := g.Restore(nil, true, nil); err != nil {
t.Fatalf("Restore: %v", err)
}
info, err := os.Stat(testDir)
if err != nil {
t.Fatalf("directory not restored: %v", err)
}
if !info.IsDir() {
t.Error("restored path should be a directory")
}
}