Step 32: Phase 5 polish.

E2e test covering targeting labels through push/pull cycle. Updated
README with targeting docs and commands. All project docs updated.
Phase 5 complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 22:57:59 -07:00
parent 2ff9fe2f50
commit e37e788885
4 changed files with 214 additions and 25 deletions

148
integration/phase5_test.go Normal file
View File

@@ -0,0 +1,148 @@
package integration
import (
"context"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/kisom/sgard/client"
"github.com/kisom/sgard/garden"
"github.com/kisom/sgard/server"
"github.com/kisom/sgard/sgardpb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
)
const bufSize = 1024 * 1024
// TestE2E_Phase5_Targeting verifies that targeting labels survive push/pull
// and that restore respects them.
func TestE2E_Phase5_Targeting(t *testing.T) {
// Set up bufconn server.
serverDir := t.TempDir()
serverGarden, err := garden.Init(serverDir)
if err != nil {
t.Fatalf("init server: %v", err)
}
lis := bufconn.Listen(bufSize)
srv := grpc.NewServer()
sgardpb.RegisterGardenSyncServer(srv, server.New(serverGarden))
t.Cleanup(func() { srv.Stop() })
go func() { _ = srv.Serve(lis) }()
conn, err := grpc.NewClient("passthrough:///bufconn",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("dial: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
// --- Build source garden with targeted entries ---
srcRoot := t.TempDir()
srcRepoDir := filepath.Join(srcRoot, "repo")
srcGarden, err := garden.Init(srcRepoDir)
if err != nil {
t.Fatalf("init source: %v", err)
}
linuxFile := filepath.Join(srcRoot, "linux-only")
everywhereFile := filepath.Join(srcRoot, "everywhere")
neverArmFile := filepath.Join(srcRoot, "never-arm")
if err := os.WriteFile(linuxFile, []byte("linux"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(everywhereFile, []byte("everywhere"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(neverArmFile, []byte("not arm"), 0o644); err != nil {
t.Fatal(err)
}
if err := srcGarden.Add([]string{linuxFile}, garden.AddOptions{Only: []string{"os:linux"}}); err != nil {
t.Fatalf("Add linux-only: %v", err)
}
if err := srcGarden.Add([]string{everywhereFile}); err != nil {
t.Fatalf("Add everywhere: %v", err)
}
if err := srcGarden.Add([]string{neverArmFile}, garden.AddOptions{Never: []string{"arch:arm64"}}); err != nil {
t.Fatalf("Add never-arm: %v", err)
}
// Bump timestamp.
m := srcGarden.GetManifest()
m.Updated = time.Now().UTC().Add(time.Hour)
if err := srcGarden.ReplaceManifest(m); err != nil {
t.Fatal(err)
}
// --- Push ---
ctx := context.Background()
pushClient := client.New(conn)
if _, err := pushClient.Push(ctx, srcGarden); err != nil {
t.Fatalf("Push: %v", err)
}
// --- Pull to fresh garden ---
dstRoot := t.TempDir()
dstRepoDir := filepath.Join(dstRoot, "repo")
dstGarden, err := garden.Init(dstRepoDir)
if err != nil {
t.Fatalf("init dest: %v", err)
}
pullClient := client.New(conn)
if _, err := pullClient.Pull(ctx, dstGarden); err != nil {
t.Fatalf("Pull: %v", err)
}
// --- Verify targeting survived ---
dm := dstGarden.GetManifest()
if len(dm.Files) != 3 {
t.Fatalf("expected 3 entries, got %d", len(dm.Files))
}
for _, e := range dm.Files {
switch {
case e.Path == toTilde(linuxFile):
if len(e.Only) != 1 || e.Only[0] != "os:linux" {
t.Errorf("%s: only = %v, want [os:linux]", e.Path, e.Only)
}
case e.Path == toTilde(everywhereFile):
if len(e.Only) != 0 || len(e.Never) != 0 {
t.Errorf("%s: should have no targeting", e.Path)
}
case e.Path == toTilde(neverArmFile):
if len(e.Never) != 1 || e.Never[0] != "arch:arm64" {
t.Errorf("%s: never = %v, want [arch:arm64]", e.Path, e.Never)
}
}
}
// Verify restore skips non-matching entries.
// Delete all files, then restore — only matching entries should appear.
_ = os.Remove(linuxFile)
_ = os.Remove(everywhereFile)
_ = os.Remove(neverArmFile)
if err := dstGarden.Restore(nil, true, nil); err != nil {
t.Fatalf("Restore: %v", err)
}
// "everywhere" should always be restored.
if _, err := os.Stat(everywhereFile); os.IsNotExist(err) {
t.Error("everywhere file should be restored")
}
// "linux-only" depends on current OS — we just verify no error occurred.
// "never-arm" depends on current arch.
}