Skip to content

Commit 50a6c8b

Browse files
danbarrclaude
andauthored
Persist config backward-compat migrations to load path (#5416)
Loading a config from an explicit path (PathProvider, LoadOrCreate- ConfigWithPath) that needed a backward-compatibility migration wrote the migrated result to the default xdg path instead of the path it was loaded from. applyBackwardCompatibility called the path-agnostic save(), which always resolves to getConfigPath(). For tests this clobbered the developer's real config file whenever a PathProvider loaded a temp config with a secrets provider set but setup_completed false, defeating the isolation added for #894. It was also a latent correctness bug for any path-based load in production. Thread the load path through applyBackwardCompatibility and persist via saveToPath(configPath). Remove the now-unused save() helper. Closes #894 Co-authored-by: Dan Barr <6922515+danbarr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bfde12d commit 50a6c8b

2 files changed

Lines changed: 50 additions & 11 deletions

File tree

pkg/config/config.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,11 @@ func createNewConfigWithDefaults() Config {
190190
}
191191
}
192192

193-
// applyBackwardCompatibility applies backward compatibility fixes to existing configs
194-
func applyBackwardCompatibility(config *Config) error {
193+
// applyBackwardCompatibility applies backward compatibility fixes to existing configs.
194+
// Any migration that needs to be persisted is written back to configPath, the same
195+
// path the config was loaded from, so that path-based loads (e.g. PathProvider) stay
196+
// isolated. An empty configPath falls back to the default path via saveToPath.
197+
func applyBackwardCompatibility(config *Config, configPath string) error {
195198
// Hack - if the secrets provider type is set to the old `basic` type,
196199
// just change it to `encrypted`.
197200
if config.Secrets.ProviderType == "basic" {
@@ -202,7 +205,7 @@ func applyBackwardCompatibility(config *Config) error {
202205
_ = os.Remove(oldPath)
203206
}
204207
config.Secrets.ProviderType = string(secrets.EncryptedType)
205-
err = config.save()
208+
err = config.saveToPath(configPath)
206209
if err != nil {
207210
return fmt.Errorf("error updating config: %w", err)
208211
}
@@ -212,7 +215,7 @@ func applyBackwardCompatibility(config *Config) error {
212215
// consider it as setup completed (for existing users)
213216
if config.Secrets.ProviderType != "" && !config.Secrets.SetupCompleted {
214217
config.Secrets.SetupCompleted = true
215-
err := config.save()
218+
err := config.saveToPath(configPath)
216219
if err != nil {
217220
return fmt.Errorf("error updating config for backward compatibility: %w", err)
218221
}
@@ -291,8 +294,9 @@ func LoadOrCreateConfigFromPath(configPath string) (*Config, error) {
291294
return nil, fmt.Errorf("failed to parse config file yaml: %w", err)
292295
}
293296

294-
// Apply backward compatibility fixes
295-
err = applyBackwardCompatibility(&config)
297+
// Apply backward compatibility fixes, persisting any migration back to the
298+
// same path the config was loaded from.
299+
err = applyBackwardCompatibility(&config, configPath)
296300
if err != nil {
297301
return nil, fmt.Errorf("failed to apply backward compatibility fixes: %w", err)
298302
}
@@ -301,11 +305,6 @@ func LoadOrCreateConfigFromPath(configPath string) (*Config, error) {
301305
return &config, nil
302306
}
303307

304-
// Save serializes the config struct and writes it to disk.
305-
func (c *Config) save() error {
306-
return c.saveToPath("")
307-
}
308-
309308
// saveToPath serializes the config struct and writes it to a specific path.
310309
// If configPath is empty, it uses the default path.
311310
func (c *Config) saveToPath(configPath string) error {

pkg/config/config_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,46 @@ func TestUpdateConfigAtPath_CallbackError(t *testing.T) {
282282
"config should not be written to disk when the callback returns an error")
283283
}
284284

285+
// TestLoadFromPath_BackwardCompatMigrationStaysOnPath guards the test-isolation
286+
// regression from issue #894: a path-based load that triggers a backward-compat
287+
// migration must persist the migration back to the same path, never to the
288+
// default (real) config path. See applyBackwardCompatibility.
289+
//
290+
//nolint:paralleltest // swaps the package-level getConfigPath; must not run in parallel
291+
func TestLoadFromPath_BackwardCompatMigrationStaysOnPath(t *testing.T) {
292+
// Not parallel: this test swaps the package-level getConfigPath sentinel.
293+
294+
// Point the default path generator at a sentinel that must never be written.
295+
sentinelPath := filepath.Join(t.TempDir(), "should-never-exist", "config.yaml")
296+
originalGetConfigPath := getConfigPath
297+
getConfigPath = func() (string, error) { return sentinelPath, nil }
298+
t.Cleanup(func() { getConfigPath = originalGetConfigPath })
299+
300+
// A config that triggers the "provider set but setup_completed false" migration.
301+
_, configPath := SetupTestConfig(t, &Config{
302+
Secrets: Secrets{
303+
ProviderType: string(secrets.EncryptedType),
304+
SetupCompleted: false,
305+
},
306+
})
307+
308+
config, err := LoadOrCreateConfigWithPath(configPath)
309+
require.NoError(t, err)
310+
assert.True(t, config.Secrets.SetupCompleted,
311+
"backward-compat migration should mark setup as completed")
312+
313+
// The migration must be persisted to the path we loaded from.
314+
reloaded, err := LoadOrCreateConfigWithPath(configPath)
315+
require.NoError(t, err)
316+
assert.True(t, reloaded.Secrets.SetupCompleted,
317+
"migration should be persisted back to the loaded path")
318+
319+
// The default/real config path must never have been touched.
320+
_, statErr := os.Stat(sentinelPath)
321+
assert.ErrorIs(t, statErr, os.ErrNotExist,
322+
"backward-compat migration must not write to the default config path")
323+
}
324+
285325
func TestSecrets_GetProviderType_EnvironmentVariable(t *testing.T) {
286326
t.Parallel()
287327

0 commit comments

Comments
 (0)