diff options
Diffstat (limited to 'internal/runtimeconfig')
| -rw-r--r-- | internal/runtimeconfig/store_test.go | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/internal/runtimeconfig/store_test.go b/internal/runtimeconfig/store_test.go index 168d2cd..906d7f6 100644 --- a/internal/runtimeconfig/store_test.go +++ b/internal/runtimeconfig/store_test.go @@ -97,6 +97,285 @@ func TestStoreReloadLogsSummary(t *testing.T) { } } +func TestSubscribe_NilListener(t *testing.T) { + store := New(appconfig.App{}) + unsub := store.Subscribe(nil) + // Should return a no-op unsubscribe without panicking. + unsub() +} + +func TestSubscribe_ReceivesUpdates(t *testing.T) { + store := New(appconfig.App{MaxTokens: 100}) + + var gotOld, gotNew appconfig.App + callCount := 0 + unsub := store.Subscribe(func(old, new appconfig.App) { + gotOld = old + gotNew = new + callCount++ + }) + + store.Set(appconfig.App{MaxTokens: 200}) + if callCount != 1 { + t.Fatalf("expected listener called once, got %d", callCount) + } + if gotOld.MaxTokens != 100 || gotNew.MaxTokens != 200 { + t.Fatalf("unexpected old/new: %d / %d", gotOld.MaxTokens, gotNew.MaxTokens) + } + + // After unsubscribe, listener must not be called again. + unsub() + store.Set(appconfig.App{MaxTokens: 300}) + if callCount != 1 { + t.Fatalf("expected listener not called after unsubscribe, got %d", callCount) + } +} + +func TestSubscribe_MultipleListeners(t *testing.T) { + store := New(appconfig.App{}) + calls := [2]int{} + unsub0 := store.Subscribe(func(_, _ appconfig.App) { calls[0]++ }) + unsub1 := store.Subscribe(func(_, _ appconfig.App) { calls[1]++ }) + + store.Set(appconfig.App{MaxTokens: 1}) + if calls[0] != 1 || calls[1] != 1 { + t.Fatalf("expected both listeners called once: %v", calls) + } + + // Unsubscribe first listener only. + unsub0() + store.Set(appconfig.App{MaxTokens: 2}) + if calls[0] != 1 || calls[1] != 2 { + t.Fatalf("expected only second listener called: %v", calls) + } + unsub1() +} + +func TestSet_ReturnsChanges(t *testing.T) { + store := New(appconfig.App{MaxTokens: 10, Provider: "ollama"}) + changes := store.Set(appconfig.App{MaxTokens: 20, Provider: "ollama"}) + found := false + for _, ch := range changes { + if ch.Key == "max_tokens" { + found = true + if ch.Old != "10" || ch.New != "20" { + t.Fatalf("unexpected change values: %+v", ch) + } + } + } + if !found { + t.Fatalf("expected max_tokens in changes, got %+v", changes) + } +} + +func TestSet_NoChanges(t *testing.T) { + cfg := appconfig.App{MaxTokens: 10} + store := New(cfg) + changes := store.Set(cfg) + if len(changes) != 0 { + t.Fatalf("expected no changes, got %+v", changes) + } +} + +func TestReload_NilLogger(t *testing.T) { + // Reload with nil logger should not panic; it exercises the nil-logger guard + // in Reload (skipping logger.Print). LoadWithOptions returns defaults when + // logger is nil, so the store gets default config applied. + store := New(appconfig.App{MaxTokens: 1}) + changes, err := store.Reload(nil, appconfig.LoadOptions{IgnoreEnv: true}) + if err != nil { + t.Fatalf("reload failed: %v", err) + } + // Config was updated from our custom value (1) to defaults (4000). + if snap := store.Snapshot(); snap.MaxTokens != 4000 { + t.Fatalf("expected default 4000, got %d", snap.MaxTokens) + } + // Should report a change for max_tokens at minimum. + found := false + for _, ch := range changes { + if ch.Key == "max_tokens" { + found = true + } + } + if !found { + t.Fatalf("expected max_tokens change, got %+v", changes) + } +} + +func TestFormatSummary_NoChanges(t *testing.T) { + result := FormatSummary("Test", nil) + if result != "Test (no changes detected)." { + t.Fatalf("unexpected: %q", result) + } +} + +func TestFormatSummary_WithChanges(t *testing.T) { + changes := []Change{ + {Key: "a", Old: "1", New: "2"}, + {Key: "b", Old: "x", New: "y"}, + } + result := FormatSummary("Reloaded", changes) + if !strings.Contains(result, "Reloaded (2 changes):") { + t.Fatalf("missing header: %q", result) + } + if !strings.Contains(result, "- a: 1 → 2") || !strings.Contains(result, "- b: x → y") { + t.Fatalf("missing details: %q", result) + } +} + +func TestStringifyValue_BoolAndFloat(t *testing.T) { + // Exercise the bool and float branches via Diff on App fields. + temp1 := 0.5 + temp2 := 0.9 + oldCfg := appconfig.App{CodingTemperature: &temp1} + newCfg := appconfig.App{CodingTemperature: &temp2} + changes := Diff(oldCfg, newCfg) + found := false + for _, ch := range changes { + if ch.Key == "coding_temperature" { + found = true + if ch.Old != "0.5" || ch.New != "0.9" { + t.Fatalf("unexpected values: %+v", ch) + } + } + } + if !found { + t.Fatalf("expected coding_temperature change, got %+v", changes) + } +} + +func TestStringifyValue_NilPointer(t *testing.T) { + // nil *float64 should produce "(unset)". + oldCfg := appconfig.App{} + temp := 0.3 + newCfg := appconfig.App{CodingTemperature: &temp} + changes := Diff(oldCfg, newCfg) + found := false + for _, ch := range changes { + if ch.Key == "coding_temperature" { + found = true + if ch.Old != "(unset)" { + t.Fatalf("expected (unset) for nil ptr, got %q", ch.Old) + } + } + } + if !found { + t.Fatalf("expected coding_temperature change") + } +} + +func TestStringifyValue_NilBoolPointer(t *testing.T) { + // CompletionWaitAll is *bool; nil should produce "(unset)". + b := true + oldCfg := appconfig.App{} + newCfg := appconfig.App{CompletionWaitAll: &b} + changes := Diff(oldCfg, newCfg) + found := false + for _, ch := range changes { + if ch.Key == "completion_wait_all" { + found = true + if ch.Old != "(unset)" || ch.New != "true" { + t.Fatalf("unexpected: old=%q new=%q", ch.Old, ch.New) + } + } + } + if !found { + t.Fatalf("expected completion_wait_all change") + } +} + +func TestStringifyValue_StringSlice(t *testing.T) { + // TriggerCharacters is []string; exercise the string-slice branch. + oldCfg := appconfig.App{TriggerCharacters: []string{".", ":"}} + newCfg := appconfig.App{TriggerCharacters: []string{".", ":", "("}} + changes := Diff(oldCfg, newCfg) + found := false + for _, ch := range changes { + if ch.Key == "trigger_characters" { + found = true + if ch.Old != ".,::" || ch.New != ".,:,(" { + // Join uses comma separator. + if ch.Old != ".,:" || ch.New != ".,:,(" { + t.Fatalf("unexpected: old=%q new=%q", ch.Old, ch.New) + } + } + } + } + if !found { + t.Fatalf("expected trigger_characters change, got %+v", changes) + } +} + +func TestStringifyValue_NilSlice(t *testing.T) { + // nil slice vs non-nil slice. + oldCfg := appconfig.App{} + newCfg := appconfig.App{TriggerCharacters: []string{"x"}} + changes := Diff(oldCfg, newCfg) + found := false + for _, ch := range changes { + if ch.Key == "trigger_characters" { + found = true + if ch.Old != "" { + t.Fatalf("expected empty for nil slice, got %q", ch.Old) + } + } + } + if !found { + t.Fatalf("expected trigger_characters change") + } +} + +func TestStringifyValue_SurfaceConfigWithTemperature(t *testing.T) { + // Exercise the SurfaceConfig temperature branch. + temp := 0.750 + oldCfg := appconfig.App{ + CompletionConfigs: []appconfig.SurfaceConfig{ + {Provider: "openai", Model: "gpt-4o", Temperature: &temp}, + }, + } + newCfg := appconfig.App{ + CompletionConfigs: []appconfig.SurfaceConfig{ + {Provider: "openai", Model: "gpt-4o"}, + }, + } + changes := Diff(oldCfg, newCfg) + found := false + for _, ch := range changes { + if ch.Key == "completion_configs" { + found = true + if !strings.Contains(ch.Old, "@0.750") { + t.Fatalf("expected temperature in old value, got %q", ch.Old) + } + } + } + if !found { + t.Fatalf("expected completion_configs change") + } +} + +func TestStringifyValue_SurfaceConfigEmptyProvider(t *testing.T) { + // Exercise the SurfaceConfig branch where provider is empty. + oldCfg := appconfig.App{ + ChatConfigs: []appconfig.SurfaceConfig{ + {Provider: "", Model: "some-model"}, + }, + } + newCfg := appconfig.App{} + changes := Diff(oldCfg, newCfg) + found := false + for _, ch := range changes { + if ch.Key == "chat_configs" { + found = true + if ch.Old != "some-model" { + t.Fatalf("expected 'some-model', got %q", ch.Old) + } + } + } + if !found { + t.Fatalf("expected chat_configs change") + } +} + func TestDiff_SurfaceModel(t *testing.T) { oldCfg := appconfig.App{CompletionConfigs: []appconfig.SurfaceConfig{{Provider: "openai", Model: "gpt-4o"}}} newCfg := appconfig.App{CompletionConfigs: []appconfig.SurfaceConfig{{Provider: "anthropic", Model: "claude-3-5-sonnet"}}} |
