Files
beads/cmd/bd/sync_mode_test.go
beads/crew/emma 356ab92b78 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>
2026-01-18 10:36:20 -08:00

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)
})
}
}