package garden import ( "os" "path/filepath" "testing" ) func TestInitCreatesStructure(t *testing.T) { root := t.TempDir() repoDir := filepath.Join(root, "repo") g, err := Init(repoDir) if err != nil { t.Fatalf("Init: %v", err) } // manifest.yaml should exist if _, err := os.Stat(filepath.Join(repoDir, "manifest.yaml")); err != nil { t.Errorf("manifest.yaml not found: %v", err) } // blobs/ directory should exist if _, err := os.Stat(filepath.Join(repoDir, "blobs")); err != nil { t.Errorf("blobs/ not found: %v", err) } if g.manifest.Version != 1 { t.Errorf("expected version 1, got %d", g.manifest.Version) } if len(g.manifest.Files) != 0 { t.Errorf("expected 0 files, got %d", len(g.manifest.Files)) } } func TestInitRejectsExisting(t *testing.T) { root := t.TempDir() repoDir := filepath.Join(root, "repo") if _, err := Init(repoDir); err != nil { t.Fatalf("first Init: %v", err) } if _, err := Init(repoDir); err == nil { t.Fatal("second Init should fail on existing repo") } } func TestOpenLoadsRepo(t *testing.T) { root := t.TempDir() repoDir := filepath.Join(root, "repo") if _, err := Init(repoDir); err != nil { t.Fatalf("Init: %v", err) } g, err := Open(repoDir) if err != nil { t.Fatalf("Open: %v", err) } if g.manifest.Version != 1 { t.Errorf("expected version 1, got %d", g.manifest.Version) } } func TestAddFile(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 file to add. testFile := filepath.Join(root, "testfile") if err := os.WriteFile(testFile, []byte("hello world\n"), 0o644); err != nil { t.Fatalf("writing test file: %v", err) } if err := g.Add([]string{testFile}); err != nil { t.Fatalf("Add: %v", err) } if len(g.manifest.Files) != 1 { t.Fatalf("expected 1 file, got %d", len(g.manifest.Files)) } entry := g.manifest.Files[0] if entry.Type != "file" { t.Errorf("expected type file, got %s", entry.Type) } if entry.Hash == "" { t.Error("expected non-empty hash") } if entry.Mode != "0644" { t.Errorf("expected mode 0644, got %s", entry.Mode) } // Verify the blob was stored. if !g.store.Exists(entry.Hash) { t.Error("blob not found in store") } // Verify manifest was persisted. g2, err := Open(repoDir) if err != nil { t.Fatalf("re-Open: %v", err) } if len(g2.manifest.Files) != 1 { t.Errorf("persisted manifest has %d files, want 1", len(g2.manifest.Files)) } } func TestAddDirectory(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.Mkdir(testDir, 0o755); err != nil { t.Fatalf("creating test dir: %v", err) } if err := g.Add([]string{testDir}); err != nil { t.Fatalf("Add: %v", err) } entry := g.manifest.Files[0] if entry.Type != "directory" { t.Errorf("expected type directory, got %s", entry.Type) } if entry.Hash != "" { t.Error("directories should have no hash") } } func TestAddSymlink(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 target and a symlink to it. target := filepath.Join(root, "target") if err := os.WriteFile(target, []byte("target content"), 0o644); err != nil { t.Fatalf("writing target: %v", err) } link := filepath.Join(root, "link") if err := os.Symlink(target, link); err != nil { t.Fatalf("creating symlink: %v", err) } if err := g.Add([]string{link}); err != nil { t.Fatalf("Add: %v", err) } entry := g.manifest.Files[0] if entry.Type != "link" { t.Errorf("expected type link, got %s", entry.Type) } if entry.Target != target { t.Errorf("expected target %s, got %s", target, entry.Target) } if entry.Hash != "" { t.Error("symlinks should have no hash") } } func TestAddDuplicateRejected(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 test file: %v", err) } if err := g.Add([]string{testFile}); err != nil { t.Fatalf("first Add: %v", err) } if err := g.Add([]string{testFile}); err == nil { t.Fatal("second Add of same path should fail") } } func TestHashFile(t *testing.T) { root := t.TempDir() testFile := filepath.Join(root, "testfile") if err := os.WriteFile(testFile, []byte("hello"), 0o644); err != nil { t.Fatalf("writing test file: %v", err) } hash, err := HashFile(testFile) if err != nil { t.Fatalf("HashFile: %v", err) } // SHA-256 of "hello" expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" if hash != expected { t.Errorf("expected %s, got %s", expected, hash) } } func TestCheckpointDetectsChanges(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 test file: %v", err) } if err := g.Add([]string{testFile}); err != nil { t.Fatalf("Add: %v", err) } origHash := g.manifest.Files[0].Hash // Modify the file. if err := os.WriteFile(testFile, []byte("modified"), 0o644); err != nil { t.Fatalf("modifying test file: %v", err) } if err := g.Checkpoint("test checkpoint"); err != nil { t.Fatalf("Checkpoint: %v", err) } if g.manifest.Files[0].Hash == origHash { t.Error("checkpoint did not update hash for modified file") } if g.manifest.Message != "test checkpoint" { t.Errorf("expected message 'test checkpoint', got %q", g.manifest.Message) } // Verify new blob exists in store. if !g.store.Exists(g.manifest.Files[0].Hash) { t.Error("new blob not found in store") } // Verify manifest persisted. g2, err := Open(repoDir) if err != nil { t.Fatalf("re-Open: %v", err) } if g2.manifest.Files[0].Hash == origHash { t.Error("persisted manifest still has old hash") } } func TestCheckpointUnchangedFile(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("same"), 0o644); err != nil { t.Fatalf("writing test file: %v", err) } if err := g.Add([]string{testFile}); err != nil { t.Fatalf("Add: %v", err) } origHash := g.manifest.Files[0].Hash origUpdated := g.manifest.Files[0].Updated if err := g.Checkpoint(""); err != nil { t.Fatalf("Checkpoint: %v", err) } if g.manifest.Files[0].Hash != origHash { t.Error("hash should not change for unmodified file") } if !g.manifest.Files[0].Updated.Equal(origUpdated) { t.Error("entry timestamp should not change for unmodified file") } } func TestCheckpointMissingFileSkipped(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 test file: %v", err) } if err := g.Add([]string{testFile}); err != nil { t.Fatalf("Add: %v", err) } // Remove the file before checkpoint. os.Remove(testFile) // Checkpoint should not fail. if err := g.Checkpoint(""); err != nil { t.Fatalf("Checkpoint should not fail for missing file: %v", err) } } func TestStatusReportsCorrectly(t *testing.T) { root := t.TempDir() repoDir := filepath.Join(root, "repo") g, err := Init(repoDir) if err != nil { t.Fatalf("Init: %v", err) } // Create and add two files. okFile := filepath.Join(root, "okfile") if err := os.WriteFile(okFile, []byte("unchanged"), 0o644); err != nil { t.Fatalf("writing ok file: %v", err) } modFile := filepath.Join(root, "modfile") if err := os.WriteFile(modFile, []byte("original"), 0o644); err != nil { t.Fatalf("writing mod file: %v", err) } missingFile := filepath.Join(root, "missingfile") if err := os.WriteFile(missingFile, []byte("will vanish"), 0o644); err != nil { t.Fatalf("writing missing file: %v", err) } if err := g.Add([]string{okFile, modFile, missingFile}); err != nil { t.Fatalf("Add: %v", err) } // Modify one file, remove another. if err := os.WriteFile(modFile, []byte("changed"), 0o644); err != nil { t.Fatalf("modifying file: %v", err) } os.Remove(missingFile) statuses, err := g.Status() if err != nil { t.Fatalf("Status: %v", err) } if len(statuses) != 3 { t.Fatalf("expected 3 statuses, got %d", len(statuses)) } stateMap := make(map[string]string) for _, s := range statuses { stateMap[s.Path] = s.State } okPath := toTildePath(okFile) modPath := toTildePath(modFile) missingPath := toTildePath(missingFile) if stateMap[okPath] != "ok" { t.Errorf("okfile: expected ok, got %s", stateMap[okPath]) } if stateMap[modPath] != "modified" { t.Errorf("modfile: expected modified, got %s", stateMap[modPath]) } if stateMap[missingPath] != "missing" { t.Errorf("missingfile: expected missing, got %s", stateMap[missingPath]) } } func TestExpandTildePath(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Skipf("cannot get home dir: %v", err) } tests := []struct { input string want string }{ {"~", home}, {"~/foo", filepath.Join(home, "foo")}, {"~/.config/nvim", filepath.Join(home, ".config/nvim")}, {"/tmp/foo", "/tmp/foo"}, } for _, tt := range tests { got, err := ExpandTildePath(tt.input) if err != nil { t.Errorf("ExpandTildePath(%q): %v", tt.input, err) continue } if got != tt.want { t.Errorf("ExpandTildePath(%q) = %q, want %q", tt.input, got, tt.want) } } }