feat(sync): add sync mode configuration (hq-ew1mbr.3)
Add configurable sync modes for Dolt storage integration: Sync modes: - git-portable (default): Export JSONL on push, import on pull - realtime: Export JSONL on every database change - dolt-native: Use Dolt remotes directly (no JSONL) - belt-and-suspenders: Both Dolt remote AND JSONL backup Configuration options in .beads/config.yaml: - sync.mode: Select sync mode - sync.export_on: push (default) or change - sync.import_on: pull (default) or change - conflict.strategy: newest (default), ours, theirs, manual - federation.remote: Dolt remote URL for dolt-native mode - federation.sovereignty: T1-T4 data sovereignty tier The sync command now displays configuration in `bd sync --status` and uses configured conflict strategy for resolution. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,67 @@ import (
|
||||
"github.com/steveyegge/beads/internal/debug"
|
||||
)
|
||||
|
||||
// Sync mode constants define how beads syncs with git/remotes.
|
||||
const (
|
||||
// SyncModeGitPortable exports JSONL on push, imports on pull (default).
|
||||
// This is the standard git-based workflow where JSONL is committed.
|
||||
SyncModeGitPortable = "git-portable"
|
||||
|
||||
// SyncModeRealtime exports JSONL on every database change.
|
||||
// Legacy behavior, more frequent writes but higher I/O.
|
||||
SyncModeRealtime = "realtime"
|
||||
|
||||
// SyncModeDoltNative uses Dolt remotes directly (dolthub://, gs://, s3://).
|
||||
// No JSONL export needed - Dolt handles sync.
|
||||
SyncModeDoltNative = "dolt-native"
|
||||
|
||||
// SyncModeBeltAndSuspenders uses both Dolt remote AND JSONL backup.
|
||||
// Maximum redundancy for critical data.
|
||||
SyncModeBeltAndSuspenders = "belt-and-suspenders"
|
||||
)
|
||||
|
||||
// Sync trigger constants define when sync operations occur.
|
||||
const (
|
||||
// SyncTriggerPush triggers sync on git push operations.
|
||||
SyncTriggerPush = "push"
|
||||
|
||||
// SyncTriggerChange triggers sync on every database change.
|
||||
SyncTriggerChange = "change"
|
||||
|
||||
// SyncTriggerPull triggers import on git pull operations.
|
||||
SyncTriggerPull = "pull"
|
||||
)
|
||||
|
||||
// Conflict strategy constants define how sync conflicts are resolved.
|
||||
const (
|
||||
// ConflictStrategyNewest keeps whichever version has the newer updated_at timestamp.
|
||||
ConflictStrategyNewest = "newest"
|
||||
|
||||
// ConflictStrategyOurs keeps the local version on conflict.
|
||||
ConflictStrategyOurs = "ours"
|
||||
|
||||
// ConflictStrategyTheirs keeps the remote version on conflict.
|
||||
ConflictStrategyTheirs = "theirs"
|
||||
|
||||
// ConflictStrategyManual requires manual resolution of conflicts.
|
||||
ConflictStrategyManual = "manual"
|
||||
)
|
||||
|
||||
// Federation sovereignty tiers define data sovereignty levels.
|
||||
const (
|
||||
// SovereigntyT1 - Full sovereignty: data never leaves controlled infrastructure.
|
||||
SovereigntyT1 = "T1"
|
||||
|
||||
// SovereigntyT2 - Regional sovereignty: data stays within region/jurisdiction.
|
||||
SovereigntyT2 = "T2"
|
||||
|
||||
// SovereigntyT3 - Provider sovereignty: data with trusted cloud provider.
|
||||
SovereigntyT3 = "T3"
|
||||
|
||||
// SovereigntyT4 - No restrictions: data can be anywhere (e.g., DoltHub public).
|
||||
SovereigntyT4 = "T4"
|
||||
)
|
||||
|
||||
var v *viper.Viper
|
||||
|
||||
// Initialize sets up the viper configuration singleton
|
||||
@@ -108,6 +169,19 @@ func Initialize() error {
|
||||
// Sync configuration defaults (bd-4u8)
|
||||
v.SetDefault("sync.require_confirmation_on_mass_delete", false)
|
||||
|
||||
// Sync mode configuration (hq-ew1mbr.3)
|
||||
// See docs/CONFIG.md for detailed documentation
|
||||
v.SetDefault("sync.mode", SyncModeGitPortable) // git-portable | realtime | dolt-native | belt-and-suspenders
|
||||
v.SetDefault("sync.export_on", SyncTriggerPush) // push | change
|
||||
v.SetDefault("sync.import_on", SyncTriggerPull) // pull | change
|
||||
|
||||
// Conflict resolution configuration
|
||||
v.SetDefault("conflict.strategy", ConflictStrategyNewest) // newest | ours | theirs | manual
|
||||
|
||||
// Federation configuration (optional Dolt remote)
|
||||
v.SetDefault("federation.remote", "") // e.g., dolthub://org/beads, gs://bucket/beads, s3://bucket/beads
|
||||
v.SetDefault("federation.sovereignty", "") // T1 | T2 | T3 | T4 (empty = no restriction)
|
||||
|
||||
// Push configuration defaults
|
||||
v.SetDefault("no-push", false)
|
||||
|
||||
@@ -530,3 +604,142 @@ func GetIdentity(flagValue string) string {
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// SyncConfig holds the sync mode configuration.
|
||||
type SyncConfig struct {
|
||||
Mode string // git-portable, realtime, dolt-native, belt-and-suspenders
|
||||
ExportOn string // push, change
|
||||
ImportOn string // pull, change
|
||||
}
|
||||
|
||||
// GetSyncConfig returns the current sync configuration.
|
||||
func GetSyncConfig() SyncConfig {
|
||||
return SyncConfig{
|
||||
Mode: GetSyncMode(),
|
||||
ExportOn: GetString("sync.export_on"),
|
||||
ImportOn: GetString("sync.import_on"),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSyncMode returns the configured sync mode.
|
||||
// Returns git-portable if not configured or invalid.
|
||||
func GetSyncMode() string {
|
||||
mode := GetString("sync.mode")
|
||||
if mode == "" {
|
||||
return SyncModeGitPortable
|
||||
}
|
||||
// Validate mode
|
||||
switch mode {
|
||||
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
|
||||
return mode
|
||||
default:
|
||||
return SyncModeGitPortable
|
||||
}
|
||||
}
|
||||
|
||||
// IsSyncModeValid checks if the given sync mode is valid.
|
||||
func IsSyncModeValid(mode string) bool {
|
||||
switch mode {
|
||||
case SyncModeGitPortable, SyncModeRealtime, SyncModeDoltNative, SyncModeBeltAndSuspenders:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ConflictConfig holds the conflict resolution configuration.
|
||||
type ConflictConfig struct {
|
||||
Strategy string // newest, ours, theirs, manual
|
||||
}
|
||||
|
||||
// GetConflictConfig returns the current conflict resolution configuration.
|
||||
func GetConflictConfig() ConflictConfig {
|
||||
return ConflictConfig{
|
||||
Strategy: GetConflictStrategy(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetConflictStrategy returns the configured conflict resolution strategy.
|
||||
// Returns newest if not configured or invalid.
|
||||
func GetConflictStrategy() string {
|
||||
strategy := GetString("conflict.strategy")
|
||||
if strategy == "" {
|
||||
return ConflictStrategyNewest
|
||||
}
|
||||
// Validate strategy
|
||||
switch strategy {
|
||||
case ConflictStrategyNewest, ConflictStrategyOurs, ConflictStrategyTheirs, ConflictStrategyManual:
|
||||
return strategy
|
||||
default:
|
||||
return ConflictStrategyNewest
|
||||
}
|
||||
}
|
||||
|
||||
// IsConflictStrategyValid checks if the given conflict strategy is valid.
|
||||
func IsConflictStrategyValid(strategy string) bool {
|
||||
switch strategy {
|
||||
case ConflictStrategyNewest, ConflictStrategyOurs, ConflictStrategyTheirs, ConflictStrategyManual:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FederationConfig holds the federation (Dolt remote) configuration.
|
||||
type FederationConfig struct {
|
||||
Remote string // dolthub://org/beads, gs://bucket/beads, s3://bucket/beads
|
||||
Sovereignty string // T1, T2, T3, T4
|
||||
}
|
||||
|
||||
// GetFederationConfig returns the current federation configuration.
|
||||
func GetFederationConfig() FederationConfig {
|
||||
return FederationConfig{
|
||||
Remote: GetString("federation.remote"),
|
||||
Sovereignty: GetSovereignty(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSovereignty returns the configured data sovereignty tier.
|
||||
// Returns empty string if not configured.
|
||||
func GetSovereignty() string {
|
||||
sovereignty := GetString("federation.sovereignty")
|
||||
// Validate sovereignty tier
|
||||
switch sovereignty {
|
||||
case SovereigntyT1, SovereigntyT2, SovereigntyT3, SovereigntyT4:
|
||||
return sovereignty
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// IsSovereigntyValid checks if the given sovereignty tier is valid.
|
||||
func IsSovereigntyValid(sovereignty string) bool {
|
||||
switch sovereignty {
|
||||
case "", SovereigntyT1, SovereigntyT2, SovereigntyT3, SovereigntyT4:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldExportOnChange returns true if sync.export_on is set to "change".
|
||||
func ShouldExportOnChange() bool {
|
||||
return GetString("sync.export_on") == SyncTriggerChange
|
||||
}
|
||||
|
||||
// ShouldImportOnChange returns true if sync.import_on is set to "change".
|
||||
func ShouldImportOnChange() bool {
|
||||
return GetString("sync.import_on") == SyncTriggerChange
|
||||
}
|
||||
|
||||
// NeedsDoltRemote returns true if the sync mode requires a Dolt remote.
|
||||
func NeedsDoltRemote() bool {
|
||||
mode := GetSyncMode()
|
||||
return mode == SyncModeDoltNative || mode == SyncModeBeltAndSuspenders
|
||||
}
|
||||
|
||||
// NeedsJSONL returns true if the sync mode requires JSONL export.
|
||||
func NeedsJSONL() bool {
|
||||
mode := GetSyncMode()
|
||||
return mode == SyncModeGitPortable || mode == SyncModeRealtime || mode == SyncModeBeltAndSuspenders
|
||||
}
|
||||
|
||||
@@ -998,3 +998,417 @@ validation:
|
||||
t.Errorf("GetString(validation.on-sync) = %q, want \"warn\"", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for sync mode configuration (hq-ew1mbr.3)
|
||||
|
||||
func TestSyncModeConstants(t *testing.T) {
|
||||
// Verify sync mode constants have expected string values
|
||||
if SyncModeGitPortable != "git-portable" {
|
||||
t.Errorf("SyncModeGitPortable = %q, want \"git-portable\"", SyncModeGitPortable)
|
||||
}
|
||||
if SyncModeRealtime != "realtime" {
|
||||
t.Errorf("SyncModeRealtime = %q, want \"realtime\"", SyncModeRealtime)
|
||||
}
|
||||
if SyncModeDoltNative != "dolt-native" {
|
||||
t.Errorf("SyncModeDoltNative = %q, want \"dolt-native\"", SyncModeDoltNative)
|
||||
}
|
||||
if SyncModeBeltAndSuspenders != "belt-and-suspenders" {
|
||||
t.Errorf("SyncModeBeltAndSuspenders = %q, want \"belt-and-suspenders\"", SyncModeBeltAndSuspenders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncTriggerConstants(t *testing.T) {
|
||||
if SyncTriggerPush != "push" {
|
||||
t.Errorf("SyncTriggerPush = %q, want \"push\"", SyncTriggerPush)
|
||||
}
|
||||
if SyncTriggerChange != "change" {
|
||||
t.Errorf("SyncTriggerChange = %q, want \"change\"", SyncTriggerChange)
|
||||
}
|
||||
if SyncTriggerPull != "pull" {
|
||||
t.Errorf("SyncTriggerPull = %q, want \"pull\"", SyncTriggerPull)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictStrategyConstants(t *testing.T) {
|
||||
if ConflictStrategyNewest != "newest" {
|
||||
t.Errorf("ConflictStrategyNewest = %q, want \"newest\"", ConflictStrategyNewest)
|
||||
}
|
||||
if ConflictStrategyOurs != "ours" {
|
||||
t.Errorf("ConflictStrategyOurs = %q, want \"ours\"", ConflictStrategyOurs)
|
||||
}
|
||||
if ConflictStrategyTheirs != "theirs" {
|
||||
t.Errorf("ConflictStrategyTheirs = %q, want \"theirs\"", ConflictStrategyTheirs)
|
||||
}
|
||||
if ConflictStrategyManual != "manual" {
|
||||
t.Errorf("ConflictStrategyManual = %q, want \"manual\"", ConflictStrategyManual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSovereigntyConstants(t *testing.T) {
|
||||
if SovereigntyT1 != "T1" {
|
||||
t.Errorf("SovereigntyT1 = %q, want \"T1\"", SovereigntyT1)
|
||||
}
|
||||
if SovereigntyT2 != "T2" {
|
||||
t.Errorf("SovereigntyT2 = %q, want \"T2\"", SovereigntyT2)
|
||||
}
|
||||
if SovereigntyT3 != "T3" {
|
||||
t.Errorf("SovereigntyT3 = %q, want \"T3\"", SovereigntyT3)
|
||||
}
|
||||
if SovereigntyT4 != "T4" {
|
||||
t.Errorf("SovereigntyT4 = %q, want \"T4\"", SovereigntyT4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncConfigDefaults(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Test sync mode default
|
||||
if got := GetSyncMode(); got != SyncModeGitPortable {
|
||||
t.Errorf("GetSyncMode() = %q, want %q", got, SyncModeGitPortable)
|
||||
}
|
||||
|
||||
// Test sync config defaults
|
||||
cfg := GetSyncConfig()
|
||||
if cfg.Mode != SyncModeGitPortable {
|
||||
t.Errorf("GetSyncConfig().Mode = %q, want %q", cfg.Mode, SyncModeGitPortable)
|
||||
}
|
||||
if cfg.ExportOn != SyncTriggerPush {
|
||||
t.Errorf("GetSyncConfig().ExportOn = %q, want %q", cfg.ExportOn, SyncTriggerPush)
|
||||
}
|
||||
if cfg.ImportOn != SyncTriggerPull {
|
||||
t.Errorf("GetSyncConfig().ImportOn = %q, want %q", cfg.ImportOn, SyncTriggerPull)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictConfigDefaults(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Test conflict strategy default
|
||||
if got := GetConflictStrategy(); got != ConflictStrategyNewest {
|
||||
t.Errorf("GetConflictStrategy() = %q, want %q", got, ConflictStrategyNewest)
|
||||
}
|
||||
|
||||
// Test conflict config
|
||||
cfg := GetConflictConfig()
|
||||
if cfg.Strategy != ConflictStrategyNewest {
|
||||
t.Errorf("GetConflictConfig().Strategy = %q, want %q", cfg.Strategy, ConflictStrategyNewest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFederationConfigDefaults(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Test federation config defaults (empty)
|
||||
cfg := GetFederationConfig()
|
||||
if cfg.Remote != "" {
|
||||
t.Errorf("GetFederationConfig().Remote = %q, want empty", cfg.Remote)
|
||||
}
|
||||
if cfg.Sovereignty != "" {
|
||||
t.Errorf("GetFederationConfig().Sovereignty = %q, want empty", cfg.Sovereignty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSyncModeValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
mode string
|
||||
valid bool
|
||||
}{
|
||||
{SyncModeGitPortable, true},
|
||||
{SyncModeRealtime, true},
|
||||
{SyncModeDoltNative, true},
|
||||
{SyncModeBeltAndSuspenders, true},
|
||||
{"invalid-mode", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.mode, func(t *testing.T) {
|
||||
if got := IsSyncModeValid(tt.mode); got != tt.valid {
|
||||
t.Errorf("IsSyncModeValid(%q) = %v, want %v", tt.mode, got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsConflictStrategyValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
strategy string
|
||||
valid bool
|
||||
}{
|
||||
{ConflictStrategyNewest, true},
|
||||
{ConflictStrategyOurs, true},
|
||||
{ConflictStrategyTheirs, true},
|
||||
{ConflictStrategyManual, true},
|
||||
{"invalid-strategy", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.strategy, func(t *testing.T) {
|
||||
if got := IsConflictStrategyValid(tt.strategy); got != tt.valid {
|
||||
t.Errorf("IsConflictStrategyValid(%q) = %v, want %v", tt.strategy, got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSovereigntyValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
sovereignty string
|
||||
valid bool
|
||||
}{
|
||||
{SovereigntyT1, true},
|
||||
{SovereigntyT2, true},
|
||||
{SovereigntyT3, true},
|
||||
{SovereigntyT4, true},
|
||||
{"", true}, // Empty is valid (means no restriction)
|
||||
{"T5", false},
|
||||
{"invalid", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.sovereignty, func(t *testing.T) {
|
||||
if got := IsSovereigntyValid(tt.sovereignty); got != tt.valid {
|
||||
t.Errorf("IsSovereigntyValid(%q) = %v, want %v", tt.sovereignty, got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncConfigFromFile(t *testing.T) {
|
||||
// Create a temporary directory for config file
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a config file with sync settings
|
||||
configContent := `
|
||||
sync:
|
||||
mode: realtime
|
||||
export_on: change
|
||||
import_on: change
|
||||
|
||||
conflict:
|
||||
strategy: ours
|
||||
|
||||
federation:
|
||||
remote: dolthub://myorg/beads
|
||||
sovereignty: T2
|
||||
`
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
||||
t.Fatalf("failed to create .beads directory: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(beadsDir, "config.yaml")
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
||||
t.Fatalf("failed to write config file: %v", err)
|
||||
}
|
||||
|
||||
// Change to tmp directory
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
// Initialize viper
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Test sync config
|
||||
syncCfg := GetSyncConfig()
|
||||
if syncCfg.Mode != SyncModeRealtime {
|
||||
t.Errorf("GetSyncConfig().Mode = %q, want %q", syncCfg.Mode, SyncModeRealtime)
|
||||
}
|
||||
if syncCfg.ExportOn != SyncTriggerChange {
|
||||
t.Errorf("GetSyncConfig().ExportOn = %q, want %q", syncCfg.ExportOn, SyncTriggerChange)
|
||||
}
|
||||
if syncCfg.ImportOn != SyncTriggerChange {
|
||||
t.Errorf("GetSyncConfig().ImportOn = %q, want %q", syncCfg.ImportOn, SyncTriggerChange)
|
||||
}
|
||||
|
||||
// Test conflict config
|
||||
conflictCfg := GetConflictConfig()
|
||||
if conflictCfg.Strategy != ConflictStrategyOurs {
|
||||
t.Errorf("GetConflictConfig().Strategy = %q, want %q", conflictCfg.Strategy, ConflictStrategyOurs)
|
||||
}
|
||||
|
||||
// Test federation config
|
||||
fedCfg := GetFederationConfig()
|
||||
if fedCfg.Remote != "dolthub://myorg/beads" {
|
||||
t.Errorf("GetFederationConfig().Remote = %q, want \"dolthub://myorg/beads\"", fedCfg.Remote)
|
||||
}
|
||||
if fedCfg.Sovereignty != SovereigntyT2 {
|
||||
t.Errorf("GetFederationConfig().Sovereignty = %q, want %q", fedCfg.Sovereignty, SovereigntyT2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldExportOnChange(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Default should be false (export on push, not change)
|
||||
if ShouldExportOnChange() {
|
||||
t.Error("ShouldExportOnChange() = true, want false (default)")
|
||||
}
|
||||
|
||||
// Set to change
|
||||
Set("sync.export_on", SyncTriggerChange)
|
||||
if !ShouldExportOnChange() {
|
||||
t.Error("ShouldExportOnChange() = false after setting to change, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldImportOnChange(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Default should be false (import on pull, not change)
|
||||
if ShouldImportOnChange() {
|
||||
t.Error("ShouldImportOnChange() = true, want false (default)")
|
||||
}
|
||||
|
||||
// Set to change
|
||||
Set("sync.import_on", SyncTriggerChange)
|
||||
if !ShouldImportOnChange() {
|
||||
t.Error("ShouldImportOnChange() = false after setting to change, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsDoltRemote(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
tests := []struct {
|
||||
mode string
|
||||
needsRemote bool
|
||||
}{
|
||||
{SyncModeGitPortable, false},
|
||||
{SyncModeRealtime, false},
|
||||
{SyncModeDoltNative, true},
|
||||
{SyncModeBeltAndSuspenders, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.mode, func(t *testing.T) {
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
Set("sync.mode", tt.mode)
|
||||
|
||||
if got := NeedsDoltRemote(); got != tt.needsRemote {
|
||||
t.Errorf("NeedsDoltRemote() with mode=%s = %v, want %v", tt.mode, got, tt.needsRemote)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsJSONL(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
tests := []struct {
|
||||
mode string
|
||||
needsJSONL bool
|
||||
}{
|
||||
{SyncModeGitPortable, true},
|
||||
{SyncModeRealtime, true},
|
||||
{SyncModeDoltNative, false},
|
||||
{SyncModeBeltAndSuspenders, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.mode, func(t *testing.T) {
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
Set("sync.mode", tt.mode)
|
||||
|
||||
if got := NeedsJSONL(); got != tt.needsJSONL {
|
||||
t.Errorf("NeedsJSONL() with mode=%s = %v, want %v", tt.mode, got, tt.needsJSONL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSyncModeInvalid(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Set invalid mode - should fall back to git-portable
|
||||
Set("sync.mode", "invalid-mode")
|
||||
if got := GetSyncMode(); got != SyncModeGitPortable {
|
||||
t.Errorf("GetSyncMode() with invalid mode = %q, want %q (fallback)", got, SyncModeGitPortable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConflictStrategyInvalid(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Set invalid strategy - should fall back to newest
|
||||
Set("conflict.strategy", "invalid-strategy")
|
||||
if got := GetConflictStrategy(); got != ConflictStrategyNewest {
|
||||
t.Errorf("GetConflictStrategy() with invalid strategy = %q, want %q (fallback)", got, ConflictStrategyNewest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSovereigntyInvalid(t *testing.T) {
|
||||
// Isolate from environment variables
|
||||
restore := envSnapshot(t)
|
||||
defer restore()
|
||||
|
||||
// Initialize config
|
||||
if err := Initialize(); err != nil {
|
||||
t.Fatalf("Initialize() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Set invalid sovereignty - should return empty
|
||||
Set("federation.sovereignty", "T99")
|
||||
if got := GetSovereignty(); got != "" {
|
||||
t.Errorf("GetSovereignty() with invalid tier = %q, want empty (fallback)", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user