From ba9e233e1907bc93cb2f674b8dc594e0fd513490 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 14:29:06 +0000 Subject: [PATCH 1/2] feat: support top-level 'plugins' config key and deprecate 'default.plugins' The plugin list can now be configured at the top level of .imposter.yaml and config.yaml, which is simpler and more intuitive. The previous nested 'default.plugins' key is still supported with a deprecation warning. https://claude.ai/code/session_01MUAgVQQU6HNuqnoKeMk33V --- docs/config.md | 10 ++--- internal/plugin/configs_test.go | 68 +++++++++++++++++++++++++++++-- internal/plugin/defaults.go | 25 ++++++++++-- internal/plugin/defaults_test.go | 70 ++++++++++++++++++++++++++------ internal/plugin/plugin.go | 17 +++++--- 5 files changed, 160 insertions(+), 30 deletions(-) diff --git a/docs/config.md b/docs/config.md index 2aa2189..2c4cee1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -71,12 +71,10 @@ plugin: # ignored if plugin.dir is set baseDir: "/path/to/base/dir" -# Default configuration regardless of engine version -default: - # List of plugins to install - plugins: - - store-dynamodb - - store-redis +# List of plugins to install +plugins: + - store-dynamodb + - store-redis # Map of environment variables to set env: diff --git a/internal/plugin/configs_test.go b/internal/plugin/configs_test.go index fd865fa..c0985b2 100644 --- a/internal/plugin/configs_test.go +++ b/internal/plugin/configs_test.go @@ -61,9 +61,9 @@ func TestEnsureConfiguredPlugins(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Setup viper configuration - viper.Set(defaultPluginsConfigKey, tt.configuredPlugins) - defer viper.Set(defaultPluginsConfigKey, nil) // Clean up + // Setup viper configuration using the new top-level key + viper.Set(pluginsConfigKey, tt.configuredPlugins) + defer viper.Set(pluginsConfigKey, nil) // Clean up // Test EnsureConfiguredPlugins count, err := EnsureConfiguredPlugins(tt.engineType, tt.version) @@ -78,3 +78,65 @@ func TestEnsureConfiguredPlugins(t *testing.T) { }) } } + +func TestEnsureConfiguredPlugins_DeprecatedKey(t *testing.T) { + configDir, err := os.MkdirTemp(os.TempDir(), "imposter-plugin-configs-deprecated-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(configDir) + config.DirPath = configDir + + tests := []struct { + name string + deprecatedPlugins []string + expectedCount int + }{ + { + name: "deprecated key with plugins", + deprecatedPlugins: []string{"store-redis"}, + expectedCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Set(defaultPluginsConfigKey, tt.deprecatedPlugins) + defer viper.Set(defaultPluginsConfigKey, nil) + + count, err := EnsureConfiguredPlugins(engine.EngineTypeDockerCore, "4.9.1") + if err != nil { + t.Errorf("EnsureConfiguredPlugins() error = %v", err) + return + } + if count != tt.expectedCount { + t.Errorf("EnsureConfiguredPlugins() count = %v, expectedCount %v", count, tt.expectedCount) + } + }) + } +} + +func TestEnsureConfiguredPlugins_BothKeys(t *testing.T) { + configDir, err := os.MkdirTemp(os.TempDir(), "imposter-plugin-configs-both-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(configDir) + config.DirPath = configDir + + viper.Set(pluginsConfigKey, []string{"store-redis"}) + viper.Set(defaultPluginsConfigKey, []string{"store-redis"}) + defer func() { + viper.Set(pluginsConfigKey, nil) + viper.Set(defaultPluginsConfigKey, nil) + }() + + count, err := EnsureConfiguredPlugins(engine.EngineTypeDockerCore, "4.9.1") + if err != nil { + t.Errorf("EnsureConfiguredPlugins() error = %v", err) + return + } + if count != 1 { + t.Errorf("EnsureConfiguredPlugins() count = %v, expected 1 (merged and deduplicated)", count) + } +} diff --git a/internal/plugin/defaults.go b/internal/plugin/defaults.go index c0c79a3..5a677e7 100644 --- a/internal/plugin/defaults.go +++ b/internal/plugin/defaults.go @@ -8,6 +8,9 @@ import ( "path/filepath" ) +const pluginsConfigKey = "plugins" + +// Deprecated: use pluginsConfigKey instead. const defaultPluginsConfigKey = "default.plugins" // addDefaultPlugins adds the provided plugins to the list of default @@ -45,9 +48,20 @@ func ListDefaultPlugins() ([]string, error) { v, err := parseConfigFile() if err != nil { return []string{}, err - } else { - return v.GetStringSlice(defaultPluginsConfigKey), nil } + return getConfiguredPlugins(v), nil +} + +// getConfiguredPlugins reads plugins from both the top-level "plugins" +// key and the deprecated "default.plugins" key, merging and deduplicating. +func getConfiguredPlugins(v *viper.Viper) []string { + plugins := v.GetStringSlice(pluginsConfigKey) + deprecated := v.GetStringSlice(defaultPluginsConfigKey) + if len(deprecated) > 0 { + logger.Warnf("'default.plugins' config key is deprecated; use top-level 'plugins' instead") + plugins = stringutil.CombineUnique(plugins, deprecated) + } + return plugins } func writeDefaultPlugins(plugins []string) error { @@ -55,7 +69,12 @@ func writeDefaultPlugins(plugins []string) error { if err != nil { return err } - v.Set(defaultPluginsConfigKey, plugins) + v.Set(pluginsConfigKey, plugins) + + // clear deprecated nested key so it is not written back + if v.IsSet(defaultPluginsConfigKey) { + v.Set(defaultPluginsConfigKey, []string{}) + } configDir, err := config.GetGlobalConfigDir() if err != nil { diff --git a/internal/plugin/defaults_test.go b/internal/plugin/defaults_test.go index b15b036..84f8b3f 100644 --- a/internal/plugin/defaults_test.go +++ b/internal/plugin/defaults_test.go @@ -29,7 +29,23 @@ func TestListDefaultPlugins(t *testing.T) { expectError: false, }, { - name: "config with plugins", + name: "top-level plugins", + configContent: `plugins: + - store-redis + - js-graal +`, + expectedPlugins: []string{"store-redis", "js-graal"}, + expectError: false, + }, + { + name: "top-level empty plugins list", + configContent: `plugins: [] +`, + expectedPlugins: []string{}, + expectError: false, + }, + { + name: "deprecated default.plugins format", configContent: `default: plugins: - store-redis @@ -39,13 +55,25 @@ func TestListDefaultPlugins(t *testing.T) { expectError: false, }, { - name: "config with empty plugins list", + name: "deprecated default.plugins empty list", configContent: `default: plugins: [] `, expectedPlugins: []string{}, expectError: false, }, + { + name: "both formats merges and deduplicates", + configContent: `plugins: + - store-redis +default: + plugins: + - js-graal + - store-redis +`, + expectedPlugins: []string{"store-redis", "js-graal"}, + expectError: false, + }, } for _, tt := range tests { @@ -321,18 +349,17 @@ func TestParseConfigFile(t *testing.T) { config.DirPath = configDir // Test with non-existent config file - viper, err := parseConfigFile() + v, err := parseConfigFile() if err != nil { t.Errorf("parseConfigFile() with non-existent file error = %v", err) } - if viper == nil { + if v == nil { t.Error("parseConfigFile() returned nil viper instance") } - // Test with existing config file - configContent := `default: - plugins: - - test-plugin + // Test with existing config file using top-level plugins + configContent := `plugins: + - test-plugin ` configFilePath := filepath.Join(configDir, "config.yaml") err = os.WriteFile(configFilePath, []byte(configContent), 0644) @@ -340,17 +367,36 @@ func TestParseConfigFile(t *testing.T) { t.Fatal(err) } - viper, err = parseConfigFile() + v, err = parseConfigFile() if err != nil { t.Errorf("parseConfigFile() with existing file error = %v", err) } - if viper == nil { + if v == nil { t.Error("parseConfigFile() returned nil viper instance") } - // Verify the config was parsed correctly - plugins := viper.GetStringSlice("default.plugins") + plugins := v.GetStringSlice("plugins") if len(plugins) != 1 || plugins[0] != "test-plugin" { t.Errorf("parseConfigFile() parsed plugins incorrectly: got %v, expected [test-plugin]", plugins) } + + // Test with deprecated default.plugins format + configContent = `default: + plugins: + - legacy-plugin +` + err = os.WriteFile(configFilePath, []byte(configContent), 0644) + if err != nil { + t.Fatal(err) + } + + v, err = parseConfigFile() + if err != nil { + t.Errorf("parseConfigFile() with deprecated format error = %v", err) + } + + legacyPlugins := v.GetStringSlice("default.plugins") + if len(legacyPlugins) != 1 || legacyPlugins[0] != "legacy-plugin" { + t.Errorf("parseConfigFile() parsed deprecated plugins incorrectly: got %v, expected [legacy-plugin]", legacyPlugins) + } } diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index d739d9c..f957f0d 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -46,19 +46,24 @@ func EnsureConfiguredPlugins(engineType engine.EngineType, version string) (int, // this includes the config from the current configuration context, // not just the global CLI config file, so it includes any // configuration in the working directory - plugins := viper.GetStringSlice(defaultPluginsConfigKey) + plugins := viper.GetStringSlice(pluginsConfigKey) + deprecated := viper.GetStringSlice(defaultPluginsConfigKey) + if len(deprecated) > 0 { + logger.Warnf("'default.plugins' config key is deprecated; use top-level 'plugins' instead") + plugins = append(plugins, deprecated...) + } + + var expanded []string for _, plugin := range plugins { // work-around for https://github.com/spf13/viper/issues/380 if strings.Contains(plugin, ",") { - for _, p := range strings.Split(plugin, ",") { - plugins = append(plugins, p) - } + expanded = append(expanded, strings.Split(plugin, ",")...) } else { - plugins = append(plugins, plugin) + expanded = append(expanded, plugin) } } - plugins = stringutil.Unique(plugins) + plugins = stringutil.Unique(expanded) logger.Tracef("found %d configured plugin(s): %v", len(plugins), plugins) return EnsurePlugins(plugins, engineType, version, false) From 156c92ebbc646cfd6157f7170602740403d371b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 12:23:56 +0000 Subject: [PATCH 2/2] chore: remove unnecessary comment in configs_test.go https://claude.ai/code/session_01MUAgVQQU6HNuqnoKeMk33V --- internal/plugin/configs_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/configs_test.go b/internal/plugin/configs_test.go index c0985b2..a0426f3 100644 --- a/internal/plugin/configs_test.go +++ b/internal/plugin/configs_test.go @@ -61,7 +61,7 @@ func TestEnsureConfiguredPlugins(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Setup viper configuration using the new top-level key + // Setup viper configuration viper.Set(pluginsConfigKey, tt.configuredPlugins) defer viper.Set(pluginsConfigKey, nil) // Clean up