Steps 10 & 11: Garden accessors and proto-manifest conversion.
Step 10: GetManifest, BlobExists, ReadBlob, WriteBlob, ReplaceManifest accessor methods on Garden. 5 tests. Step 11: ManifestToProto/ProtoToManifest conversion functions in server package with time.Time <-> timestamppb handling. Round-trip test covering all 3 entry types. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ ARCHITECTURE.md for design details.
|
|||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
**Phase:** Phase 2 in progress. Step 9 complete, ready for Steps 10+11.
|
**Phase:** Phase 2 in progress. Steps 9–11 complete, ready for Step 12.
|
||||||
|
|
||||||
**Last updated:** 2026-03-23
|
**Last updated:** 2026-03-23
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Phase 2: gRPC Remote Sync.
|
|||||||
|
|
||||||
## Up Next
|
## Up Next
|
||||||
|
|
||||||
Steps 10 (Garden accessors) + 11 (proto-manifest conversion) — can be parallel.
|
Step 12: Server Implementation (No Auth).
|
||||||
|
|
||||||
## Known Issues / Decisions Deferred
|
## Known Issues / Decisions Deferred
|
||||||
|
|
||||||
@@ -69,3 +69,5 @@ Steps 10 (Garden accessors) + 11 (proto-manifest conversion) — can be parallel
|
|||||||
| 2026-03-23 | — | README, goreleaser config, version command, Nix flake, homebrew formula, release pipeline validated (v0.1.0–v0.1.2). |
|
| 2026-03-23 | — | README, goreleaser config, version command, Nix flake, homebrew formula, release pipeline validated (v0.1.0–v0.1.2). |
|
||||||
| 2026-03-23 | — | v1.0.0 released. Docs updated for release. |
|
| 2026-03-23 | — | v1.0.0 released. Docs updated for release. |
|
||||||
| 2026-03-23 | 9 | Proto definitions: 5 RPCs (Push/Pull manifest+blobs, Prune), generated sgardpb, Makefile, deps added. |
|
| 2026-03-23 | 9 | Proto definitions: 5 RPCs (Push/Pull manifest+blobs, Prune), generated sgardpb, Makefile, deps added. |
|
||||||
|
| 2026-03-23 | 10 | Garden accessor methods: GetManifest, BlobExists, ReadBlob, WriteBlob, ReplaceManifest. 5 tests. |
|
||||||
|
| 2026-03-23 | 11 | Proto-manifest conversion: ManifestToProto/ProtoToManifest with round-trip tests. |
|
||||||
|
|||||||
@@ -106,17 +106,17 @@ Depends on Step 5.
|
|||||||
|
|
||||||
*Can be done in parallel with Step 11.*
|
*Can be done in parallel with Step 11.*
|
||||||
|
|
||||||
- [ ] `garden/garden.go`: `GetManifest()`, `BlobExists()`, `ReadBlob()`, `WriteBlob()`, `ReplaceManifest()`
|
- [x] `garden/garden.go`: `GetManifest()`, `BlobExists()`, `ReadBlob()`, `WriteBlob()`, `ReplaceManifest()`
|
||||||
- [ ] Tests for each accessor
|
- [x] Tests for each accessor
|
||||||
- [ ] Verify: `go test ./garden/...`
|
- [x] Verify: `go test ./garden/...`
|
||||||
|
|
||||||
### Step 11: Proto-Manifest Conversion
|
### Step 11: Proto-Manifest Conversion
|
||||||
|
|
||||||
*Can be done in parallel with Step 10.*
|
*Can be done in parallel with Step 10.*
|
||||||
|
|
||||||
- [ ] `server/convert.go`: `ManifestToProto`, `ProtoToManifest`, entry helpers
|
- [x] `server/convert.go`: `ManifestToProto`, `ProtoToManifest`, entry helpers
|
||||||
- [ ] `server/convert_test.go`: round-trip test
|
- [x] `server/convert_test.go`: round-trip test
|
||||||
- [ ] Verify: `go test ./server/...`
|
- [x] Verify: `go test ./server/...`
|
||||||
|
|
||||||
### Step 12: Server Implementation (No Auth)
|
### Step 12: Server Implementation (No Auth)
|
||||||
|
|
||||||
|
|||||||
62
server/convert.go
Normal file
62
server/convert.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kisom/sgard/manifest"
|
||||||
|
"github.com/kisom/sgard/sgardpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ManifestToProto converts a manifest.Manifest to its protobuf representation.
|
||||||
|
func ManifestToProto(m *manifest.Manifest) *sgardpb.Manifest {
|
||||||
|
files := make([]*sgardpb.ManifestEntry, len(m.Files))
|
||||||
|
for i, e := range m.Files {
|
||||||
|
files[i] = EntryToProto(e)
|
||||||
|
}
|
||||||
|
return &sgardpb.Manifest{
|
||||||
|
Version: int32(m.Version),
|
||||||
|
Created: timestamppb.New(m.Created),
|
||||||
|
Updated: timestamppb.New(m.Updated),
|
||||||
|
Message: m.Message,
|
||||||
|
Files: files,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtoToManifest converts a protobuf Manifest to a manifest.Manifest.
|
||||||
|
func ProtoToManifest(p *sgardpb.Manifest) *manifest.Manifest {
|
||||||
|
pFiles := p.GetFiles()
|
||||||
|
files := make([]manifest.Entry, len(pFiles))
|
||||||
|
for i, e := range pFiles {
|
||||||
|
files[i] = ProtoToEntry(e)
|
||||||
|
}
|
||||||
|
return &manifest.Manifest{
|
||||||
|
Version: int(p.GetVersion()),
|
||||||
|
Created: p.GetCreated().AsTime(),
|
||||||
|
Updated: p.GetUpdated().AsTime(),
|
||||||
|
Message: p.GetMessage(),
|
||||||
|
Files: files,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryToProto converts a manifest.Entry to its protobuf representation.
|
||||||
|
func EntryToProto(e manifest.Entry) *sgardpb.ManifestEntry {
|
||||||
|
return &sgardpb.ManifestEntry{
|
||||||
|
Path: e.Path,
|
||||||
|
Hash: e.Hash,
|
||||||
|
Type: e.Type,
|
||||||
|
Mode: e.Mode,
|
||||||
|
Target: e.Target,
|
||||||
|
Updated: timestamppb.New(e.Updated),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtoToEntry converts a protobuf ManifestEntry to a manifest.Entry.
|
||||||
|
func ProtoToEntry(p *sgardpb.ManifestEntry) manifest.Entry {
|
||||||
|
return manifest.Entry{
|
||||||
|
Path: p.GetPath(),
|
||||||
|
Hash: p.GetHash(),
|
||||||
|
Type: p.GetType(),
|
||||||
|
Mode: p.GetMode(),
|
||||||
|
Target: p.GetTarget(),
|
||||||
|
Updated: p.GetUpdated().AsTime(),
|
||||||
|
}
|
||||||
|
}
|
||||||
124
server/convert_test.go
Normal file
124
server/convert_test.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kisom/sgard/manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestManifestRoundTrip(t *testing.T) {
|
||||||
|
now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||||
|
m := &manifest.Manifest{
|
||||||
|
Version: 1,
|
||||||
|
Created: now,
|
||||||
|
Updated: now,
|
||||||
|
Message: "test checkpoint",
|
||||||
|
Files: []manifest.Entry{
|
||||||
|
{Path: "~/.bashrc", Hash: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", Type: "file", Mode: "0644", Updated: now},
|
||||||
|
{Path: "~/.config/nvim", Type: "directory", Mode: "0755", Updated: now},
|
||||||
|
{Path: "~/.vimrc", Type: "link", Target: "~/.config/nvim/init.vim", Updated: now},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
proto := ManifestToProto(m)
|
||||||
|
back := ProtoToManifest(proto)
|
||||||
|
|
||||||
|
if back.Version != m.Version {
|
||||||
|
t.Errorf("Version: got %d, want %d", back.Version, m.Version)
|
||||||
|
}
|
||||||
|
if !back.Created.Equal(m.Created) {
|
||||||
|
t.Errorf("Created: got %v, want %v", back.Created, m.Created)
|
||||||
|
}
|
||||||
|
if !back.Updated.Equal(m.Updated) {
|
||||||
|
t.Errorf("Updated: got %v, want %v", back.Updated, m.Updated)
|
||||||
|
}
|
||||||
|
if back.Message != m.Message {
|
||||||
|
t.Errorf("Message: got %q, want %q", back.Message, m.Message)
|
||||||
|
}
|
||||||
|
if len(back.Files) != len(m.Files) {
|
||||||
|
t.Fatalf("Files count: got %d, want %d", len(back.Files), len(m.Files))
|
||||||
|
}
|
||||||
|
for i, want := range m.Files {
|
||||||
|
got := back.Files[i]
|
||||||
|
if got.Path != want.Path {
|
||||||
|
t.Errorf("Files[%d].Path: got %q, want %q", i, got.Path, want.Path)
|
||||||
|
}
|
||||||
|
if got.Hash != want.Hash {
|
||||||
|
t.Errorf("Files[%d].Hash: got %q, want %q", i, got.Hash, want.Hash)
|
||||||
|
}
|
||||||
|
if got.Type != want.Type {
|
||||||
|
t.Errorf("Files[%d].Type: got %q, want %q", i, got.Type, want.Type)
|
||||||
|
}
|
||||||
|
if got.Mode != want.Mode {
|
||||||
|
t.Errorf("Files[%d].Mode: got %q, want %q", i, got.Mode, want.Mode)
|
||||||
|
}
|
||||||
|
if got.Target != want.Target {
|
||||||
|
t.Errorf("Files[%d].Target: got %q, want %q", i, got.Target, want.Target)
|
||||||
|
}
|
||||||
|
if !got.Updated.Equal(want.Updated) {
|
||||||
|
t.Errorf("Files[%d].Updated: got %v, want %v", i, got.Updated, want.Updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyManifestRoundTrip(t *testing.T) {
|
||||||
|
now := time.Date(2026, 6, 15, 8, 30, 0, 0, time.UTC)
|
||||||
|
m := &manifest.Manifest{
|
||||||
|
Version: 1,
|
||||||
|
Created: now,
|
||||||
|
Updated: now,
|
||||||
|
Files: []manifest.Entry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
proto := ManifestToProto(m)
|
||||||
|
back := ProtoToManifest(proto)
|
||||||
|
|
||||||
|
if back.Version != m.Version {
|
||||||
|
t.Errorf("Version: got %d, want %d", back.Version, m.Version)
|
||||||
|
}
|
||||||
|
if !back.Created.Equal(m.Created) {
|
||||||
|
t.Errorf("Created: got %v, want %v", back.Created, m.Created)
|
||||||
|
}
|
||||||
|
if !back.Updated.Equal(m.Updated) {
|
||||||
|
t.Errorf("Updated: got %v, want %v", back.Updated, m.Updated)
|
||||||
|
}
|
||||||
|
if back.Message != "" {
|
||||||
|
t.Errorf("Message: got %q, want empty", back.Message)
|
||||||
|
}
|
||||||
|
if len(back.Files) != 0 {
|
||||||
|
t.Errorf("Files count: got %d, want 0", len(back.Files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEmptyOptionalFieldsRoundTrip(t *testing.T) {
|
||||||
|
now := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
e := manifest.Entry{
|
||||||
|
Path: "~/.profile",
|
||||||
|
Type: "file",
|
||||||
|
Updated: now,
|
||||||
|
// Hash, Mode, Target intentionally empty
|
||||||
|
}
|
||||||
|
|
||||||
|
proto := EntryToProto(e)
|
||||||
|
back := ProtoToEntry(proto)
|
||||||
|
|
||||||
|
if back.Path != e.Path {
|
||||||
|
t.Errorf("Path: got %q, want %q", back.Path, e.Path)
|
||||||
|
}
|
||||||
|
if back.Hash != "" {
|
||||||
|
t.Errorf("Hash: got %q, want empty", back.Hash)
|
||||||
|
}
|
||||||
|
if back.Type != e.Type {
|
||||||
|
t.Errorf("Type: got %q, want %q", back.Type, e.Type)
|
||||||
|
}
|
||||||
|
if back.Mode != "" {
|
||||||
|
t.Errorf("Mode: got %q, want empty", back.Mode)
|
||||||
|
}
|
||||||
|
if back.Target != "" {
|
||||||
|
t.Errorf("Target: got %q, want empty", back.Target)
|
||||||
|
}
|
||||||
|
if !back.Updated.Equal(e.Updated) {
|
||||||
|
t.Errorf("Updated: got %v, want %v", back.Updated, e.Updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user