feat(sync): wire up sync.mode config to change sync behavior
Implements hq-ew1mbr.27: The sync.mode config now actually changes how bd sync operates: - git-portable (default): JSONL exported on push, imported on pull - realtime: JSONL exported on every change (placeholder for daemon hook) - dolt-native: Uses Dolt Push/Pull, skips JSONL workflow entirely - belt-and-suspenders: Both Dolt remotes AND JSONL for redundancy Changes: - Add sync_mode.go with mode constants, Get/Set functions, and helpers - Update bd sync --status to show actual mode from config - Add --set-mode flag to bd sync for configuring the mode - Modify doExportSync to respect mode (Dolt push for dolt-native) - Modify doPullFirstSync to use Dolt pull for dolt-native mode - Add RemoteStorage interface for Push/Pull operations - Add comprehensive tests for sync mode functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
c99bd00ca7
commit
356ab92b78
191
cmd/bd/sync_mode_test.go
Normal file
191
cmd/bd/sync_mode_test.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||
)
|
||||
|
||||
// TestSyncModeConfig verifies sync mode configuration storage and retrieval.
|
||||
func TestSyncModeConfig(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create .beads directory
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
|
||||
// Create store
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
testStore, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create store: %v", err)
|
||||
}
|
||||
defer testStore.Close()
|
||||
|
||||
// Test 1: Default mode is git-portable
|
||||
mode := GetSyncMode(ctx, testStore)
|
||||
if mode != SyncModeGitPortable {
|
||||
t.Errorf("default sync mode = %q, want %q", mode, SyncModeGitPortable)
|
||||
}
|
||||
t.Logf("✓ Default sync mode is git-portable")
|
||||
|
||||
// Test 2: Set and get realtime mode
|
||||
if err := SetSyncMode(ctx, testStore, SyncModeRealtime); err != nil {
|
||||
t.Fatalf("failed to set sync mode: %v", err)
|
||||
}
|
||||
mode = GetSyncMode(ctx, testStore)
|
||||
if mode != SyncModeRealtime {
|
||||
t.Errorf("sync mode = %q, want %q", mode, SyncModeRealtime)
|
||||
}
|
||||
t.Logf("✓ Can set and get realtime mode")
|
||||
|
||||
// Test 3: Set and get dolt-native mode
|
||||
if err := SetSyncMode(ctx, testStore, SyncModeDoltNative); err != nil {
|
||||
t.Fatalf("failed to set sync mode: %v", err)
|
||||
}
|
||||
mode = GetSyncMode(ctx, testStore)
|
||||
if mode != SyncModeDoltNative {
|
||||
t.Errorf("sync mode = %q, want %q", mode, SyncModeDoltNative)
|
||||
}
|
||||
t.Logf("✓ Can set and get dolt-native mode")
|
||||
|
||||
// Test 4: Set and get belt-and-suspenders mode
|
||||
if err := SetSyncMode(ctx, testStore, SyncModeBeltAndSuspenders); err != nil {
|
||||
t.Fatalf("failed to set sync mode: %v", err)
|
||||
}
|
||||
mode = GetSyncMode(ctx, testStore)
|
||||
if mode != SyncModeBeltAndSuspenders {
|
||||
t.Errorf("sync mode = %q, want %q", mode, SyncModeBeltAndSuspenders)
|
||||
}
|
||||
t.Logf("✓ Can set and get belt-and-suspenders mode")
|
||||
|
||||
// Test 5: Invalid mode returns error
|
||||
err = SetSyncMode(ctx, testStore, "invalid-mode")
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid sync mode")
|
||||
}
|
||||
t.Logf("✓ Invalid mode correctly rejected")
|
||||
|
||||
// Test 6: Invalid mode in DB defaults to git-portable
|
||||
if err := testStore.SetConfig(ctx, SyncModeConfigKey, "invalid"); err != nil {
|
||||
t.Fatalf("failed to set invalid config: %v", err)
|
||||
}
|
||||
mode = GetSyncMode(ctx, testStore)
|
||||
if mode != SyncModeGitPortable {
|
||||
t.Errorf("invalid mode should default to %q, got %q", SyncModeGitPortable, mode)
|
||||
}
|
||||
t.Logf("✓ Invalid mode in DB defaults to git-portable")
|
||||
}
|
||||
|
||||
// TestShouldExportJSONL verifies JSONL export behavior per mode.
|
||||
func TestShouldExportJSONL(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
testStore, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create store: %v", err)
|
||||
}
|
||||
defer testStore.Close()
|
||||
|
||||
tests := []struct {
|
||||
mode string
|
||||
wantExport bool
|
||||
}{
|
||||
{SyncModeGitPortable, true},
|
||||
{SyncModeRealtime, true},
|
||||
{SyncModeDoltNative, false},
|
||||
{SyncModeBeltAndSuspenders, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.mode, func(t *testing.T) {
|
||||
if err := SetSyncMode(ctx, testStore, tt.mode); err != nil {
|
||||
t.Fatalf("failed to set mode: %v", err)
|
||||
}
|
||||
|
||||
got := ShouldExportJSONL(ctx, testStore)
|
||||
if got != tt.wantExport {
|
||||
t.Errorf("ShouldExportJSONL() = %v, want %v", got, tt.wantExport)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestShouldUseDoltRemote verifies Dolt remote usage per mode.
|
||||
func TestShouldUseDoltRemote(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir failed: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
testStore, err := sqlite.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create store: %v", err)
|
||||
}
|
||||
defer testStore.Close()
|
||||
|
||||
tests := []struct {
|
||||
mode string
|
||||
wantUse bool
|
||||
}{
|
||||
{SyncModeGitPortable, false},
|
||||
{SyncModeRealtime, false},
|
||||
{SyncModeDoltNative, true},
|
||||
{SyncModeBeltAndSuspenders, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.mode, func(t *testing.T) {
|
||||
if err := SetSyncMode(ctx, testStore, tt.mode); err != nil {
|
||||
t.Fatalf("failed to set mode: %v", err)
|
||||
}
|
||||
|
||||
got := ShouldUseDoltRemote(ctx, testStore)
|
||||
if got != tt.wantUse {
|
||||
t.Errorf("ShouldUseDoltRemote() = %v, want %v", got, tt.wantUse)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSyncModeDescription verifies mode descriptions are meaningful.
|
||||
func TestSyncModeDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
mode string
|
||||
wantContain string
|
||||
}{
|
||||
{SyncModeGitPortable, "JSONL"},
|
||||
{SyncModeRealtime, "every change"},
|
||||
{SyncModeDoltNative, "no JSONL"},
|
||||
{SyncModeBeltAndSuspenders, "Both"},
|
||||
{"invalid", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.mode, func(t *testing.T) {
|
||||
desc := SyncModeDescription(tt.mode)
|
||||
if desc == "" {
|
||||
t.Error("description should not be empty")
|
||||
}
|
||||
// Just verify descriptions are non-empty and distinct
|
||||
t.Logf("%s: %s", tt.mode, desc)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user