sync app filled in

This commit is contained in:
2026-06-01 10:53:28 -04:00
parent ebe0920f89
commit fb0673a1c5
36 changed files with 4527 additions and 98 deletions

View File

@@ -2,11 +2,15 @@ package sync
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -112,7 +116,7 @@ func TestApplyDeltaDetectsServerChanges(t *testing.T) {
}
// Client sends empty delta
result, err := service.ApplyDelta(ctx, snap.ID, Delta{Changes: nil})
result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: nil})
if err != nil {
t.Fatalf("ApplyDelta() error = %v", err)
}
@@ -133,6 +137,31 @@ func TestApplyDeltaDetectsServerChanges(t *testing.T) {
}
}
func TestApplyDeltaMarshalsEmptyCollectionsAsArrays(t *testing.T) {
service, _ := setupTestService(t)
ctx := context.Background()
snap, err := service.InitSync(ctx, "device-1", "user:test")
if err != nil {
t.Fatalf("InitSync() error = %v", err)
}
result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{})
if err != nil {
t.Fatalf("ApplyDelta() error = %v", err)
}
payload, err := json.Marshal(result)
if err != nil {
t.Fatalf("marshal delta result: %v", err)
}
for _, expected := range []string{`"serverDelta":[]`, `"conflicts":[]`} {
if !strings.Contains(string(payload), expected) {
t.Fatalf("delta JSON = %s, want %s", payload, expected)
}
}
}
func TestApplyDeltaDetectsConflicts(t *testing.T) {
service, sourceDir := setupTestService(t)
ctx := context.Background()
@@ -157,7 +186,7 @@ func TestApplyDeltaDetectsConflicts(t *testing.T) {
Modified: time.Now().UTC(),
}
result, err := service.ApplyDelta(ctx, snap.ID, Delta{Changes: []Change{clientChange}})
result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{clientChange}})
if err != nil {
t.Fatalf("ApplyDelta() error = %v", err)
}
@@ -189,15 +218,17 @@ func TestApplyDeltaAllowsDifferentFileEditsWithoutConflict(t *testing.T) {
t.Fatalf("update guide.md: %v", err)
}
clientContent := "# Hello\n\nClient-side hello update"
clientChange := Change{
Type: ChangeUpdate,
Path: "hello.md",
Hash: "clienthash-different-file",
Size: 128,
Hash: sha256Hex(clientContent),
Content: clientContent,
Size: int64(len(clientContent)),
Modified: time.Now().UTC(),
}
result, err := service.ApplyDelta(ctx, snap.ID, Delta{Changes: []Change{clientChange}})
result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{clientChange}})
if err != nil {
t.Fatalf("ApplyDelta() error = %v", err)
}
@@ -213,6 +244,71 @@ func TestApplyDeltaAllowsDifferentFileEditsWithoutConflict(t *testing.T) {
}
}
func TestApplyDeltaAppliesClientCreateWithContent(t *testing.T) {
service, sourceDir := setupTestService(t)
ctx := context.Background()
snap, err := service.InitSync(ctx, "device-1", "user:test")
if err != nil {
t.Fatalf("InitSync() error = %v", err)
}
content := "# Inbox\n\nCreated from macOS"
result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{{
Type: ChangeCreate,
Path: "inbox/from-mac.md",
Hash: sha256Hex(content),
Content: content,
Size: int64(len(content)),
Modified: time.Now().UTC(),
}}})
if err != nil {
t.Fatalf("ApplyDelta() error = %v", err)
}
if len(result.Conflicts) != 0 {
t.Fatalf("expected no conflicts, got %#v", result.Conflicts)
}
if result.NewSnapshotID == "" || result.NewSnapshotID == snap.ID {
t.Fatalf("new snapshot id = %q, old = %q", result.NewSnapshotID, snap.ID)
}
written, err := os.ReadFile(filepath.Join(sourceDir, "inbox", "from-mac.md"))
if err != nil {
t.Fatalf("read synced file: %v", err)
}
if string(written) != content {
t.Fatalf("synced content = %q, want %q", string(written), content)
}
stored, err := service.GetContent(sha256Hex(content))
if err != nil {
t.Fatalf("GetContent() error = %v", err)
}
if string(stored) != content {
t.Fatalf("stored content = %q, want %q", string(stored), content)
}
}
func TestApplyDeltaRejectsHashMismatch(t *testing.T) {
service, _ := setupTestService(t)
ctx := context.Background()
snap, err := service.InitSync(ctx, "device-1", "user:test")
if err != nil {
t.Fatalf("InitSync() error = %v", err)
}
_, err = service.ApplyDelta(ctx, snap.ID, "user:test", Delta{Changes: []Change{{
Type: ChangeCreate,
Path: "bad.md",
Hash: "not-the-content-hash",
Content: "# Bad\n",
}}})
if err == nil {
t.Fatal("expected hash mismatch error")
}
}
func TestResolveConflictsPreservesServerContent(t *testing.T) {
service, sourceDir := setupTestService(t)
ctx := context.Background()
@@ -227,7 +323,7 @@ func TestResolveConflictsPreservesServerContent(t *testing.T) {
t.Fatalf("update hello.md: %v", err)
}
_, err = service.ResolveConflicts(ctx, snap.ID, []Resolution{{
_, err = service.ResolveConflicts(ctx, snap.ID, "user:test", []Resolution{{
Path: "hello.md",
Strategy: ResolutionServerWins,
}})
@@ -260,7 +356,7 @@ func TestResolveConflictsCreatesNewSnapshot(t *testing.T) {
},
}
newSnap, err := service.ResolveConflicts(ctx, snap.ID, resolutions)
newSnap, err := service.ResolveConflicts(ctx, snap.ID, "user:test", resolutions)
if err != nil {
t.Fatalf("ResolveConflicts() error = %v", err)
}
@@ -300,6 +396,14 @@ func TestGetContentReturnsFileBytes(t *testing.T) {
}
}
func TestGetContentRejectsInvalidHash(t *testing.T) {
service, _ := setupTestService(t)
if _, err := service.GetContent("short"); err == nil {
t.Fatal("expected invalid hash error")
}
}
func TestSnapshotDeltaHandles100FilesUnderTarget(t *testing.T) {
service, sourceDir := setupTestService(t)
ctx := context.Background()
@@ -329,7 +433,7 @@ func TestSnapshotDeltaHandles100FilesUnderTarget(t *testing.T) {
}
start = time.Now()
result, err := service.ApplyDelta(ctx, snap.ID, Delta{})
result, err := service.ApplyDelta(ctx, snap.ID, "user:test", Delta{})
if err != nil {
t.Fatalf("ApplyDelta() error = %v", err)
}
@@ -343,3 +447,8 @@ func TestSnapshotDeltaHandles100FilesUnderTarget(t *testing.T) {
t.Fatalf("server delta path = %q, want note-050.md", result.ServerDelta[0].Path)
}
}
func sha256Hex(content string) string {
sum := sha256.Sum256([]byte(content))
return hex.EncodeToString(sum[:])
}