* fix(config): remove duplicate declarations between config.go and sync.go Commite82e15a8created sync.go with typed constants (SyncMode, ConflictStrategy, Sovereignty) but didn't remove the original untyped constants from config.go that were added in16f8c3d3. This caused redeclaration errors preventing the project from building. Changes: - Remove duplicate SyncMode, ConflictStrategy, Sovereignty constants from config.go (keep typed versions in sync.go) - Remove duplicate GetSyncMode, GetConflictStrategy, GetSovereignty functions from config.go (keep sync.go versions with warnings) - Update SyncConfig, ConflictConfig, FederationConfig structs to use typed fields instead of string - Add IsSyncModeValid, IsConflictStrategyValid, IsSovereigntyValid wrapper functions that use sync.go's validation maps - Update cmd/bd/sync.go to use typed ConflictStrategy parameter - Update tests to work with typed constants * fix(dolt): handle Merge return values in concurrent test * fix(test): add --repo flag to show_test.go to bypass auto-routing The tests were failing because the create command was routing issues to ~/.beads-planning instead of the test's temp directory. Adding --repo . overrides auto-routing and creates issues in the test dir.
1416 lines
39 KiB
Go
1416 lines
39 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// envSnapshot saves and clears BD_/BEADS_ environment variables.
|
|
// Returns a restore function that should be deferred.
|
|
func envSnapshot(t *testing.T) func() {
|
|
t.Helper()
|
|
saved := make(map[string]string)
|
|
for _, env := range os.Environ() {
|
|
if strings.HasPrefix(env, "BD_") || strings.HasPrefix(env, "BEADS_") {
|
|
parts := strings.SplitN(env, "=", 2)
|
|
key := parts[0]
|
|
saved[key] = os.Getenv(key)
|
|
os.Unsetenv(key)
|
|
}
|
|
}
|
|
return func() {
|
|
// Clear any test-set variables first
|
|
for _, env := range os.Environ() {
|
|
if strings.HasPrefix(env, "BD_") || strings.HasPrefix(env, "BEADS_") {
|
|
parts := strings.SplitN(env, "=", 2)
|
|
os.Unsetenv(parts[0])
|
|
}
|
|
}
|
|
// Restore original values
|
|
for key, val := range saved {
|
|
os.Setenv(key, val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInitialize(t *testing.T) {
|
|
// Test that initialization doesn't error
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
if v == nil {
|
|
t.Fatal("viper instance is nil after Initialize()")
|
|
}
|
|
}
|
|
|
|
func TestDefaults(t *testing.T) {
|
|
// Isolate from environment variables
|
|
restore := envSnapshot(t)
|
|
defer restore()
|
|
|
|
// Reset viper for test isolation
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
key string
|
|
expected interface{}
|
|
getter func(string) interface{}
|
|
}{
|
|
{"json", false, func(k string) interface{} { return GetBool(k) }},
|
|
{"no-daemon", false, func(k string) interface{} { return GetBool(k) }},
|
|
{"no-auto-flush", false, func(k string) interface{} { return GetBool(k) }},
|
|
{"no-auto-import", false, func(k string) interface{} { return GetBool(k) }},
|
|
{"db", "", func(k string) interface{} { return GetString(k) }},
|
|
{"actor", "", func(k string) interface{} { return GetString(k) }},
|
|
{"flush-debounce", 30 * time.Second, func(k string) interface{} { return GetDuration(k) }},
|
|
{"auto-start-daemon", true, func(k string) interface{} { return GetBool(k) }},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.key, func(t *testing.T) {
|
|
got := tt.getter(tt.key)
|
|
if got != tt.expected {
|
|
t.Errorf("GetXXX(%q) = %v, want %v", tt.key, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnvironmentBinding(t *testing.T) {
|
|
// Test environment variable binding
|
|
tests := []struct {
|
|
envVar string
|
|
key string
|
|
value string
|
|
expected interface{}
|
|
getter func(string) interface{}
|
|
}{
|
|
{"BD_JSON", "json", "true", true, func(k string) interface{} { return GetBool(k) }},
|
|
{"BD_NO_DAEMON", "no-daemon", "true", true, func(k string) interface{} { return GetBool(k) }},
|
|
{"BD_ACTOR", "actor", "testuser", "testuser", func(k string) interface{} { return GetString(k) }},
|
|
{"BD_DB", "db", "/tmp/test.db", "/tmp/test.db", func(k string) interface{} { return GetString(k) }},
|
|
{"BEADS_FLUSH_DEBOUNCE", "flush-debounce", "10s", 10 * time.Second, func(k string) interface{} { return GetDuration(k) }},
|
|
{"BEADS_AUTO_START_DAEMON", "auto-start-daemon", "false", false, func(k string) interface{} { return GetBool(k) }},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.envVar, func(t *testing.T) {
|
|
// Set environment variable
|
|
oldValue := os.Getenv(tt.envVar)
|
|
_ = os.Setenv(tt.envVar, tt.value)
|
|
defer os.Setenv(tt.envVar, oldValue)
|
|
|
|
// Re-initialize viper to pick up env var
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
got := tt.getter(tt.key)
|
|
if got != tt.expected {
|
|
t.Errorf("GetXXX(%q) with %s=%s = %v, want %v", tt.key, tt.envVar, tt.value, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigFile(t *testing.T) {
|
|
// Isolate from environment variables
|
|
restore := envSnapshot(t)
|
|
defer restore()
|
|
|
|
// Create a temporary directory for config file
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a config file
|
|
configContent := `
|
|
json: true
|
|
no-daemon: true
|
|
actor: configuser
|
|
flush-debounce: 15s
|
|
`
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config file: %v", err)
|
|
}
|
|
|
|
// Create .beads directory
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
|
t.Fatalf("failed to create .beads directory: %v", err)
|
|
}
|
|
|
|
// Move config to .beads directory
|
|
beadsConfigPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.Rename(configPath, beadsConfigPath); err != nil {
|
|
t.Fatalf("failed to move config file: %v", err)
|
|
}
|
|
|
|
// Change to tmp directory so config file is discovered
|
|
t.Chdir(tmpDir)
|
|
|
|
// Initialize viper
|
|
var err error
|
|
err = Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test that config file values are loaded
|
|
if got := GetBool("json"); got != true {
|
|
t.Errorf("GetBool(json) = %v, want true", got)
|
|
}
|
|
|
|
if got := GetBool("no-daemon"); got != true {
|
|
t.Errorf("GetBool(no-daemon) = %v, want true", got)
|
|
}
|
|
|
|
if got := GetString("actor"); got != "configuser" {
|
|
t.Errorf("GetString(actor) = %q, want \"configuser\"", got)
|
|
}
|
|
|
|
if got := GetDuration("flush-debounce"); got != 15*time.Second {
|
|
t.Errorf("GetDuration(flush-debounce) = %v, want 15s", got)
|
|
}
|
|
}
|
|
|
|
func TestConfigPrecedence(t *testing.T) {
|
|
// Create a temporary directory for config file
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a config file with json: false
|
|
configContent := `json: false`
|
|
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)
|
|
|
|
// Test 1: Config file value (json: false)
|
|
var err error
|
|
err = Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
if got := GetBool("json"); got != false {
|
|
t.Errorf("GetBool(json) from config file = %v, want false", got)
|
|
}
|
|
|
|
// Test 2: Environment variable overrides config file
|
|
_ = os.Setenv("BD_JSON", "true")
|
|
defer func() { _ = os.Unsetenv("BD_JSON") }()
|
|
|
|
err = Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
if got := GetBool("json"); got != true {
|
|
t.Errorf("GetBool(json) with env var = %v, want true (env should override config)", got)
|
|
}
|
|
}
|
|
|
|
func TestSetAndGet(t *testing.T) {
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test Set and Get
|
|
Set("test-key", "test-value")
|
|
if got := GetString("test-key"); got != "test-value" {
|
|
t.Errorf("GetString(test-key) = %q, want \"test-value\"", got)
|
|
}
|
|
|
|
Set("test-bool", true)
|
|
if got := GetBool("test-bool"); got != true {
|
|
t.Errorf("GetBool(test-bool) = %v, want true", got)
|
|
}
|
|
|
|
Set("test-int", 42)
|
|
if got := GetInt("test-int"); got != 42 {
|
|
t.Errorf("GetInt(test-int) = %d, want 42", got)
|
|
}
|
|
}
|
|
|
|
func TestAllSettings(t *testing.T) {
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
Set("custom-key", "custom-value")
|
|
|
|
settings := AllSettings()
|
|
if settings == nil {
|
|
t.Fatal("AllSettings() returned nil")
|
|
}
|
|
|
|
// Check that our custom key is in the settings
|
|
if val, ok := settings["custom-key"]; !ok || val != "custom-value" {
|
|
t.Errorf("AllSettings() missing or incorrect custom-key: got %v", val)
|
|
}
|
|
}
|
|
|
|
func TestGetStringSlice(t *testing.T) {
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test with Set
|
|
Set("test-slice", []string{"a", "b", "c"})
|
|
got := GetStringSlice("test-slice")
|
|
if len(got) != 3 || got[0] != "a" || got[1] != "b" || got[2] != "c" {
|
|
t.Errorf("GetStringSlice(test-slice) = %v, want [a b c]", got)
|
|
}
|
|
|
|
// Test with non-existent key - should return empty/nil slice
|
|
got = GetStringSlice("nonexistent-key")
|
|
if len(got) != 0 {
|
|
t.Errorf("GetStringSlice(nonexistent-key) = %v, want empty slice", got)
|
|
}
|
|
}
|
|
|
|
func TestGetStringSliceFromConfig(t *testing.T) {
|
|
// Create a temporary directory for config file
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a config file with string slice
|
|
configContent := `
|
|
repos:
|
|
primary: /path/to/primary
|
|
additional:
|
|
- /path/to/repo1
|
|
- /path/to/repo2
|
|
`
|
|
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
|
|
var err error
|
|
err = Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test that string slice is loaded correctly
|
|
got := GetStringSlice("repos.additional")
|
|
if len(got) != 2 || got[0] != "/path/to/repo1" || got[1] != "/path/to/repo2" {
|
|
t.Errorf("GetStringSlice(repos.additional) = %v, want [/path/to/repo1 /path/to/repo2]", got)
|
|
}
|
|
}
|
|
|
|
func TestGetMultiRepoConfig(t *testing.T) {
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test when repos.primary is not set (single-repo mode)
|
|
config := GetMultiRepoConfig()
|
|
if config != nil {
|
|
t.Errorf("GetMultiRepoConfig() with no repos.primary = %+v, want nil", config)
|
|
}
|
|
|
|
// Test when repos.primary is set (multi-repo mode)
|
|
Set("repos.primary", "/path/to/primary")
|
|
Set("repos.additional", []string{"/path/to/repo1", "/path/to/repo2"})
|
|
|
|
config = GetMultiRepoConfig()
|
|
if config == nil {
|
|
t.Fatal("GetMultiRepoConfig() returned nil when repos.primary is set")
|
|
}
|
|
|
|
if config.Primary != "/path/to/primary" {
|
|
t.Errorf("GetMultiRepoConfig().Primary = %q, want \"/path/to/primary\"", config.Primary)
|
|
}
|
|
|
|
if len(config.Additional) != 2 || config.Additional[0] != "/path/to/repo1" || config.Additional[1] != "/path/to/repo2" {
|
|
t.Errorf("GetMultiRepoConfig().Additional = %v, want [/path/to/repo1 /path/to/repo2]", config.Additional)
|
|
}
|
|
}
|
|
|
|
func TestGetMultiRepoConfigFromFile(t *testing.T) {
|
|
// Create a temporary directory for config file
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a config file with multi-repo config
|
|
configContent := `
|
|
repos:
|
|
primary: /main/repo
|
|
additional:
|
|
- /extra/repo1
|
|
- /extra/repo2
|
|
- /extra/repo3
|
|
`
|
|
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
|
|
var err error
|
|
err = Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test that multi-repo config is loaded correctly
|
|
config := GetMultiRepoConfig()
|
|
if config == nil {
|
|
t.Fatal("GetMultiRepoConfig() returned nil")
|
|
}
|
|
|
|
if config.Primary != "/main/repo" {
|
|
t.Errorf("GetMultiRepoConfig().Primary = %q, want \"/main/repo\"", config.Primary)
|
|
}
|
|
|
|
if len(config.Additional) != 3 {
|
|
t.Errorf("GetMultiRepoConfig().Additional has %d items, want 3", len(config.Additional))
|
|
}
|
|
}
|
|
|
|
func TestNilViperBehavior(t *testing.T) {
|
|
// Save the current viper instance
|
|
savedV := v
|
|
|
|
// Set viper to nil to test nil-safety
|
|
v = nil
|
|
defer func() { v = savedV }()
|
|
|
|
// All getters should return zero values without panicking
|
|
if got := GetString("any-key"); got != "" {
|
|
t.Errorf("GetString with nil viper = %q, want \"\"", got)
|
|
}
|
|
|
|
if got := GetBool("any-key"); got != false {
|
|
t.Errorf("GetBool with nil viper = %v, want false", got)
|
|
}
|
|
|
|
if got := GetInt("any-key"); got != 0 {
|
|
t.Errorf("GetInt with nil viper = %d, want 0", got)
|
|
}
|
|
|
|
if got := GetDuration("any-key"); got != 0 {
|
|
t.Errorf("GetDuration with nil viper = %v, want 0", got)
|
|
}
|
|
|
|
if got := GetStringSlice("any-key"); got == nil || len(got) != 0 {
|
|
t.Errorf("GetStringSlice with nil viper = %v, want empty slice", got)
|
|
}
|
|
|
|
if got := AllSettings(); got == nil || len(got) != 0 {
|
|
t.Errorf("AllSettings with nil viper = %v, want empty map", got)
|
|
}
|
|
|
|
if got := GetMultiRepoConfig(); got != nil {
|
|
t.Errorf("GetMultiRepoConfig with nil viper = %+v, want nil", got)
|
|
}
|
|
|
|
// Set should not panic
|
|
Set("any-key", "any-value") // Should be a no-op
|
|
}
|
|
|
|
func TestGetIdentity(t *testing.T) {
|
|
// Initialize viper
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test 1: Flag value takes precedence over everything
|
|
got := GetIdentity("flag-identity")
|
|
if got != "flag-identity" {
|
|
t.Errorf("GetIdentity(flag-identity) = %q, want \"flag-identity\"", got)
|
|
}
|
|
|
|
// Test 2: Empty flag falls back to BEADS_IDENTITY env
|
|
oldEnv := os.Getenv("BEADS_IDENTITY")
|
|
_ = os.Setenv("BEADS_IDENTITY", "env-identity")
|
|
defer func() {
|
|
if oldEnv == "" {
|
|
_ = os.Unsetenv("BEADS_IDENTITY")
|
|
} else {
|
|
_ = os.Setenv("BEADS_IDENTITY", oldEnv)
|
|
}
|
|
}()
|
|
|
|
// Re-initialize to pick up env var
|
|
_ = Initialize()
|
|
got = GetIdentity("")
|
|
if got != "env-identity" {
|
|
t.Errorf("GetIdentity(\"\") with BEADS_IDENTITY = %q, want \"env-identity\"", got)
|
|
}
|
|
|
|
// Test 3: Without flag or env, should fall back to git user.name or hostname
|
|
_ = os.Unsetenv("BEADS_IDENTITY")
|
|
_ = Initialize()
|
|
got = GetIdentity("")
|
|
// We can't predict the exact value (depends on git config and hostname)
|
|
// but it should not be empty or "unknown" on most systems
|
|
if got == "" {
|
|
t.Error("GetIdentity(\"\") without flag or env returned empty string")
|
|
}
|
|
}
|
|
|
|
func TestGetExternalProjects(t *testing.T) {
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test default (empty map)
|
|
got := GetExternalProjects()
|
|
if got == nil {
|
|
t.Error("GetExternalProjects() returned nil, want empty map")
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("GetExternalProjects() = %v, want empty map", got)
|
|
}
|
|
|
|
// Test with Set
|
|
Set("external_projects", map[string]string{
|
|
"beads": "../beads",
|
|
"gastown": "/absolute/path/to/gastown",
|
|
})
|
|
|
|
got = GetExternalProjects()
|
|
if len(got) != 2 {
|
|
t.Errorf("GetExternalProjects() has %d items, want 2", len(got))
|
|
}
|
|
if got["beads"] != "../beads" {
|
|
t.Errorf("GetExternalProjects()[beads] = %q, want \"../beads\"", got["beads"])
|
|
}
|
|
if got["gastown"] != "/absolute/path/to/gastown" {
|
|
t.Errorf("GetExternalProjects()[gastown] = %q, want \"/absolute/path/to/gastown\"", got["gastown"])
|
|
}
|
|
}
|
|
|
|
func TestGetExternalProjectsFromConfig(t *testing.T) {
|
|
// Create a temporary directory for config file
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a config file with external_projects
|
|
configContent := `
|
|
external_projects:
|
|
beads: ../beads
|
|
gastown: /path/to/gastown
|
|
other: ./relative/path
|
|
`
|
|
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
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test that external_projects is loaded correctly
|
|
got := GetExternalProjects()
|
|
if len(got) != 3 {
|
|
t.Errorf("GetExternalProjects() has %d items, want 3", len(got))
|
|
}
|
|
if got["beads"] != "../beads" {
|
|
t.Errorf("GetExternalProjects()[beads] = %q, want \"../beads\"", got["beads"])
|
|
}
|
|
if got["gastown"] != "/path/to/gastown" {
|
|
t.Errorf("GetExternalProjects()[gastown] = %q, want \"/path/to/gastown\"", got["gastown"])
|
|
}
|
|
if got["other"] != "./relative/path" {
|
|
t.Errorf("GetExternalProjects()[other] = %q, want \"./relative/path\"", got["other"])
|
|
}
|
|
}
|
|
|
|
func TestResolveExternalProjectPath(t *testing.T) {
|
|
// Create a temporary directory structure
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a project directory to resolve to
|
|
projectDir := filepath.Join(tmpDir, "beads-project")
|
|
if err := os.MkdirAll(projectDir, 0750); err != nil {
|
|
t.Fatalf("failed to create project directory: %v", err)
|
|
}
|
|
|
|
// Create config file
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
|
t.Fatalf("failed to create .beads directory: %v", err)
|
|
}
|
|
|
|
configContent := `
|
|
external_projects:
|
|
beads: beads-project
|
|
missing: nonexistent-path
|
|
absolute: ` + projectDir + `
|
|
`
|
|
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
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test resolving a relative path that exists
|
|
got := ResolveExternalProjectPath("beads")
|
|
if got != projectDir {
|
|
t.Errorf("ResolveExternalProjectPath(beads) = %q, want %q", got, projectDir)
|
|
}
|
|
|
|
// Test resolving a path that doesn't exist
|
|
got = ResolveExternalProjectPath("missing")
|
|
if got != "" {
|
|
t.Errorf("ResolveExternalProjectPath(missing) = %q, want empty string", got)
|
|
}
|
|
|
|
// Test resolving a project that isn't configured
|
|
got = ResolveExternalProjectPath("unknown")
|
|
if got != "" {
|
|
t.Errorf("ResolveExternalProjectPath(unknown) = %q, want empty string", got)
|
|
}
|
|
|
|
// Test resolving an absolute path
|
|
got = ResolveExternalProjectPath("absolute")
|
|
if got != projectDir {
|
|
t.Errorf("ResolveExternalProjectPath(absolute) = %q, want %q", got, projectDir)
|
|
}
|
|
}
|
|
|
|
func TestGetIdentityFromConfig(t *testing.T) {
|
|
// Create a temporary directory for config file
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a config file with identity
|
|
configContent := `identity: config-identity`
|
|
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)
|
|
}
|
|
|
|
// Clear BEADS_IDENTITY env var
|
|
oldEnv := os.Getenv("BEADS_IDENTITY")
|
|
_ = os.Unsetenv("BEADS_IDENTITY")
|
|
defer func() {
|
|
if oldEnv != "" {
|
|
_ = os.Setenv("BEADS_IDENTITY", oldEnv)
|
|
}
|
|
}()
|
|
|
|
// Change to tmp directory
|
|
t.Chdir(tmpDir)
|
|
|
|
// Initialize viper
|
|
err := Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Test that identity from config file is used
|
|
got := GetIdentity("")
|
|
if got != "config-identity" {
|
|
t.Errorf("GetIdentity(\"\") with config file = %q, want \"config-identity\"", got)
|
|
}
|
|
|
|
// Test that flag still takes precedence
|
|
got = GetIdentity("flag-override")
|
|
if got != "flag-override" {
|
|
t.Errorf("GetIdentity(flag-override) = %q, want \"flag-override\"", got)
|
|
}
|
|
}
|
|
|
|
func TestGetValueSource(t *testing.T) {
|
|
// Initialize config
|
|
if err := Initialize(); err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
setup func()
|
|
cleanup func()
|
|
expected ConfigSource
|
|
}{
|
|
{
|
|
name: "default value returns SourceDefault",
|
|
key: "json",
|
|
setup: func() {},
|
|
cleanup: func() {},
|
|
expected: SourceDefault,
|
|
},
|
|
{
|
|
name: "env var returns SourceEnvVar",
|
|
key: "json",
|
|
setup: func() {
|
|
os.Setenv("BD_JSON", "true")
|
|
},
|
|
cleanup: func() {
|
|
os.Unsetenv("BD_JSON")
|
|
},
|
|
expected: SourceEnvVar,
|
|
},
|
|
{
|
|
name: "BEADS_ prefixed env var returns SourceEnvVar",
|
|
key: "identity",
|
|
setup: func() {
|
|
os.Setenv("BEADS_IDENTITY", "test-identity")
|
|
},
|
|
cleanup: func() {
|
|
os.Unsetenv("BEADS_IDENTITY")
|
|
},
|
|
expected: SourceEnvVar,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Reinitialize to clear state
|
|
if err := Initialize(); err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
tt.setup()
|
|
defer tt.cleanup()
|
|
|
|
got := GetValueSource(tt.key)
|
|
if got != tt.expected {
|
|
t.Errorf("GetValueSource(%q) = %v, want %v", tt.key, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckOverrides_FlagOverridesEnvVar(t *testing.T) {
|
|
// Initialize config
|
|
if err := Initialize(); err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
|
|
// Set an env var
|
|
os.Setenv("BD_JSON", "true")
|
|
defer os.Unsetenv("BD_JSON")
|
|
|
|
// Simulate flag override
|
|
flagOverrides := map[string]struct {
|
|
Value interface{}
|
|
WasSet bool
|
|
}{
|
|
"json": {Value: false, WasSet: true},
|
|
}
|
|
|
|
overrides := CheckOverrides(flagOverrides)
|
|
|
|
// Should detect that flag overrides env var
|
|
found := false
|
|
for _, o := range overrides {
|
|
if o.Key == "json" && o.OverriddenBy == SourceFlag {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Error("Expected to find flag override for 'json' key")
|
|
}
|
|
}
|
|
|
|
func TestConfigSourceConstants(t *testing.T) {
|
|
// Verify source constants have expected string values
|
|
if SourceDefault != "default" {
|
|
t.Errorf("SourceDefault = %q, want \"default\"", SourceDefault)
|
|
}
|
|
if SourceConfigFile != "config_file" {
|
|
t.Errorf("SourceConfigFile = %q, want \"config_file\"", SourceConfigFile)
|
|
}
|
|
if SourceEnvVar != "env_var" {
|
|
t.Errorf("SourceEnvVar = %q, want \"env_var\"", SourceEnvVar)
|
|
}
|
|
if SourceFlag != "flag" {
|
|
t.Errorf("SourceFlag = %q, want \"flag\"", SourceFlag)
|
|
}
|
|
}
|
|
|
|
// TestResolveExternalProjectPathFromRepoRoot tests that external_projects paths
|
|
// are resolved from repo root (parent of .beads/), NOT from CWD.
|
|
// This is the fix for oss-lbp (related to Bug 3 in the spec).
|
|
func TestResolveExternalProjectPathFromRepoRoot(t *testing.T) {
|
|
// Helper to canonicalize paths for comparison (handles macOS /var -> /private/var symlink)
|
|
canonicalize := func(path string) string {
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
resolved, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return path
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
t.Run("relative path resolved from repo root not CWD", func(t *testing.T) {
|
|
// Create a repo structure:
|
|
// tmpDir/
|
|
// .beads/
|
|
// config.yaml
|
|
// beads-project/ <- relative path should resolve here
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create .beads directory with config file
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
|
t.Fatalf("failed to create .beads dir: %v", err)
|
|
}
|
|
|
|
// Create the target project directory
|
|
projectDir := filepath.Join(tmpDir, "beads-project")
|
|
if err := os.MkdirAll(projectDir, 0750); err != nil {
|
|
t.Fatalf("failed to create project dir: %v", err)
|
|
}
|
|
|
|
// Create config file with relative path
|
|
configContent := `
|
|
external_projects:
|
|
beads: beads-project
|
|
`
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
|
|
// Change to a DIFFERENT directory (to test that CWD doesn't affect resolution)
|
|
// This simulates daemon context where CWD is .beads/
|
|
origDir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get cwd: %v", err)
|
|
}
|
|
if err := os.Chdir(beadsDir); err != nil {
|
|
t.Fatalf("failed to chdir: %v", err)
|
|
}
|
|
defer os.Chdir(origDir)
|
|
|
|
// Reload config from the new location
|
|
if err := Initialize(); err != nil {
|
|
t.Fatalf("failed to initialize config: %v", err)
|
|
}
|
|
|
|
// Verify ConfigFileUsed() returns the config path
|
|
usedConfig := ConfigFileUsed()
|
|
if usedConfig == "" {
|
|
t.Skip("config file not loaded - skipping test")
|
|
}
|
|
|
|
// Resolve the external project path
|
|
got := ResolveExternalProjectPath("beads")
|
|
|
|
// The path should resolve to tmpDir/beads-project (repo root + relative path)
|
|
// NOT to .beads/beads-project (CWD + relative path)
|
|
// Use canonicalize to handle macOS /var -> /private/var symlink
|
|
if canonicalize(got) != canonicalize(projectDir) {
|
|
t.Errorf("ResolveExternalProjectPath(beads) = %q, want %q", got, projectDir)
|
|
}
|
|
|
|
// Verify the wrong path doesn't exist (CWD-based resolution)
|
|
wrongPath := filepath.Join(beadsDir, "beads-project")
|
|
if canonicalize(got) == canonicalize(wrongPath) {
|
|
t.Errorf("path was incorrectly resolved from CWD: %s", wrongPath)
|
|
}
|
|
})
|
|
|
|
t.Run("CWD should not affect resolution", func(t *testing.T) {
|
|
// Create two different directory structures
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create main repo with .beads and target project
|
|
mainRepoDir := filepath.Join(tmpDir, "main-repo")
|
|
beadsDir := filepath.Join(mainRepoDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
|
t.Fatalf("failed to create .beads dir: %v", err)
|
|
}
|
|
|
|
// Create the target project as a sibling directory
|
|
siblingProject := filepath.Join(tmpDir, "sibling-project")
|
|
if err := os.MkdirAll(siblingProject, 0750); err != nil {
|
|
t.Fatalf("failed to create sibling project: %v", err)
|
|
}
|
|
|
|
// Create config file with parent-relative path
|
|
configContent := `
|
|
external_projects:
|
|
sibling: ../sibling-project
|
|
`
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
|
|
// Test from multiple different CWDs
|
|
// Note: We only test from mainRepoDir and beadsDir, not from tmpDir
|
|
// because when CWD is tmpDir, the config file at mainRepoDir/.beads/config.yaml
|
|
// won't be discovered (viper searches from CWD upward)
|
|
testDirs := []string{
|
|
mainRepoDir, // From repo root
|
|
beadsDir, // From .beads/ (daemon context)
|
|
}
|
|
|
|
for _, testDir := range testDirs {
|
|
// Change to test directory
|
|
origDir, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("failed to get cwd: %v", err)
|
|
}
|
|
if err := os.Chdir(testDir); err != nil {
|
|
t.Fatalf("failed to chdir to %s: %v", testDir, err)
|
|
}
|
|
|
|
// Reload config
|
|
if err := Initialize(); err != nil {
|
|
os.Chdir(origDir)
|
|
t.Fatalf("failed to initialize config: %v", err)
|
|
}
|
|
|
|
// Resolve the external project path
|
|
got := ResolveExternalProjectPath("sibling")
|
|
|
|
// Restore CWD before checking result
|
|
os.Chdir(origDir)
|
|
|
|
// Path should always resolve to the sibling project,
|
|
// regardless of which directory we were in
|
|
// Use canonicalize to handle macOS /var -> /private/var symlink
|
|
if canonicalize(got) != canonicalize(siblingProject) {
|
|
t.Errorf("from CWD=%s: ResolveExternalProjectPath(sibling) = %q, want %q",
|
|
testDir, got, siblingProject)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidationConfigDefaults(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 validation.on-create default is "none"
|
|
if got := GetString("validation.on-create"); got != "none" {
|
|
t.Errorf("GetString(validation.on-create) = %q, want \"none\"", got)
|
|
}
|
|
|
|
// Test validation.on-sync default is "none"
|
|
if got := GetString("validation.on-sync"); got != "none" {
|
|
t.Errorf("GetString(validation.on-sync) = %q, want \"none\"", got)
|
|
}
|
|
}
|
|
|
|
func TestValidationConfigFromFile(t *testing.T) {
|
|
// Create a temporary directory for config file
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a config file with validation settings
|
|
configContent := `
|
|
validation:
|
|
on-create: error
|
|
on-sync: warn
|
|
`
|
|
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 that validation settings are loaded correctly
|
|
if got := GetString("validation.on-create"); got != "error" {
|
|
t.Errorf("GetString(validation.on-create) = %q, want \"error\"", got)
|
|
}
|
|
if got := GetString("validation.on-sync"); got != "warn" {
|
|
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
|
|
cfg := GetFederationConfig()
|
|
if cfg.Remote != "" {
|
|
t.Errorf("GetFederationConfig().Remote = %q, want empty", cfg.Remote)
|
|
}
|
|
// Default sovereignty is T1 when not configured
|
|
if cfg.Sovereignty != SovereigntyT1 {
|
|
t.Errorf("GetFederationConfig().Sovereignty = %q, want %q (default)", cfg.Sovereignty, SovereigntyT1)
|
|
}
|
|
}
|
|
|
|
func TestIsSyncModeValid(t *testing.T) {
|
|
tests := []struct {
|
|
mode string
|
|
valid bool
|
|
}{
|
|
{string(SyncModeGitPortable), true},
|
|
{string(SyncModeRealtime), true},
|
|
{string(SyncModeDoltNative), true},
|
|
{string(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
|
|
}{
|
|
{string(ConflictStrategyNewest), true},
|
|
{string(ConflictStrategyOurs), true},
|
|
{string(ConflictStrategyTheirs), true},
|
|
{string(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
|
|
}{
|
|
{string(SovereigntyT1), true},
|
|
{string(SovereigntyT2), true},
|
|
{string(SovereigntyT3), true},
|
|
{string(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 SyncMode
|
|
needsRemote bool
|
|
}{
|
|
{SyncModeGitPortable, false},
|
|
{SyncModeRealtime, false},
|
|
{SyncModeDoltNative, true},
|
|
{SyncModeBeltAndSuspenders, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.mode), func(t *testing.T) {
|
|
if err := Initialize(); err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
Set("sync.mode", string(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 SyncMode
|
|
needsJSONL bool
|
|
}{
|
|
{SyncModeGitPortable, true},
|
|
{SyncModeRealtime, true},
|
|
{SyncModeDoltNative, false},
|
|
{SyncModeBeltAndSuspenders, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.mode), func(t *testing.T) {
|
|
if err := Initialize(); err != nil {
|
|
t.Fatalf("Initialize() returned error: %v", err)
|
|
}
|
|
Set("sync.mode", string(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 T1 (default) with warning
|
|
Set("federation.sovereignty", "T99")
|
|
if got := GetSovereignty(); got != SovereigntyT1 {
|
|
t.Errorf("GetSovereignty() with invalid tier = %q, want %q (fallback)", got, SovereigntyT1)
|
|
}
|
|
}
|