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) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 23:19:48 -07:00
parent 0113703908
commit 34330a35ef
2 changed files with 188 additions and 0 deletions

View File

@@ -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.

View File

@@ -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 {