1package config
2
3import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10
11 "github.com/charmbracelet/crush/internal/csync"
12 "github.com/charmbracelet/crush/internal/oauth"
13 "github.com/stretchr/testify/require"
14)
15
16func TestConfigStore_ConfigPath_GlobalAlwaysWorks(t *testing.T) {
17 t.Parallel()
18
19 store := &ConfigStore{
20 globalDataPath: "/some/global/crush.json",
21 }
22
23 path, err := store.configPath(ScopeGlobal)
24 require.NoError(t, err)
25 require.Equal(t, "/some/global/crush.json", path)
26}
27
28func TestConfigStore_ConfigPath_WorkspaceReturnsPath(t *testing.T) {
29 t.Parallel()
30
31 store := &ConfigStore{
32 workspacePath: "/some/workspace/.crush/crush.json",
33 }
34
35 path, err := store.configPath(ScopeWorkspace)
36 require.NoError(t, err)
37 require.Equal(t, "/some/workspace/.crush/crush.json", path)
38}
39
40func TestConfigStore_ConfigPath_WorkspaceErrorsWhenEmpty(t *testing.T) {
41 t.Parallel()
42
43 store := &ConfigStore{
44 globalDataPath: "/some/global/crush.json",
45 workspacePath: "",
46 }
47
48 _, err := store.configPath(ScopeWorkspace)
49 require.Error(t, err)
50 require.True(t, errors.Is(err, ErrNoWorkspaceConfig))
51}
52
53func TestConfigStore_SetConfigField_WorkspaceScopeGuard(t *testing.T) {
54 t.Parallel()
55
56 store := &ConfigStore{
57 config: &Config{},
58 globalDataPath: filepath.Join(t.TempDir(), "global.json"),
59 workspacePath: "",
60 }
61
62 err := store.SetConfigField(ScopeWorkspace, "foo", "bar")
63 require.Error(t, err)
64 require.True(t, errors.Is(err, ErrNoWorkspaceConfig))
65}
66
67func TestConfigStore_SetConfigField_GlobalScopeAlwaysWorks(t *testing.T) {
68 t.Parallel()
69
70 dir := t.TempDir()
71 globalPath := filepath.Join(dir, "crush.json")
72 store := &ConfigStore{
73 config: &Config{},
74 globalDataPath: globalPath,
75 }
76
77 err := store.SetConfigField(ScopeGlobal, "foo", "bar")
78 require.NoError(t, err)
79
80 data, err := os.ReadFile(globalPath)
81 require.NoError(t, err)
82 require.Contains(t, string(data), `"foo"`)
83}
84
85func TestConfigStore_RemoveConfigField_WorkspaceScopeGuard(t *testing.T) {
86 t.Parallel()
87
88 store := &ConfigStore{
89 config: &Config{},
90 globalDataPath: filepath.Join(t.TempDir(), "global.json"),
91 workspacePath: "",
92 }
93
94 err := store.RemoveConfigField(ScopeWorkspace, "foo")
95 require.Error(t, err)
96 require.True(t, errors.Is(err, ErrNoWorkspaceConfig))
97}
98
99func TestConfigStore_HasConfigField_WorkspaceScopeGuard(t *testing.T) {
100 t.Parallel()
101
102 store := &ConfigStore{
103 config: &Config{},
104 globalDataPath: filepath.Join(t.TempDir(), "global.json"),
105 workspacePath: "",
106 }
107
108 has := store.HasConfigField(ScopeWorkspace, "foo")
109 require.False(t, has)
110}
111
112func TestConfigStore_RuntimeOverrides_Independent(t *testing.T) {
113 t.Parallel()
114
115 store1 := &ConfigStore{config: &Config{}}
116 store2 := &ConfigStore{config: &Config{}}
117
118 require.False(t, store1.Overrides().SkipPermissionRequests)
119 require.False(t, store2.Overrides().SkipPermissionRequests)
120
121 store1.Overrides().SkipPermissionRequests = true
122
123 require.True(t, store1.Overrides().SkipPermissionRequests)
124 require.False(t, store2.Overrides().SkipPermissionRequests)
125}
126
127func TestConfigStore_RuntimeOverrides_MutableViaPointer(t *testing.T) {
128 t.Parallel()
129
130 store := &ConfigStore{config: &Config{}}
131 overrides := store.Overrides()
132
133 require.False(t, overrides.SkipPermissionRequests)
134
135 overrides.SkipPermissionRequests = true
136 require.True(t, store.Overrides().SkipPermissionRequests)
137}
138
139func TestGlobalWorkspaceDir(t *testing.T) {
140 dir := t.TempDir()
141 t.Setenv("CRUSH_GLOBAL_DATA", dir)
142
143 wsDir := GlobalWorkspaceDir()
144 globalData := GlobalConfigData()
145
146 require.Equal(t, filepath.Dir(globalData), wsDir)
147 require.Equal(t, dir, wsDir)
148}
149
150func TestScope_String(t *testing.T) {
151 t.Parallel()
152
153 require.Equal(t, "global", ScopeGlobal.String())
154 require.Equal(t, "workspace", ScopeWorkspace.String())
155 require.Contains(t, Scope(99).String(), "Scope(99)")
156}
157
158func TestConfigStaleness_CleanImmediatelyAfterSnapshot(t *testing.T) {
159 t.Parallel()
160
161 dir := t.TempDir()
162 configPath := filepath.Join(dir, "crush.json")
163
164 // Create a config file
165 content := []byte(`{"options": {"debug": true}}`)
166 require.NoError(t, os.WriteFile(configPath, content, 0o600))
167
168 store := &ConfigStore{
169 config: &Config{},
170 globalDataPath: configPath,
171 }
172 store.captureStalenessSnapshot([]string{configPath})
173
174 result := store.ConfigStaleness()
175 require.False(t, result.Dirty)
176 require.Empty(t, result.Changed)
177 require.Empty(t, result.Missing)
178}
179
180func TestConfigStaleness_DetectsFileContentChange(t *testing.T) {
181 t.Parallel()
182
183 dir := t.TempDir()
184 configPath := filepath.Join(dir, "crush.json")
185
186 // Create initial config file
187 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": false}`), 0o600))
188
189 store := &ConfigStore{
190 config: &Config{},
191 globalDataPath: configPath,
192 }
193 store.captureStalenessSnapshot([]string{configPath})
194
195 // Modify the file
196 time.Sleep(10 * time.Millisecond) // Ensure different mtime
197 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": true}`), 0o600))
198
199 result := store.ConfigStaleness()
200 require.True(t, result.Dirty)
201 require.Contains(t, result.Changed, configPath)
202 require.Empty(t, result.Missing)
203}
204
205func TestConfigStaleness_DetectsFileDeletion(t *testing.T) {
206 t.Parallel()
207
208 dir := t.TempDir()
209 configPath := filepath.Join(dir, "crush.json")
210
211 // Create initial config file
212 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": true}`), 0o600))
213
214 store := &ConfigStore{
215 config: &Config{},
216 globalDataPath: configPath,
217 }
218 store.captureStalenessSnapshot([]string{configPath})
219
220 // Delete the file
221 require.NoError(t, os.Remove(configPath))
222
223 result := store.ConfigStaleness()
224 require.True(t, result.Dirty)
225 require.Empty(t, result.Changed)
226 require.Contains(t, result.Missing, configPath)
227}
228
229func TestConfigStaleness_DetectsNewFile(t *testing.T) {
230 t.Parallel()
231
232 dir := t.TempDir()
233 configPath := filepath.Join(dir, "crush.json")
234
235 // Don't create file initially
236 store := &ConfigStore{
237 config: &Config{},
238 globalDataPath: configPath,
239 }
240 store.captureStalenessSnapshot([]string{configPath})
241
242 // Now create the file
243 time.Sleep(10 * time.Millisecond)
244 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": true}`), 0o600))
245
246 result := store.ConfigStaleness()
247 require.True(t, result.Dirty)
248 require.Contains(t, result.Changed, configPath)
249 require.Empty(t, result.Missing)
250}
251
252func TestConfigStaleness_SortedOutput(t *testing.T) {
253 t.Parallel()
254
255 dir := t.TempDir()
256 pathA := filepath.Join(dir, "a.json")
257 pathB := filepath.Join(dir, "b.json")
258 pathC := filepath.Join(dir, "c.json")
259
260 // Create all files
261 for _, p := range []string{pathA, pathB, pathC} {
262 require.NoError(t, os.WriteFile(p, []byte(`{}`), 0o600))
263 }
264
265 store := &ConfigStore{
266 config: &Config{},
267 globalDataPath: pathA,
268 }
269 // Add in reverse order to test sorting
270 store.captureStalenessSnapshot([]string{pathC, pathA, pathB})
271
272 // Modify all files
273 time.Sleep(10 * time.Millisecond)
274 for _, p := range []string{pathA, pathB, pathC} {
275 require.NoError(t, os.WriteFile(p, []byte(`{"changed": true}`), 0o600))
276 }
277
278 result := store.ConfigStaleness()
279 require.True(t, result.Dirty)
280 // Should be sorted alphabetically
281 require.Equal(t, []string{pathA, pathB, pathC}, result.Changed)
282}
283
284func TestConfigStaleness_RefreshClearsDirtyState(t *testing.T) {
285 t.Parallel()
286
287 dir := t.TempDir()
288 configPath := filepath.Join(dir, "crush.json")
289
290 // Create initial config file
291 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": false}`), 0o600))
292
293 store := &ConfigStore{
294 config: &Config{},
295 globalDataPath: configPath,
296 }
297 store.captureStalenessSnapshot([]string{configPath})
298
299 // Modify the file
300 time.Sleep(10 * time.Millisecond)
301 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": true}`), 0o600))
302
303 // Verify dirty
304 result := store.ConfigStaleness()
305 require.True(t, result.Dirty)
306
307 // Refresh snapshot
308 require.NoError(t, store.RefreshStalenessSnapshot())
309
310 // Verify clean now
311 result = store.ConfigStaleness()
312 require.False(t, result.Dirty)
313 require.Empty(t, result.Changed)
314 require.Empty(t, result.Missing)
315}
316
317// TestReloadFromDisk_UsesNewConfigValues is a regression test ensuring that
318// ReloadFromDisk updates store state BEFORE running model/agent setup,
319// so the new config values are used rather than stale pre-reload values.
320func TestReloadFromDisk_UsesNewConfigValues(t *testing.T) {
321 t.Parallel()
322
323 dir := t.TempDir()
324 configPath := filepath.Join(dir, "crush.json")
325
326 // Create initial config with one model preference
327 initialConfig := `{
328 "models": {
329 "large": {"provider": "openai", "model": "gpt-4"}
330 },
331 "providers": {
332 "openai": {
333 "api_key": "test-key",
334 "models": [{"id": "gpt-4", "name": "GPT-4"}]
335 }
336 }
337 }`
338 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
339
340 // Load initial config properly
341 store, err := Load(dir, dir, false)
342 require.NoError(t, err)
343
344 // Set globalDataPath for the test (Load doesn't set this directly)
345 store.globalDataPath = configPath
346 store.CaptureStalenessSnapshot([]string{configPath})
347
348 // Verify initial model
349 require.Equal(t, "openai", store.config.Models[SelectedModelTypeLarge].Provider)
350 require.Equal(t, "gpt-4", store.config.Models[SelectedModelTypeLarge].Model)
351
352 // Modify config on disk to change model
353 updatedConfig := `{
354 "models": {
355 "large": {"provider": "anthropic", "model": "claude-3"}
356 },
357 "providers": {
358 "openai": {
359 "api_key": "test-key",
360 "models": [{"id": "gpt-4", "name": "GPT-4"}]
361 },
362 "anthropic": {
363 "api_key": "test-key-2",
364 "models": [{"id": "claude-3", "name": "Claude 3"}]
365 }
366 }
367 }`
368 time.Sleep(10 * time.Millisecond)
369 require.NoError(t, os.WriteFile(configPath, []byte(updatedConfig), 0o600))
370
371 // Reload from disk
372 ctx := context.Background()
373 err = store.ReloadFromDisk(ctx)
374 require.NoError(t, err)
375
376 // Verify the NEW config values are now in effect (regression check)
377 require.Equal(t, "anthropic", store.config.Models[SelectedModelTypeLarge].Provider)
378 require.Equal(t, "claude-3", store.config.Models[SelectedModelTypeLarge].Model)
379}
380
381// TestSetConfigField_AutoReloads verifies that SetConfigField automatically
382// reloads config into memory after writing, so subsequent reads see the new value.
383func TestSetConfigField_AutoReloads(t *testing.T) {
384 t.Parallel()
385
386 dir := t.TempDir()
387 configPath := filepath.Join(dir, "crush.json")
388
389 // Create initial config file with debug = false
390 initialConfig := `{"options": {"debug": false}}`
391 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
392
393 // Load initial config
394 store, err := Load(dir, dir, false)
395 require.NoError(t, err)
396
397 // Verify initial state
398 require.False(t, store.config.Options.Debug)
399
400 // Set globalDataPath and capture snapshot for staleness tracking
401 store.globalDataPath = configPath
402 store.CaptureStalenessSnapshot([]string{configPath})
403
404 // Use SetConfigField to change debug to true
405 err = store.SetConfigField(ScopeGlobal, "options.debug", true)
406 require.NoError(t, err)
407
408 // Verify in-memory state was automatically reloaded and reflects the change
409 require.True(t, store.config.Options.Debug, "Expected config to auto-reload and show debug = true")
410
411 // Verify staleness is clean after the reload
412 staleness := store.ConfigStaleness()
413 require.False(t, staleness.Dirty, "Expected staleness to be clean after auto-reload")
414}
415
416// TestRemoveConfigField_AutoReloads verifies that RemoveConfigField automatically
417// reloads config into memory after writing.
418func TestRemoveConfigField_AutoReloads(t *testing.T) {
419 t.Parallel()
420
421 dir := t.TempDir()
422 configPath := filepath.Join(dir, "crush.json")
423
424 // Create initial config file with a custom option
425 initialConfig := `{"options": {"debug": true, "custom_field": "value"}}`
426 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
427
428 // Load initial config
429 store, err := Load(dir, dir, false)
430 require.NoError(t, err)
431
432 // Set globalDataPath and capture snapshot
433 store.globalDataPath = configPath
434 store.CaptureStalenessSnapshot([]string{configPath})
435
436 // Verify the field exists initially (indirectly - store loaded successfully)
437 require.True(t, store.config.Options.Debug)
438
439 // Remove the debug field
440 err = store.RemoveConfigField(ScopeGlobal, "options.debug")
441 require.NoError(t, err)
442
443 // Verify auto-reload occurred and stale state is clean
444 staleness := store.ConfigStaleness()
445 require.False(t, staleness.Dirty, "Expected staleness to be clean after auto-reload from RemoveConfigField")
446}
447
448// TestSetConfigField_AutoReloadSkipsWhenNoWorkingDir verifies that auto-reload
449// gracefully skips when working directory is not set (e.g., during testing).
450func TestSetConfigField_AutoReloadSkipsWhenNoWorkingDir(t *testing.T) {
451 t.Parallel()
452
453 dir := t.TempDir()
454 configPath := filepath.Join(dir, "crush.json")
455
456 // Create a store without working directory (like some test setups)
457 store := &ConfigStore{
458 config: &Config{},
459 globalDataPath: configPath,
460 // workingDir is empty
461 }
462
463 // SetConfigField should succeed even without workingDir (auto-reload skips)
464 err := store.SetConfigField(ScopeGlobal, "foo", "bar")
465 require.NoError(t, err)
466
467 // Verify file was still written
468 data, err := os.ReadFile(configPath)
469 require.NoError(t, err)
470 require.Contains(t, string(data), "foo")
471}
472
473// TestAutoReloadDisabledDuringReload verifies that auto-reload is suppressed
474// during ReloadFromDisk to prevent re-entrant/nested reload calls.
475func TestAutoReloadDisabledDuringReload(t *testing.T) {
476 t.Parallel()
477
478 dir := t.TempDir()
479 configPath := filepath.Join(dir, "crush.json")
480
481 // Create initial config with a provider that will trigger config modification during reload
482 // (simulating the anthropic OAuth token removal case)
483 initialConfig := `{
484 "providers": {
485 "anthropic": {
486 "api_key": "test-key",
487 "oauth": {"access_token": "token", "refresh_token": "refresh"}
488 }
489 }
490 }`
491 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
492
493 // Load will trigger configureProviders which removes anthropic OAuth config
494 // This should NOT cause infinite recursion thanks to autoReloadDisabled guard
495 store, err := Load(dir, dir, false)
496 require.NoError(t, err)
497
498 // Verify the store loaded successfully and autoReloadDisabled was unset
499 require.False(t, store.autoReloadDisabled)
500
501 // Capture snapshot and verify reload also works without recursion
502 store.globalDataPath = configPath
503 store.CaptureStalenessSnapshot([]string{configPath})
504
505 // Modify file and reload - this should work without re-entrancy issues
506 time.Sleep(10 * time.Millisecond)
507 require.NoError(t, os.WriteFile(configPath, []byte(`{"options": {"debug": true}}`), 0o600))
508
509 err = store.ReloadFromDisk(context.Background())
510 require.NoError(t, err)
511
512 // Verify reload completed successfully
513 require.False(t, store.autoReloadDisabled, "autoReloadDisabled should be false after ReloadFromDisk")
514}
515
516// TestSetConfigFields_AutoReloadsAtomically verifies that SetConfigFields writes
517// multiple fields in a single disk write and triggers only one auto-reload,
518// avoiding intermediate states where only some fields are persisted.
519func TestSetConfigFields_AutoReloadsAtomically(t *testing.T) {
520 t.Parallel()
521
522 dir := t.TempDir()
523 configPath := filepath.Join(dir, "crush.json")
524
525 // Create initial config file.
526 initialConfig := `{"options": {"debug": false}}`
527 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
528
529 // Load initial config.
530 store, err := Load(dir, dir, false)
531 require.NoError(t, err)
532
533 // Set globalDataPath and capture snapshot.
534 store.globalDataPath = configPath
535 store.CaptureStalenessSnapshot([]string{configPath})
536
537 // Write multiple fields atomically.
538 err = store.SetConfigFields(ScopeGlobal, map[string]any{
539 "options.debug": true,
540 "options.custom": "hello",
541 })
542 require.NoError(t, err)
543
544 // Verify both fields are reflected in memory.
545 require.True(t, store.config.Options.Debug)
546}
547
548func TestLoadTokenFromDisk_ReturnsNewerToken(t *testing.T) {
549 t.Parallel()
550
551 dir := t.TempDir()
552 configPath := filepath.Join(dir, "crush.json")
553
554 // Create config file with a newer token on disk
555 configContent := `{
556 "providers": {
557 "hyper": {
558 "oauth": {
559 "access_token": "newer-token-from-disk",
560 "refresh_token": "refresh-abc",
561 "expires_in": 3600,
562 "expires_at": 9999999999
563 }
564 }
565 }
566 }`
567 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
568
569 store := &ConfigStore{
570 config: &Config{},
571 globalDataPath: configPath,
572 }
573
574 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
575 require.NoError(t, err)
576 require.NotNil(t, token)
577 require.Equal(t, "newer-token-from-disk", token.AccessToken)
578 require.Equal(t, "refresh-abc", token.RefreshToken)
579 require.Equal(t, 3600, token.ExpiresIn)
580 require.Equal(t, int64(9999999999), token.ExpiresAt)
581}
582
583func TestLoadTokenFromDisk_ReturnsNilWhenSameToken(t *testing.T) {
584 t.Parallel()
585
586 dir := t.TempDir()
587 configPath := filepath.Join(dir, "crush.json")
588
589 // Create config file with the same token
590 configContent := `{
591 "providers": {
592 "hyper": {
593 "oauth": {
594 "access_token": "same-token",
595 "refresh_token": "refresh-abc",
596 "expires_in": 3600,
597 "expires_at": 9999999999
598 }
599 }
600 }
601 }`
602 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
603
604 store := &ConfigStore{
605 config: &Config{},
606 globalDataPath: configPath,
607 }
608
609 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
610 require.NoError(t, err)
611 require.NotNil(t, token)
612 require.Equal(t, "same-token", token.AccessToken)
613}
614
615func TestLoadTokenFromDisk_ReturnsNilWhenFileMissing(t *testing.T) {
616 t.Parallel()
617
618 dir := t.TempDir()
619 configPath := filepath.Join(dir, "nonexistent.json")
620
621 store := &ConfigStore{
622 config: &Config{},
623 globalDataPath: configPath,
624 }
625
626 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
627 require.NoError(t, err)
628 require.Nil(t, token)
629}
630
631func TestLoadTokenFromDisk_ReturnsNilWhenProviderMissing(t *testing.T) {
632 t.Parallel()
633
634 dir := t.TempDir()
635 configPath := filepath.Join(dir, "crush.json")
636
637 // Create config file without the hyper provider
638 configContent := `{"providers": {"openai": {"api_key": "test-key"}}}`
639 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
640
641 store := &ConfigStore{
642 config: &Config{},
643 globalDataPath: configPath,
644 }
645
646 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
647 require.NoError(t, err)
648 require.Nil(t, token)
649}
650
651func TestLoadTokenFromDisk_ReturnsNilWhenOAuthMissing(t *testing.T) {
652 t.Parallel()
653
654 dir := t.TempDir()
655 configPath := filepath.Join(dir, "crush.json")
656
657 // Create config file with provider but no OAuth token
658 configContent := `{"providers": {"hyper": {"api_key": "test-key"}}}`
659 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
660
661 store := &ConfigStore{
662 config: &Config{},
663 globalDataPath: configPath,
664 }
665
666 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
667 require.NoError(t, err)
668 require.Nil(t, token)
669}
670
671func TestRefreshOAuthToken_UsesDiskTokenWhenDifferent(t *testing.T) {
672 t.Parallel()
673
674 dir := t.TempDir()
675 configPath := filepath.Join(dir, "crush.json")
676
677 // Create config file with a newer token on disk
678 configContent := `{
679 "providers": {
680 "hyper": {
681 "api_key": "newer-access-token",
682 "oauth": {
683 "access_token": "newer-access-token",
684 "refresh_token": "refresh-abc",
685 "expires_in": 3600,
686 "expires_at": 9999999999
687 }
688 }
689 }
690 }`
691 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
692
693 // Set up store with an older in-memory token
694 oldToken := &oauth.Token{
695 AccessToken: "older-access-token",
696 RefreshToken: "refresh-abc",
697 ExpiresIn: 3600,
698 ExpiresAt: time.Now().Add(-time.Hour).Unix(), // Expired
699 }
700
701 providers := csync.NewMap[string, ProviderConfig]()
702 providers.Set("hyper", ProviderConfig{
703 ID: "hyper",
704 Name: "Hyper",
705 APIKey: oldToken.AccessToken,
706 OAuthToken: oldToken,
707 })
708
709 store := &ConfigStore{
710 config: &Config{
711 Providers: providers,
712 },
713 globalDataPath: configPath,
714 }
715
716 // Refresh should use the disk token without making an external call
717 err := store.RefreshOAuthToken(context.Background(), ScopeGlobal, "hyper")
718 require.NoError(t, err)
719
720 // Verify the in-memory token was updated to the disk token
721 updatedConfig, ok := store.config.Providers.Get("hyper")
722 require.True(t, ok)
723 require.Equal(t, "newer-access-token", updatedConfig.APIKey)
724 require.Equal(t, "newer-access-token", updatedConfig.OAuthToken.AccessToken)
725 require.Equal(t, "refresh-abc", updatedConfig.OAuthToken.RefreshToken)
726}