detached 035c4f0

fix(config): make runtime provider read current config

commit
035c4f0528c2afa067654d01b74a5d529e94c4c2
author
brice · · 2026-06-23 14:25 UTC
committer
brice · · 2026-06-23 20:01 UTC
parent
75ca6b4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +106 -19
M internal/bootstrap/runtime_config.go +14 -15
@@ -1,35 +1,37 @@
11 package bootstrap
22
33 import (
4- "sync"
5-
64 "github.com/bnema/dumber/internal/application/port"
75 "github.com/bnema/dumber/internal/domain/entity"
86 "github.com/bnema/dumber/internal/infrastructure/config"
97 )
108
119 type configChangeManager interface {
10+ Get() *config.Config
1211 Watch() error
1312 OnConfigChange(func(*config.Config))
1413 }
1514
1615 type runtimeConfigProvider struct {
17- mu sync.RWMutex
18- current port.RuntimeConfigSnapshot
19- manager configChangeManager
16+ fallback port.RuntimeConfigSnapshot
17+ manager configChangeManager
2018 }
2119
2220 func NewRuntimeConfigProvider(cfg *config.Config, manager configChangeManager) port.RuntimeConfigProvider {
2321 return &runtimeConfigProvider{
24- current: RuntimeConfigSnapshotFromConfig(cfg),
25- manager: manager,
22+ fallback: RuntimeConfigSnapshotFromConfig(cfg),
23+ manager: manager,
2624 }
2725 }
2826
2927 func (p *runtimeConfigProvider) Current() port.RuntimeConfigSnapshot {
30- p.mu.RLock()
31- defer p.mu.RUnlock()
32- return cloneRuntimeConfigSnapshot(p.current)
28+ if p == nil {
29+ return port.RuntimeConfigSnapshot{}
30+ }
31+ if p.manager == nil {
32+ return cloneRuntimeConfigSnapshot(p.fallback)
33+ }
34+ return cloneRuntimeConfigSnapshot(RuntimeConfigSnapshotFromConfig(p.manager.Get()))
3335 }
3436
3537 func (p *runtimeConfigProvider) Watch() error {
@@ -45,9 +47,6 @@ func (p *runtimeConfigProvider) OnChange(callback func(port.RuntimeConfigSnapsho
4547 }
4648 p.manager.OnConfigChange(func(cfg *config.Config) {
4749 snapshot := RuntimeConfigSnapshotFromConfig(cfg)
48- p.mu.Lock()
49- p.current = snapshot
50- p.mu.Unlock()
5150 if callback != nil {
5251 callback(cloneRuntimeConfigSnapshot(snapshot))
5352 }
@@ -83,8 +82,8 @@ func RuntimeConfigSnapshotFromConfig(cfg *config.Config) port.RuntimeConfigSnaps
8382 DefaultUIScale: cfg.DefaultUIScale,
8483 SidebarWidth: cfg.SidebarWidth,
8584 Appearance: cfg.Appearance,
86- Workspace: cfg.Workspace,
87- Session: cfg.Session,
85+ Workspace: cloneWorkspaceConfig(cfg.Workspace),
86+ Session: cloneSessionConfig(cfg.Session),
8887 Clipboard: port.RuntimeClipboardConfig{AutoCopyOnSelection: cfg.Clipboard.AutoCopyOnSelection},
8988 SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),
9089 DefaultSearchEngine: cfg.DefaultSearchEngine,
@@ -1,35 +1,37 @@
1 package bootstrap1 package bootstrap
2 2
3 import (3 import (
4- "sync"
5-
6 "github.com/bnema/dumber/internal/application/port"4 "github.com/bnema/dumber/internal/application/port"
7 "github.com/bnema/dumber/internal/domain/entity"5 "github.com/bnema/dumber/internal/domain/entity"
8 "github.com/bnema/dumber/internal/infrastructure/config"6 "github.com/bnema/dumber/internal/infrastructure/config"
9 )7 )
10 8
11 type configChangeManager interface {9 type configChangeManager interface {
10+ Get() *config.Config
12 Watch() error11 Watch() error
13 OnConfigChange(func(*config.Config))12 OnConfigChange(func(*config.Config))
14 }13 }
15 14
16 type runtimeConfigProvider struct {15 type runtimeConfigProvider struct {
17- mu sync.RWMutex16+ fallback port.RuntimeConfigSnapshot
18- current port.RuntimeConfigSnapshot17+ manager configChangeManager
19- manager configChangeManager
20 }18 }
21 19
22 func NewRuntimeConfigProvider(cfg *config.Config, manager configChangeManager) port.RuntimeConfigProvider {20 func NewRuntimeConfigProvider(cfg *config.Config, manager configChangeManager) port.RuntimeConfigProvider {
23 return &runtimeConfigProvider{21 return &runtimeConfigProvider{
24- current: RuntimeConfigSnapshotFromConfig(cfg),22+ fallback: RuntimeConfigSnapshotFromConfig(cfg),
25- manager: manager,23+ manager: manager,
26 }24 }
27 }25 }
28 26
29 func (p *runtimeConfigProvider) Current() port.RuntimeConfigSnapshot {27 func (p *runtimeConfigProvider) Current() port.RuntimeConfigSnapshot {
30- p.mu.RLock()28+ if p == nil {
31- defer p.mu.RUnlock()29+ return port.RuntimeConfigSnapshot{}
32- return cloneRuntimeConfigSnapshot(p.current)30+ }
31+ if p.manager == nil {
32+ return cloneRuntimeConfigSnapshot(p.fallback)
33+ }
34+ return cloneRuntimeConfigSnapshot(RuntimeConfigSnapshotFromConfig(p.manager.Get()))
33 }35 }
34 36
35 func (p *runtimeConfigProvider) Watch() error {37 func (p *runtimeConfigProvider) Watch() error {
@@ -45,9 +47,6 @@ func (p *runtimeConfigProvider) OnChange(callback func(port.RuntimeConfigSnapsho
45 }47 }
46 p.manager.OnConfigChange(func(cfg *config.Config) {48 p.manager.OnConfigChange(func(cfg *config.Config) {
47 snapshot := RuntimeConfigSnapshotFromConfig(cfg)49 snapshot := RuntimeConfigSnapshotFromConfig(cfg)
48- p.mu.Lock()
49- p.current = snapshot
50- p.mu.Unlock()
51 if callback != nil {50 if callback != nil {
52 callback(cloneRuntimeConfigSnapshot(snapshot))51 callback(cloneRuntimeConfigSnapshot(snapshot))
53 }52 }
@@ -83,8 +82,8 @@ func RuntimeConfigSnapshotFromConfig(cfg *config.Config) port.RuntimeConfigSnaps
83 DefaultUIScale: cfg.DefaultUIScale,82 DefaultUIScale: cfg.DefaultUIScale,
84 SidebarWidth: cfg.SidebarWidth,83 SidebarWidth: cfg.SidebarWidth,
85 Appearance: cfg.Appearance,84 Appearance: cfg.Appearance,
86- Workspace: cfg.Workspace,85+ Workspace: cloneWorkspaceConfig(cfg.Workspace),
87- Session: cfg.Session,86+ Session: cloneSessionConfig(cfg.Session),
88 Clipboard: port.RuntimeClipboardConfig{AutoCopyOnSelection: cfg.Clipboard.AutoCopyOnSelection},87 Clipboard: port.RuntimeClipboardConfig{AutoCopyOnSelection: cfg.Clipboard.AutoCopyOnSelection},
89 SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),88 SearchShortcuts: runtimeSearchShortcutsFromConfig(cfg.SearchShortcuts),
90 DefaultSearchEngine: cfg.DefaultSearchEngine,89 DefaultSearchEngine: cfg.DefaultSearchEngine,
M internal/bootstrap/runtime_config_test.go +92 -4
@@ -10,6 +10,7 @@ import (
1010 )
1111
1212 type fakeConfigManager struct {
13+ current *config.Config
1314 watchCalls int
1415 callbacks []func(*config.Config)
1516 }
@@ -23,6 +24,17 @@ func (f *fakeConfigManager) OnConfigChange(callback func(*config.Config)) {
2324 f.callbacks = append(f.callbacks, callback)
2425 }
2526
27+func (f *fakeConfigManager) Get() *config.Config {
28+ return f.current
29+}
30+
31+func (f *fakeConfigManager) emit(next *config.Config) {
32+ f.current = next
33+ for _, callback := range f.callbacks {
34+ callback(next)
35+ }
36+}
37+
2638 func TestEngineSettingsPayloadFromConfigMapsRuntimeFields(t *testing.T) {
2739 cfg := config.DefaultConfig()
2840 cfg.DefaultUIScale = 1.25
@@ -69,7 +81,7 @@ func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
6981
7082 next := config.DefaultConfig()
7183 next.DefaultUIScale = 1.8
72- manager.callbacks[0](next)
84+ manager.emit(next)
7385
7486 if got := provider.Current().UI.DefaultUIScale; got != 1.8 {
7587 t.Fatalf("Current DefaultUIScale=%v, want 1.8", got)
@@ -79,6 +91,46 @@ func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
7991 }
8092 }
8193
94+func TestRuntimeConfigProviderCurrentReflectsManagerGetWithoutSubscription(t *testing.T) {
95+ initial := config.DefaultConfig()
96+ initial.DefaultUIScale = 1
97+ manager := &fakeConfigManager{current: initial}
98+ provider := NewRuntimeConfigProvider(initial, manager)
99+
100+ next := config.DefaultConfig()
101+ next.DefaultUIScale = 1.8
102+ manager.current = next
103+
104+ if got := provider.Current().UI.DefaultUIScale; got != 1.8 {
105+ t.Fatalf("Current DefaultUIScale=%v, want 1.8", got)
106+ }
107+}
108+
109+func TestRuntimeConfigProviderOnChangeCallbackCanCallCurrent(t *testing.T) {
110+ initial := config.DefaultConfig()
111+ initial.DefaultUIScale = 1
112+ manager := &fakeConfigManager{current: initial}
113+ provider := NewRuntimeConfigProvider(initial, manager)
114+
115+ completed := false
116+ var seen port.RuntimeConfigSnapshot
117+ provider.OnChange(func(port.RuntimeConfigSnapshot) {
118+ seen = provider.Current()
119+ completed = true
120+ })
121+
122+ next := config.DefaultConfig()
123+ next.DefaultUIScale = 1.8
124+ manager.emit(next)
125+
126+ if !completed {
127+ t.Fatal("callback did not complete")
128+ }
129+ if seen.UI.DefaultUIScale != 1.8 {
130+ t.Fatalf("callback Current DefaultUIScale=%v, want 1.8", seen.UI.DefaultUIScale)
131+ }
132+}
133+
82134 func TestRuntimeConfigProviderWatchDelegatesToManager(t *testing.T) {
83135 manager := &fakeConfigManager{}
84136 provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)
@@ -96,7 +148,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
96148 cfg.SearchShortcuts = map[string]config.SearchShortcut{
97149 "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},
98150 }
99- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{})
151+ provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})
100152
101153 first := provider.Current()
102154 first.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}
@@ -109,7 +161,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
109161
110162 func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {
111163 cfg := runtimeConfigWithNestedMutableFields()
112- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{})
164+ provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})
113165
114166 first := provider.Current()
115167 mutateNestedRuntimeConfigSnapshot(first)
@@ -126,7 +178,7 @@ func TestRuntimeConfigProviderOnChangePassesNestedConfigClone(t *testing.T) {
126178 mutateNestedRuntimeConfigSnapshot(snapshot)
127179 })
128180
129- manager.callbacks[0](runtimeConfigWithNestedMutableFields())
181+ manager.emit(runtimeConfigWithNestedMutableFields())
130182
131183 assertNestedRuntimeConfigSnapshotUnchanged(t, provider.Current())
132184 }
@@ -236,6 +288,33 @@ func assertMapEntryAbsent(t *testing.T, actions map[string]entity.ActionBinding,
236288 }
237289 }
238290
291+func assertNestedSourceConfigUnchanged(t *testing.T, cfg *config.Config) {
292+ t.Helper()
293+
294+ assertActionBinding(t, cfg.Workspace.PaneMode.Actions, "split-right", "r")
295+ assertMapEntryAbsent(t, cfg.Workspace.PaneMode.Actions, "added-pane")
296+
297+ assertActionBinding(t, cfg.Workspace.TabMode.Actions, "next-tab", "l")
298+ assertMapEntryAbsent(t, cfg.Workspace.TabMode.Actions, "added-tab")
299+
300+ assertActionBinding(t, cfg.Workspace.ResizeMode.Actions, "grow-right", "right")
301+ assertMapEntryAbsent(t, cfg.Workspace.ResizeMode.Actions, "added-resize")
302+
303+ assertActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-history-systemview", "ctrl+h")
304+ assertMapEntryAbsent(t, cfg.Workspace.Shortcuts.Actions, "added-shortcut")
305+
306+ profile := cfg.Workspace.FloatingPane.Profiles["docs"]
307+ if got := profile.Keys[0]; got != "alt+d" {
308+ t.Fatalf("floating profile key=%q, want %q", got, "alt+d")
309+ }
310+ if _, ok := cfg.Workspace.FloatingPane.Profiles["added-floating"]; ok {
311+ t.Fatal("floating pane profiles must not include entries added through a returned snapshot")
312+ }
313+
314+ assertActionBinding(t, cfg.Session.SessionMode.Actions, "session-manager", "s")
315+ assertMapEntryAbsent(t, cfg.Session.SessionMode.Actions, "added-session")
316+}
317+
239318 func TestRuntimeConfigSnapshotFromConfigMapsUIRuntimeFields(t *testing.T) {
240319 cfg := config.DefaultConfig()
241320 cfg.DefaultUIScale = 1.4
@@ -276,6 +355,15 @@ func TestRuntimeConfigSnapshotFromConfigMapsUIRuntimeFields(t *testing.T) {
276355 }
277356 }
278357
358+func TestRuntimeConfigSnapshotFromConfigDetachesNestedMutableFields(t *testing.T) {
359+ cfg := runtimeConfigWithNestedMutableFields()
360+ snapshot := RuntimeConfigSnapshotFromConfig(cfg)
361+
362+ mutateNestedRuntimeConfigSnapshot(snapshot)
363+
364+ assertNestedSourceConfigUnchanged(t, cfg)
365+}
366+
279367 func TestRuntimeConfigSnapshotFromNilConfigReturnsZeroSnapshot(t *testing.T) {
280368 got := RuntimeConfigSnapshotFromConfig(nil)
281369 if !reflect.DeepEqual(got, port.RuntimeConfigSnapshot{}) {
@@ -10,6 +10,7 @@ import (
10 )10 )
11 11
12 type fakeConfigManager struct {12 type fakeConfigManager struct {
13+ current *config.Config
13 watchCalls int14 watchCalls int
14 callbacks []func(*config.Config)15 callbacks []func(*config.Config)
15 }16 }
@@ -23,6 +24,17 @@ func (f *fakeConfigManager) OnConfigChange(callback func(*config.Config)) {
23 f.callbacks = append(f.callbacks, callback)24 f.callbacks = append(f.callbacks, callback)
24 }25 }
25 26
27+func (f *fakeConfigManager) Get() *config.Config {
28+ return f.current
29+}
30+
31+func (f *fakeConfigManager) emit(next *config.Config) {
32+ f.current = next
33+ for _, callback := range f.callbacks {
34+ callback(next)
35+ }
36+}
37+
26 func TestEngineSettingsPayloadFromConfigMapsRuntimeFields(t *testing.T) {38 func TestEngineSettingsPayloadFromConfigMapsRuntimeFields(t *testing.T) {
27 cfg := config.DefaultConfig()39 cfg := config.DefaultConfig()
28 cfg.DefaultUIScale = 1.2540 cfg.DefaultUIScale = 1.25
@@ -69,7 +81,7 @@ func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
69 81
70 next := config.DefaultConfig()82 next := config.DefaultConfig()
71 next.DefaultUIScale = 1.883 next.DefaultUIScale = 1.8
72- manager.callbacks[0](next)84+ manager.emit(next)
73 85
74 if got := provider.Current().UI.DefaultUIScale; got != 1.8 {86 if got := provider.Current().UI.DefaultUIScale; got != 1.8 {
75 t.Fatalf("Current DefaultUIScale=%v, want 1.8", got)87 t.Fatalf("Current DefaultUIScale=%v, want 1.8", got)
@@ -79,6 +91,46 @@ func TestRuntimeConfigProviderUpdatesSnapshotBeforeCallback(t *testing.T) {
79 }91 }
80 }92 }
81 93
94+func TestRuntimeConfigProviderCurrentReflectsManagerGetWithoutSubscription(t *testing.T) {
95+ initial := config.DefaultConfig()
96+ initial.DefaultUIScale = 1
97+ manager := &fakeConfigManager{current: initial}
98+ provider := NewRuntimeConfigProvider(initial, manager)
99+
100+ next := config.DefaultConfig()
101+ next.DefaultUIScale = 1.8
102+ manager.current = next
103+
104+ if got := provider.Current().UI.DefaultUIScale; got != 1.8 {
105+ t.Fatalf("Current DefaultUIScale=%v, want 1.8", got)
106+ }
107+}
108+
109+func TestRuntimeConfigProviderOnChangeCallbackCanCallCurrent(t *testing.T) {
110+ initial := config.DefaultConfig()
111+ initial.DefaultUIScale = 1
112+ manager := &fakeConfigManager{current: initial}
113+ provider := NewRuntimeConfigProvider(initial, manager)
114+
115+ completed := false
116+ var seen port.RuntimeConfigSnapshot
117+ provider.OnChange(func(port.RuntimeConfigSnapshot) {
118+ seen = provider.Current()
119+ completed = true
120+ })
121+
122+ next := config.DefaultConfig()
123+ next.DefaultUIScale = 1.8
124+ manager.emit(next)
125+
126+ if !completed {
127+ t.Fatal("callback did not complete")
128+ }
129+ if seen.UI.DefaultUIScale != 1.8 {
130+ t.Fatalf("callback Current DefaultUIScale=%v, want 1.8", seen.UI.DefaultUIScale)
131+ }
132+}
133+
82 func TestRuntimeConfigProviderWatchDelegatesToManager(t *testing.T) {134 func TestRuntimeConfigProviderWatchDelegatesToManager(t *testing.T) {
83 manager := &fakeConfigManager{}135 manager := &fakeConfigManager{}
84 provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)136 provider := NewRuntimeConfigProvider(config.DefaultConfig(), manager)
@@ -96,7 +148,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
96 cfg.SearchShortcuts = map[string]config.SearchShortcut{148 cfg.SearchShortcuts = map[string]config.SearchShortcut{
97 "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},149 "gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},
98 }150 }
99- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{})151+ provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})
100 152
101 first := provider.Current()153 first := provider.Current()
102 first.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}154 first.UI.SearchShortcuts["gh"] = port.RuntimeSearchShortcut{URL: "mutated"}
@@ -109,7 +161,7 @@ func TestRuntimeConfigProviderCurrentReturnsMapClone(t *testing.T) {
109 161
110 func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {162 func TestRuntimeConfigProviderCurrentReturnsNestedConfigClone(t *testing.T) {
111 cfg := runtimeConfigWithNestedMutableFields()163 cfg := runtimeConfigWithNestedMutableFields()
112- provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{})164+ provider := NewRuntimeConfigProvider(cfg, &fakeConfigManager{current: cfg})
113 165
114 first := provider.Current()166 first := provider.Current()
115 mutateNestedRuntimeConfigSnapshot(first)167 mutateNestedRuntimeConfigSnapshot(first)
@@ -126,7 +178,7 @@ func TestRuntimeConfigProviderOnChangePassesNestedConfigClone(t *testing.T) {
126 mutateNestedRuntimeConfigSnapshot(snapshot)178 mutateNestedRuntimeConfigSnapshot(snapshot)
127 })179 })
128 180
129- manager.callbacks[0](runtimeConfigWithNestedMutableFields())181+ manager.emit(runtimeConfigWithNestedMutableFields())
130 182
131 assertNestedRuntimeConfigSnapshotUnchanged(t, provider.Current())183 assertNestedRuntimeConfigSnapshotUnchanged(t, provider.Current())
132 }184 }
@@ -236,6 +288,33 @@ func assertMapEntryAbsent(t *testing.T, actions map[string]entity.ActionBinding,
236 }288 }
237 }289 }
238 290
291+func assertNestedSourceConfigUnchanged(t *testing.T, cfg *config.Config) {
292+ t.Helper()
293+
294+ assertActionBinding(t, cfg.Workspace.PaneMode.Actions, "split-right", "r")
295+ assertMapEntryAbsent(t, cfg.Workspace.PaneMode.Actions, "added-pane")
296+
297+ assertActionBinding(t, cfg.Workspace.TabMode.Actions, "next-tab", "l")
298+ assertMapEntryAbsent(t, cfg.Workspace.TabMode.Actions, "added-tab")
299+
300+ assertActionBinding(t, cfg.Workspace.ResizeMode.Actions, "grow-right", "right")
301+ assertMapEntryAbsent(t, cfg.Workspace.ResizeMode.Actions, "added-resize")
302+
303+ assertActionBinding(t, cfg.Workspace.Shortcuts.Actions, "toggle-history-systemview", "ctrl+h")
304+ assertMapEntryAbsent(t, cfg.Workspace.Shortcuts.Actions, "added-shortcut")
305+
306+ profile := cfg.Workspace.FloatingPane.Profiles["docs"]
307+ if got := profile.Keys[0]; got != "alt+d" {
308+ t.Fatalf("floating profile key=%q, want %q", got, "alt+d")
309+ }
310+ if _, ok := cfg.Workspace.FloatingPane.Profiles["added-floating"]; ok {
311+ t.Fatal("floating pane profiles must not include entries added through a returned snapshot")
312+ }
313+
314+ assertActionBinding(t, cfg.Session.SessionMode.Actions, "session-manager", "s")
315+ assertMapEntryAbsent(t, cfg.Session.SessionMode.Actions, "added-session")
316+}
317+
239 func TestRuntimeConfigSnapshotFromConfigMapsUIRuntimeFields(t *testing.T) {318 func TestRuntimeConfigSnapshotFromConfigMapsUIRuntimeFields(t *testing.T) {
240 cfg := config.DefaultConfig()319 cfg := config.DefaultConfig()
241 cfg.DefaultUIScale = 1.4320 cfg.DefaultUIScale = 1.4
@@ -276,6 +355,15 @@ func TestRuntimeConfigSnapshotFromConfigMapsUIRuntimeFields(t *testing.T) {
276 }355 }
277 }356 }
278 357
358+func TestRuntimeConfigSnapshotFromConfigDetachesNestedMutableFields(t *testing.T) {
359+ cfg := runtimeConfigWithNestedMutableFields()
360+ snapshot := RuntimeConfigSnapshotFromConfig(cfg)
361+
362+ mutateNestedRuntimeConfigSnapshot(snapshot)
363+
364+ assertNestedSourceConfigUnchanged(t, cfg)
365+}
366+
279 func TestRuntimeConfigSnapshotFromNilConfigReturnsZeroSnapshot(t *testing.T) {367 func TestRuntimeConfigSnapshotFromNilConfigReturnsZeroSnapshot(t *testing.T) {
280 got := RuntimeConfigSnapshotFromConfig(nil)368 got := RuntimeConfigSnapshotFromConfig(nil)
281 if !reflect.DeepEqual(got, port.RuntimeConfigSnapshot{}) {369 if !reflect.DeepEqual(got, port.RuntimeConfigSnapshot{}) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close