store_test.go

  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}