package main import ( "os" "path/filepath" "testing" "verstak/internal/core/config" "verstak/internal/core/plugins" ) // TestSetPluginEnabled_BrokenPlugin_Rollback verifies that when activation fails, // the plugin is fully rolled back: not Active, not Enabled, and not in config. func TestSetPluginEnabled_BrokenPlugin_Rollback(t *testing.T) { // Isolate global config to a temp dir so we don't pollute the user's real config tmpCfgDir := filepath.Join(t.TempDir(), "config") if err := os.MkdirAll(tmpCfgDir, 0o750); err != nil { t.Fatalf("mkdir config dir: %v", err) } t.Setenv("XDG_CONFIG_HOME", tmpCfgDir) vaultRoot := t.TempDir() // Create .verstak/plugins/ structure pluginsDir := filepath.Join(vaultRoot, ".verstak", "plugins") if err := os.MkdirAll(pluginsDir, 0o750); err != nil { t.Fatalf("mkdir plugins: %v", err) } // Create a broken plugin: valid Lua but invalid background task interval brokenDir := filepath.Join(pluginsDir, "broken") if err := os.MkdirAll(brokenDir, 0o750); err != nil { t.Fatalf("mkdir broken plugin: %v", err) } // plugin.json with an invalid background task interval ("not-a-duration") pluginJSON := `{ "name": "broken", "version": "0.1.0", "hooks": { "on_install": "on_install", "on_init": "on_init" }, "background_tasks": [ {"id": "bad-task", "interval": "not-a-duration", "script": "bad.lua"} ] }` if err := os.WriteFile(filepath.Join(brokenDir, "plugin.json"), []byte(pluginJSON), 0o644); err != nil { t.Fatalf("write plugin.json: %v", err) } // main.lua — on_install is a no-op (tables not needed for this test), // on_init is harmless. Activation will fail on the invalid background task interval. mainLua := ` function on_install() -- no-op end function on_init() -- harmless end ` if err := os.WriteFile(filepath.Join(brokenDir, "main.lua"), []byte(mainLua), 0o644); err != nil { t.Fatalf("write main.lua: %v", err) } // Create App with real plugin manager app := &App{ plugins: plugins.NewManager(vaultRoot), vault: vaultRoot, vaultOpen: true, } // Discover the plugin app.plugins.Discover() // Step 1: Install the plugin (creates tables, marks installed in config) if err := app.plugins.Install("broken"); err != nil { t.Fatalf("install broken plugin: %v", err) } // Verify plugin is installed if !app.plugins.IsInstalled("broken") { t.Fatal("expected plugin to be installed") } // Step 2: Try to enable — should fail because background task has invalid interval err := app.SetPluginEnabled("broken", true) if err == nil { t.Fatal("expected SetPluginEnabled to fail for broken plugin, got nil") } t.Logf("SetPluginEnabled returned expected error: %v", err) // Step 3: Verify plugin is NOT Active and NOT Enabled (in-memory rollback) found := false for _, p := range app.plugins.Plugins() { if p.Meta.Name == "broken" { found = true if p.Active { t.Error("expected plugin to NOT be Active after failed activation") } if p.Enabled { t.Error("expected plugin to NOT be Enabled after failed activation (in-memory rollback)") } } } if !found { t.Fatal("plugin 'broken' not found in manager") } // Step 4: Verify plugin is NOT in EnabledPlugins config appCfg, err := config.LoadAppConfig() if err != nil { t.Fatalf("load app config: %v", err) } if appCfg != nil { for _, name := range appCfg.EnabledPlugins { if name == "broken" { t.Error("expected 'broken' to NOT be in EnabledPlugins config after failed activation") } } } }