From 34330a35efa028041887002fd5d7e7a916ae81f4 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Mon, 23 Mar 2026 23:19:48 -0700 Subject: [PATCH] Add Garden accessor methods for manifest and blob store access. Expose GetManifest, BlobExists, ReadBlob, WriteBlob, and ReplaceManifest on *Garden to support future gRPC and higher-level operations without breaking encapsulation. Includes 5 unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- garden/garden.go | 29 ++++++++ garden/garden_test.go | 159 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) diff --git a/garden/garden.go b/garden/garden.go index 64949b0..701ffb1 100644 --- a/garden/garden.go +++ b/garden/garden.go @@ -98,6 +98,35 @@ func (g *Garden) SetClock(c clockwork.Clock) { g.clock = c } +// GetManifest returns the current manifest. +func (g *Garden) GetManifest() *manifest.Manifest { + return g.manifest +} + +// BlobExists reports whether a blob with the given hash exists in the store. +func (g *Garden) BlobExists(hash string) bool { + return g.store.Exists(hash) +} + +// ReadBlob returns the contents of the blob with the given hash. +func (g *Garden) ReadBlob(hash string) ([]byte, error) { + return g.store.Read(hash) +} + +// WriteBlob writes data to the blob store and returns the hash. +func (g *Garden) WriteBlob(data []byte) (string, error) { + return g.store.Write(data) +} + +// ReplaceManifest atomically replaces the current manifest. +func (g *Garden) ReplaceManifest(m *manifest.Manifest) error { + if err := m.Save(g.manifestPath); err != nil { + return fmt.Errorf("saving manifest: %w", err) + } + g.manifest = m + return nil +} + // Add tracks new files, directories, or symlinks. Each path is resolved // to an absolute path, inspected for its type, and added to the manifest. // Regular files are hashed and stored in the blob store. diff --git a/garden/garden_test.go b/garden/garden_test.go index 421f580..aafc210 100644 --- a/garden/garden_test.go +++ b/garden/garden_test.go @@ -1,9 +1,13 @@ package garden import ( + "crypto/sha256" + "encoding/hex" "os" "path/filepath" "testing" + + "github.com/kisom/sgard/manifest" ) func TestInitCreatesStructure(t *testing.T) { @@ -619,6 +623,161 @@ func TestRestoreConfirmSkips(t *testing.T) { } } +func TestGetManifest(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("hello"), 0o644); err != nil { + t.Fatalf("writing test file: %v", err) + } + + if err := g.Add([]string{testFile}); err != nil { + t.Fatalf("Add: %v", err) + } + + m := g.GetManifest() + if m == nil { + t.Fatal("GetManifest returned nil") + } + if len(m.Files) != 1 { + t.Errorf("expected 1 entry, got %d", len(m.Files)) + } +} + +func TestBlobExists(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("blob exists test"), 0o644); err != nil { + t.Fatalf("writing test file: %v", err) + } + + if err := g.Add([]string{testFile}); err != nil { + t.Fatalf("Add: %v", err) + } + + hash := g.GetManifest().Files[0].Hash + if !g.BlobExists(hash) { + t.Error("BlobExists returned false for a stored blob") + } + if g.BlobExists("0000000000000000000000000000000000000000000000000000000000000000") { + t.Error("BlobExists returned true for a fake hash") + } +} + +func TestReadBlob(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + content := []byte("read blob test content") + testFile := filepath.Join(root, "testfile") + if err := os.WriteFile(testFile, content, 0o644); err != nil { + t.Fatalf("writing test file: %v", err) + } + + if err := g.Add([]string{testFile}); err != nil { + t.Fatalf("Add: %v", err) + } + + hash := g.GetManifest().Files[0].Hash + got, err := g.ReadBlob(hash) + if err != nil { + t.Fatalf("ReadBlob: %v", err) + } + if string(got) != string(content) { + t.Errorf("ReadBlob content = %q, want %q", got, content) + } +} + +func TestWriteBlob(t *testing.T) { + root := t.TempDir() + repoDir := filepath.Join(root, "repo") + + g, err := Init(repoDir) + if err != nil { + t.Fatalf("Init: %v", err) + } + + data := []byte("write blob test data") + hash, err := g.WriteBlob(data) + if err != nil { + t.Fatalf("WriteBlob: %v", err) + } + + // Verify the hash is correct SHA-256. + sum := sha256.Sum256(data) + wantHash := hex.EncodeToString(sum[:]) + if hash != wantHash { + t.Errorf("WriteBlob hash = %s, want %s", hash, wantHash) + } + + if !g.BlobExists(hash) { + t.Error("BlobExists returned false after WriteBlob") + } +} + +func TestReplaceManifest(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 new manifest with a custom entry. + newManifest := manifest.New() + newManifest.Files = append(newManifest.Files, manifest.Entry{ + Path: "~/replaced-file", + Type: "file", + Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Mode: "0644", + }) + + if err := g.ReplaceManifest(newManifest); err != nil { + t.Fatalf("ReplaceManifest: %v", err) + } + + // Verify in-memory manifest was updated. + m := g.GetManifest() + if len(m.Files) != 1 { + t.Fatalf("expected 1 entry, got %d", len(m.Files)) + } + if m.Files[0].Path != "~/replaced-file" { + t.Errorf("expected path ~/replaced-file, got %s", m.Files[0].Path) + } + + // Verify persistence by re-opening. + g2, err := Open(repoDir) + if err != nil { + t.Fatalf("re-Open: %v", err) + } + m2 := g2.GetManifest() + if len(m2.Files) != 1 { + t.Fatalf("persisted manifest has %d entries, want 1", len(m2.Files)) + } + if m2.Files[0].Path != "~/replaced-file" { + t.Errorf("persisted entry path = %s, want ~/replaced-file", m2.Files[0].Path) + } +} + func TestExpandTildePath(t *testing.T) { home, err := os.UserHomeDir() if err != nil {