mode_legend_preferences_gateway_test.go RawHistoryFolder
1package config
2
3import (
4 "context"
5 "sync/atomic"
6 "testing"
7
8 "github.com/bnema/dumber/internal/domain/entity"
9
10 "github.com/stretchr/testify/require"
11)
12
13func TestModeLegendPreferencesGateway_SaveModeLegendCollapsed_Persists(t *testing.T) {
14 configHome := t.TempDir()
15 mgr := newTestOmniboxPreferencesGatewayManager(t, configHome)
16 gateway := NewModeLegendPreferencesGateway(mgr)
17 require.False(t, mgr.Get().Workspace.Styling.ModeLegendCollapsed)
18
19 require.NoError(t, gateway.SaveModeLegendCollapsed(context.Background(), true))
20 require.True(t, newTestOmniboxPreferencesGatewayManager(t, configHome).Get().Workspace.Styling.ModeLegendCollapsed)
21
22 require.NoError(t, gateway.SaveModeLegendCollapsed(context.Background(), false))
23 require.False(t, newTestOmniboxPreferencesGatewayManager(t, configHome).Get().Workspace.Styling.ModeLegendCollapsed)
24}
25
26func TestModeLegendPreferencesGateway_NilManagerReturnsError(t *testing.T) {
27 require.Error(t, NewModeLegendPreferencesGateway(nil).SaveModeLegendCollapsed(context.Background(), true))
28}
29
30// newWatchedTestManager mirrors production, where the manager is in watching mode
31// so Save() publishes the in-memory config directly instead of re-reading the
32// file. No fsnotify watcher is started, which keeps the tests deterministic.
33func newWatchedTestManager(t *testing.T, configHome string) *Manager {
34 t.Helper()
35
36 mgr := newTestOmniboxPreferencesGatewayManager(t, configHome)
37 mgr.watching = true
38
39 return mgr
40}
41
42// TestManagerUpdate_UpdateAfterStaleSnapshotDoesNotLoseWrites forces the
43// interleaving a Get()-then-Save() implementation loses: another update
44// publishes right after the snapshot the first update works from was taken.
45// Update must apply its change on top of that update instead of overwriting it.
46func TestManagerUpdate_UpdateAfterStaleSnapshotDoesNotLoseWrites(t *testing.T) {
47 configHome := t.TempDir()
48 mgr := newWatchedTestManager(t, configHome)
49 legend := NewModeLegendPreferencesGateway(mgr)
50 omnibox := NewOmniboxPreferencesGateway(mgr)
51
52 saveOther := func() error {
53 return omnibox.SaveOmniboxInitialBehavior(context.Background(), entity.OmniboxInitialBehaviorMostVisited)
54 }
55 queued := make(chan error, 1)
56 var fired atomic.Bool
57 mgr.afterSnapshot = func() {
58 // The nested update calls this hook too; only the first snapshot reacts.
59 if fired.Swap(true) {
60 return
61 }
62 func() {
63 if mgr.mu.TryLock() {
64 // The snapshot was taken without keeping the lock: another
65 // update publishes before this one continues.
66 mgr.mu.Unlock()
67 require.NoError(t, saveOther())
68 queued <- nil
69 return
70 }
71 // The lock is held across the whole update: the other update has
72 // to queue behind it.
73 go func() { queued <- saveOther() }()
74 }()
75 }
76
77 require.NoError(t, legend.SaveModeLegendCollapsed(context.Background(), true))
78 require.NoError(t, <-queued)
79
80 for name, got := range map[string]*Config{
81 "in-memory": mgr.Get(),
82 "on-disk": newTestOmniboxPreferencesGatewayManager(t, configHome).Get(),
83 } {
84 require.True(t, got.Workspace.Styling.ModeLegendCollapsed, name)
85 require.Equal(t, entity.OmniboxInitialBehaviorMostVisited, got.Omnibox.InitialBehavior, name)
86 }
87}
88
89func TestManagerUpdate_AppliesToLatestConfigAfterSave(t *testing.T) {
90 mgr := newWatchedTestManager(t, t.TempDir())
91
92 cfg := mgr.Get()
93 cfg.Omnibox.InitialBehavior = entity.OmniboxInitialBehaviorMostVisited
94 require.NoError(t, mgr.Save(cfg))
95
96 require.NoError(t, NewModeLegendPreferencesGateway(mgr).SaveModeLegendCollapsed(context.Background(), true))
97
98 got := mgr.Get()
99 require.True(t, got.Workspace.Styling.ModeLegendCollapsed)
100 require.Equal(t, entity.OmniboxInitialBehaviorMostVisited, got.Omnibox.InitialBehavior)
101}
102
103func TestManagerUpdate_RejectsNilAndInvalidMutations(t *testing.T) {
104 mgr := newWatchedTestManager(t, t.TempDir())
105 before := mgr.Get().Workspace.Styling.ModeLegendDelayMs
106
107 require.Error(t, mgr.Update(nil))
108 require.Error(t, mgr.Update(func(cfg *Config) { cfg.Workspace.Styling.ModeLegendDelayMs = -1 }))
109 require.Equal(t, before, mgr.Get().Workspace.Styling.ModeLegendDelayMs)
110}
111
112func TestManagerUpdate_ConcurrentUpdatesAreAllPersisted(t *testing.T) {
113 configHome := t.TempDir()
114 mgr := newWatchedTestManager(t, configHome)
115
116 mutations := []func(*Config){
117 func(cfg *Config) { cfg.Workspace.Styling.ModeLegendCollapsed = true },
118 func(cfg *Config) { cfg.Omnibox.InitialBehavior = entity.OmniboxInitialBehaviorNone },
119 func(cfg *Config) { cfg.Workspace.Styling.ModeLegendDelayMs = 1234 },
120 func(cfg *Config) { cfg.Workspace.Styling.TransitionDuration = 321 },
121 func(cfg *Config) { cfg.Workspace.Styling.ModeBorderWidth = 7 },
122 func(cfg *Config) { cfg.Workspace.Styling.ModeLegendLinger = false },
123 }
124
125 start := make(chan struct{})
126 errs := make(chan error, len(mutations))
127 for _, mutate := range mutations {
128 go func() {
129 <-start
130 errs <- mgr.Update(mutate)
131 }()
132 }
133 close(start)
134 for range mutations {
135 require.NoError(t, <-errs)
136 }
137
138 for name, got := range map[string]*Config{
139 "in-memory": mgr.Get(),
140 "on-disk": newTestOmniboxPreferencesGatewayManager(t, configHome).Get(),
141 } {
142 require.True(t, got.Workspace.Styling.ModeLegendCollapsed, name)
143 require.Equal(t, entity.OmniboxInitialBehaviorNone, got.Omnibox.InitialBehavior, name)
144 require.Equal(t, 1234, got.Workspace.Styling.ModeLegendDelayMs, name)
145 require.Equal(t, 321, got.Workspace.Styling.TransitionDuration, name)
146 require.Equal(t, 7, got.Workspace.Styling.ModeBorderWidth, name)
147 require.False(t, got.Workspace.Styling.ModeLegendLinger, name)
148 }
149}