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>
192 lines
5.1 KiB
Go
192 lines
5.1 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|