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 dir := t.TempDir()
322 configPath := filepath.Join(dir, "crush.json")
323
324 // Isolate from the host's global config so only test-provided
325 // providers are visible.
326 t.Setenv("CRUSH_GLOBAL_CONFIG", dir)
327 t.Setenv("CRUSH_GLOBAL_DATA", dir)
328 resetProviderState()
329 t.Cleanup(resetProviderState)
330
331 // Create initial config with one model preference
332 initialConfig := `{
333 "models": {
334 "large": {"provider": "openai", "model": "gpt-4"}
335 },
336 "providers": {
337 "openai": {
338 "api_key": "test-key",
339 "models": [{"id": "gpt-4", "name": "GPT-4"}]
340 }
341 }
342 }`
343 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
344
345 // Load initial config properly
346 store, err := Load(dir, dir, false)
347 require.NoError(t, err)
348
349 // Set globalDataPath for the test (Load doesn't set this directly)
350 store.globalDataPath = configPath
351 store.CaptureStalenessSnapshot([]string{configPath})
352
353 // Verify initial model
354 require.Equal(t, "openai", store.config.Models[SelectedModelTypeLarge].Provider)
355 require.Equal(t, "gpt-4", store.config.Models[SelectedModelTypeLarge].Model)
356
357 // Modify config on disk to change model
358 updatedConfig := `{
359 "models": {
360 "large": {"provider": "anthropic", "model": "claude-3"}
361 },
362 "providers": {
363 "openai": {
364 "api_key": "test-key",
365 "models": [{"id": "gpt-4", "name": "GPT-4"}]
366 },
367 "anthropic": {
368 "api_key": "test-key-2",
369 "models": [{"id": "claude-3", "name": "Claude 3"}]
370 }
371 }
372 }`
373 time.Sleep(10 * time.Millisecond)
374 require.NoError(t, os.WriteFile(configPath, []byte(updatedConfig), 0o600))
375
376 // Reload from disk
377 ctx := context.Background()
378 err = store.ReloadFromDisk(ctx)
379 require.NoError(t, err)
380
381 // Verify the NEW config values are now in effect (regression check)
382 require.Equal(t, "anthropic", store.config.Models[SelectedModelTypeLarge].Provider)
383 require.Equal(t, "claude-3", store.config.Models[SelectedModelTypeLarge].Model)
384}
385
386// TestSetConfigField_AutoReloads verifies that SetConfigField automatically
387// reloads config into memory after writing, so subsequent reads see the new value.
388func TestSetConfigField_AutoReloads(t *testing.T) {
389 t.Parallel()
390
391 dir := t.TempDir()
392 configPath := filepath.Join(dir, "crush.json")
393
394 // Create initial config file with debug = false
395 initialConfig := `{"options": {"debug": false}}`
396 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
397
398 // Load initial config
399 store, err := Load(dir, dir, false)
400 require.NoError(t, err)
401
402 // Verify initial state
403 require.False(t, store.config.Options.Debug)
404
405 // Set globalDataPath and capture snapshot for staleness tracking
406 store.globalDataPath = configPath
407 store.CaptureStalenessSnapshot([]string{configPath})
408
409 // Use SetConfigField to change debug to true
410 err = store.SetConfigField(ScopeGlobal, "options.debug", true)
411 require.NoError(t, err)
412
413 // Verify in-memory state was automatically reloaded and reflects the change
414 require.True(t, store.config.Options.Debug, "Expected config to auto-reload and show debug = true")
415
416 // Verify staleness is clean after the reload
417 staleness := store.ConfigStaleness()
418 require.False(t, staleness.Dirty, "Expected staleness to be clean after auto-reload")
419}
420
421// TestRemoveConfigField_AutoReloads verifies that RemoveConfigField automatically
422// reloads config into memory after writing.
423func TestRemoveConfigField_AutoReloads(t *testing.T) {
424 t.Parallel()
425
426 dir := t.TempDir()
427 configPath := filepath.Join(dir, "crush.json")
428
429 // Create initial config file with a custom option
430 initialConfig := `{"options": {"debug": true, "custom_field": "value"}}`
431 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
432
433 // Load initial config
434 store, err := Load(dir, dir, false)
435 require.NoError(t, err)
436
437 // Set globalDataPath and capture snapshot
438 store.globalDataPath = configPath
439 store.CaptureStalenessSnapshot([]string{configPath})
440
441 // Verify the field exists initially (indirectly - store loaded successfully)
442 require.True(t, store.config.Options.Debug)
443
444 // Remove the debug field
445 err = store.RemoveConfigField(ScopeGlobal, "options.debug")
446 require.NoError(t, err)
447
448 // Verify auto-reload occurred and stale state is clean
449 staleness := store.ConfigStaleness()
450 require.False(t, staleness.Dirty, "Expected staleness to be clean after auto-reload from RemoveConfigField")
451}
452
453// TestSetConfigField_AutoReloadSkipsWhenNoWorkingDir verifies that auto-reload
454// gracefully skips when working directory is not set (e.g., during testing).
455func TestSetConfigField_AutoReloadSkipsWhenNoWorkingDir(t *testing.T) {
456 t.Parallel()
457
458 dir := t.TempDir()
459 configPath := filepath.Join(dir, "crush.json")
460
461 // Create a store without working directory (like some test setups)
462 store := &ConfigStore{
463 config: &Config{},
464 globalDataPath: configPath,
465 // workingDir is empty
466 }
467
468 // SetConfigField should succeed even without workingDir (auto-reload skips)
469 err := store.SetConfigField(ScopeGlobal, "foo", "bar")
470 require.NoError(t, err)
471
472 // Verify file was still written
473 data, err := os.ReadFile(configPath)
474 require.NoError(t, err)
475 require.Contains(t, string(data), "foo")
476}
477
478// TestAutoReloadDisabledDuringReload verifies that auto-reload is suppressed
479// during ReloadFromDisk to prevent re-entrant/nested reload calls.
480func TestAutoReloadDisabledDuringReload(t *testing.T) {
481 t.Parallel()
482
483 dir := t.TempDir()
484 configPath := filepath.Join(dir, "crush.json")
485
486 // Create initial config with a provider that will trigger config modification during reload
487 // (simulating the anthropic OAuth token removal case)
488 initialConfig := `{
489 "providers": {
490 "anthropic": {
491 "api_key": "test-key",
492 "oauth": {"access_token": "token", "refresh_token": "refresh"}
493 }
494 }
495 }`
496 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
497
498 // Load will trigger configureProviders which removes anthropic OAuth config
499 // This should NOT cause infinite recursion thanks to autoReloadDisabled guard
500 store, err := Load(dir, dir, false)
501 require.NoError(t, err)
502
503 // Verify the store loaded successfully and autoReloadDisabled was unset
504 require.False(t, store.autoReloadDisabled)
505
506 // Capture snapshot and verify reload also works without recursion
507 store.globalDataPath = configPath
508 store.CaptureStalenessSnapshot([]string{configPath})
509
510 // Modify file and reload - this should work without re-entrancy issues
511 time.Sleep(10 * time.Millisecond)
512 require.NoError(t, os.WriteFile(configPath, []byte(`{"options": {"debug": true}}`), 0o600))
513
514 err = store.ReloadFromDisk(context.Background())
515 require.NoError(t, err)
516
517 // Verify reload completed successfully
518 require.False(t, store.autoReloadDisabled, "autoReloadDisabled should be false after ReloadFromDisk")
519}
520
521// TestSetConfigFields_AutoReloadsAtomically verifies that SetConfigFields writes
522// multiple fields in a single disk write and triggers only one auto-reload,
523// avoiding intermediate states where only some fields are persisted.
524func TestSetConfigFields_AutoReloadsAtomically(t *testing.T) {
525 t.Parallel()
526
527 dir := t.TempDir()
528 configPath := filepath.Join(dir, "crush.json")
529
530 // Create initial config file.
531 initialConfig := `{"options": {"debug": false}}`
532 require.NoError(t, os.WriteFile(configPath, []byte(initialConfig), 0o600))
533
534 // Load initial config.
535 store, err := Load(dir, dir, false)
536 require.NoError(t, err)
537
538 // Set globalDataPath and capture snapshot.
539 store.globalDataPath = configPath
540 store.CaptureStalenessSnapshot([]string{configPath})
541
542 // Write multiple fields atomically.
543 err = store.SetConfigFields(ScopeGlobal, map[string]any{
544 "options.debug": true,
545 "options.custom": "hello",
546 })
547 require.NoError(t, err)
548
549 // Verify both fields are reflected in memory.
550 require.True(t, store.config.Options.Debug)
551}
552
553func TestLoadTokenFromDisk_ReturnsNewerToken(t *testing.T) {
554 t.Parallel()
555
556 dir := t.TempDir()
557 configPath := filepath.Join(dir, "crush.json")
558
559 // Create config file with a newer token on disk
560 configContent := `{
561 "providers": {
562 "hyper": {
563 "oauth": {
564 "access_token": "newer-token-from-disk",
565 "refresh_token": "refresh-abc",
566 "expires_in": 3600,
567 "expires_at": 9999999999
568 }
569 }
570 }
571 }`
572 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
573
574 store := &ConfigStore{
575 config: &Config{},
576 globalDataPath: configPath,
577 }
578
579 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
580 require.NoError(t, err)
581 require.NotNil(t, token)
582 require.Equal(t, "newer-token-from-disk", token.AccessToken)
583 require.Equal(t, "refresh-abc", token.RefreshToken)
584 require.Equal(t, 3600, token.ExpiresIn)
585 require.Equal(t, int64(9999999999), token.ExpiresAt)
586}
587
588func TestLoadTokenFromDisk_ReturnsNilWhenSameToken(t *testing.T) {
589 t.Parallel()
590
591 dir := t.TempDir()
592 configPath := filepath.Join(dir, "crush.json")
593
594 // Create config file with the same token
595 configContent := `{
596 "providers": {
597 "hyper": {
598 "oauth": {
599 "access_token": "same-token",
600 "refresh_token": "refresh-abc",
601 "expires_in": 3600,
602 "expires_at": 9999999999
603 }
604 }
605 }
606 }`
607 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
608
609 store := &ConfigStore{
610 config: &Config{},
611 globalDataPath: configPath,
612 }
613
614 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
615 require.NoError(t, err)
616 require.NotNil(t, token)
617 require.Equal(t, "same-token", token.AccessToken)
618}
619
620func TestLoadTokenFromDisk_ReturnsNilWhenFileMissing(t *testing.T) {
621 t.Parallel()
622
623 dir := t.TempDir()
624 configPath := filepath.Join(dir, "nonexistent.json")
625
626 store := &ConfigStore{
627 config: &Config{},
628 globalDataPath: configPath,
629 }
630
631 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
632 require.NoError(t, err)
633 require.Nil(t, token)
634}
635
636func TestLoadTokenFromDisk_ReturnsNilWhenProviderMissing(t *testing.T) {
637 t.Parallel()
638
639 dir := t.TempDir()
640 configPath := filepath.Join(dir, "crush.json")
641
642 // Create config file without the hyper provider
643 configContent := `{"providers": {"openai": {"api_key": "test-key"}}}`
644 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
645
646 store := &ConfigStore{
647 config: &Config{},
648 globalDataPath: configPath,
649 }
650
651 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
652 require.NoError(t, err)
653 require.Nil(t, token)
654}
655
656func TestLoadTokenFromDisk_ReturnsNilWhenOAuthMissing(t *testing.T) {
657 t.Parallel()
658
659 dir := t.TempDir()
660 configPath := filepath.Join(dir, "crush.json")
661
662 // Create config file with provider but no OAuth token
663 configContent := `{"providers": {"hyper": {"api_key": "test-key"}}}`
664 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
665
666 store := &ConfigStore{
667 config: &Config{},
668 globalDataPath: configPath,
669 }
670
671 token, err := store.loadTokenFromDisk(ScopeGlobal, "hyper")
672 require.NoError(t, err)
673 require.Nil(t, token)
674}
675
676func TestRefreshOAuthToken_UsesDiskTokenWhenDifferent(t *testing.T) {
677 t.Parallel()
678
679 dir := t.TempDir()
680 configPath := filepath.Join(dir, "crush.json")
681
682 // Create config file with a newer token on disk
683 configContent := `{
684 "providers": {
685 "hyper": {
686 "api_key": "newer-access-token",
687 "oauth": {
688 "access_token": "newer-access-token",
689 "refresh_token": "refresh-abc",
690 "expires_in": 3600,
691 "expires_at": 9999999999
692 }
693 }
694 }
695 }`
696 require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o600))
697
698 // Set up store with an older in-memory token
699 oldToken := &oauth.Token{
700 AccessToken: "older-access-token",
701 RefreshToken: "refresh-abc",
702 ExpiresIn: 3600,
703 ExpiresAt: time.Now().Add(-time.Hour).Unix(), // Expired
704 }
705
706 providers := csync.NewMap[string, ProviderConfig]()
707 providers.Set("hyper", ProviderConfig{
708 ID: "hyper",
709 Name: "Hyper",
710 APIKey: oldToken.AccessToken,
711 OAuthToken: oldToken,
712 })
713
714 store := &ConfigStore{
715 config: &Config{
716 Providers: providers,
717 },
718 globalDataPath: configPath,
719 }
720
721 // Refresh should use the disk token without making an external call
722 err := store.RefreshOAuthToken(context.Background(), ScopeGlobal, "hyper")
723 require.NoError(t, err)
724
725 // Verify the in-memory token was updated to the disk token
726 updatedConfig, ok := store.config.Providers.Get("hyper")
727 require.True(t, ok)
728 require.Equal(t, "newer-access-token", updatedConfig.APIKey)
729 require.Equal(t, "newer-access-token", updatedConfig.OAuthToken.AccessToken)
730 require.Equal(t, "refresh-abc", updatedConfig.OAuthToken.RefreshToken)
731}