tests(plugins): optimize tests

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-22 12:40:38 -05:00
parent 39be1878cb
commit f0f191266c
10 changed files with 268 additions and 245 deletions

View File

@ -3,7 +3,6 @@ package plugins
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync" "sync"
@ -15,65 +14,43 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("Manager", func() { var _ = Describe("Manager", Ordered, func() {
var ( var (
manager *Manager manager *Manager
ctx context.Context ctx context.Context
testdataDir string testdataDir string
tmpDir string
) )
BeforeEach(func() { BeforeAll(func() {
ctx = GinkgoT().Context() ctx = context.Background()
// Get testdata directory // Get testdata directory (where fake-metadata-agent.wasm lives)
_, currentFile, _, ok := runtime.Caller(0) _, currentFile, _, ok := runtime.Caller(0)
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
testdataDir = filepath.Join(filepath.Dir(currentFile), "testdata") testdataDir = filepath.Join(filepath.Dir(currentFile), "testdata")
// Create temp dir for plugins
var err error
tmpDir, err = os.MkdirTemp("", "plugins-test-*")
Expect(err).ToNot(HaveOccurred())
// Setup config // Setup config
DeferCleanup(configtest.SetupConfig()) DeferCleanup(configtest.SetupConfig())
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = testdataDir
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Create a fresh manager for each test // Create manager once for all tests
manager = &Manager{ manager = &Manager{
plugins: make(map[string]*pluginInstance), plugins: make(map[string]*pluginInstance),
} }
err = manager.Start(ctx) err := manager.Start(ctx)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
DeferCleanup(func() { DeferCleanup(func() {
_ = manager.Stop() _ = manager.Stop()
_ = os.RemoveAll(tmpDir)
}) })
}) })
copyTestPlugin := func(destName string) string {
srcPath := filepath.Join(testdataDir, "test-plugin.wasm")
destPath := filepath.Join(tmpDir, destName+".wasm")
data, err := os.ReadFile(srcPath)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(destPath, data, 0600)
Expect(err).ToNot(HaveOccurred())
return destPath
}
Describe("LoadPlugin", func() { Describe("LoadPlugin", func() {
It("loads a new plugin by name", func() { It("auto-loads plugins from folder on Start", func() {
copyTestPlugin("new-plugin") // Plugin is already loaded by manager.Start() via discoverPlugins
err := manager.LoadPlugin("new-plugin")
Expect(err).ToNot(HaveOccurred())
names := manager.PluginNames(string(CapabilityMetadataAgent)) names := manager.PluginNames(string(CapabilityMetadataAgent))
Expect(names).To(ContainElement("new-plugin")) Expect(names).To(ContainElement("fake-metadata-agent"))
}) })
It("returns error when plugin file does not exist", func() { It("returns error when plugin file does not exist", func() {
@ -83,19 +60,21 @@ var _ = Describe("Manager", func() {
}) })
It("returns error when plugin is already loaded", func() { It("returns error when plugin is already loaded", func() {
copyTestPlugin("duplicate") // Plugin was loaded on Start, try to load again
err := manager.LoadPlugin("fake-metadata-agent")
err := manager.LoadPlugin("duplicate")
Expect(err).ToNot(HaveOccurred())
err = manager.LoadPlugin("duplicate")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("already loaded")) Expect(err.Error()).To(ContainSubstring("already loaded"))
}) })
It("returns error when plugins folder is not configured", func() { It("returns error when plugins folder is not configured", func() {
originalFolder := conf.Server.Plugins.Folder
originalDataFolder := conf.Server.DataFolder
conf.Server.Plugins.Folder = "" conf.Server.Plugins.Folder = ""
conf.Server.DataFolder = "" conf.Server.DataFolder = ""
defer func() {
conf.Server.Plugins.Folder = originalFolder
conf.Server.DataFolder = originalDataFolder
}()
err := manager.LoadPlugin("test") err := manager.LoadPlugin("test")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
@ -105,15 +84,21 @@ var _ = Describe("Manager", func() {
Describe("UnloadPlugin", func() { Describe("UnloadPlugin", func() {
It("removes a loaded plugin", func() { It("removes a loaded plugin", func() {
copyTestPlugin("to-unload") // Plugin is already loaded from Start
err := manager.LoadPlugin("to-unload") err := manager.UnloadPlugin("fake-metadata-agent")
Expect(err).ToNot(HaveOccurred())
err = manager.UnloadPlugin("to-unload")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
names := manager.PluginNames(string(CapabilityMetadataAgent)) names := manager.PluginNames(string(CapabilityMetadataAgent))
Expect(names).ToNot(ContainElement("to-unload")) Expect(names).ToNot(ContainElement("fake-metadata-agent"))
})
It("can reload after unload", func() {
// Reload the plugin we just unloaded
err := manager.LoadPlugin("fake-metadata-agent")
Expect(err).ToNot(HaveOccurred())
names := manager.PluginNames(string(CapabilityMetadataAgent))
Expect(names).To(ContainElement("fake-metadata-agent"))
}) })
It("returns error when plugin not found", func() { It("returns error when plugin not found", func() {
@ -125,15 +110,11 @@ var _ = Describe("Manager", func() {
Describe("ReloadPlugin", func() { Describe("ReloadPlugin", func() {
It("unloads and reloads a plugin", func() { It("unloads and reloads a plugin", func() {
copyTestPlugin("to-reload") err := manager.ReloadPlugin("fake-metadata-agent")
err := manager.LoadPlugin("to-reload")
Expect(err).ToNot(HaveOccurred())
err = manager.ReloadPlugin("to-reload")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
names := manager.PluginNames(string(CapabilityMetadataAgent)) names := manager.PluginNames(string(CapabilityMetadataAgent))
Expect(names).To(ContainElement("to-reload")) Expect(names).To(ContainElement("fake-metadata-agent"))
}) })
It("returns error when plugin not found", func() { It("returns error when plugin not found", func() {
@ -141,31 +122,10 @@ var _ = Describe("Manager", func() {
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to unload")) Expect(err.Error()).To(ContainSubstring("failed to unload"))
}) })
It("keeps plugin unloaded if reload fails", func() {
copyTestPlugin("fail-reload")
err := manager.LoadPlugin("fail-reload")
Expect(err).ToNot(HaveOccurred())
// Remove the wasm file so reload will fail
wasmPath := filepath.Join(tmpDir, "fail-reload.wasm")
err = os.Remove(wasmPath)
Expect(err).ToNot(HaveOccurred())
err = manager.ReloadPlugin("fail-reload")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to reload"))
// Plugin should no longer be loaded
names := manager.PluginNames(string(CapabilityMetadataAgent))
Expect(names).ToNot(ContainElement("fail-reload"))
})
}) })
It("can call the plugin concurrently", func() { It("can call the plugin concurrently", func() {
copyTestPlugin("new-plugin") // Plugin is already loaded
err := manager.LoadPlugin("new-plugin")
Expect(err).ToNot(HaveOccurred())
const concurrency = 100 const concurrency = 100
errs := make(chan error, concurrency) errs := make(chan error, concurrency)
@ -176,7 +136,7 @@ var _ = Describe("Manager", func() {
for i := range concurrency { for i := range concurrency {
go func(i int) { go func(i int) {
defer g.Done() defer g.Done()
a, ok := manager.LoadMediaAgent("new-plugin") a, ok := manager.LoadMediaAgent("fake-metadata-agent")
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
agent := a.(agents.ArtistBiographyRetriever) agent := a.(agents.ArtistBiographyRetriever)
bio, err := agent.GetArtistBiography(ctx, fmt.Sprintf("artist-%d", i), fmt.Sprintf("Artist %d", i), "") bio, err := agent.GetArtistBiography(ctx, fmt.Sprintf("artist-%d", i), fmt.Sprintf("Artist %d", i), "")
@ -199,5 +159,4 @@ var _ = Describe("Manager", func() {
} }
} }
}) })
}) })

View File

@ -36,8 +36,8 @@ var _ = Describe("MetadataAgent", Ordered, func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// Copy test plugin to temp dir // Copy test plugin to temp dir
srcPath := filepath.Join(testdataDir, "test-plugin.wasm") srcPath := filepath.Join(testdataDir, "fake-metadata-agent.wasm")
destPath := filepath.Join(tmpDir, "test-plugin.wasm") destPath := filepath.Join(tmpDir, "fake-metadata-agent.wasm")
data, err := os.ReadFile(srcPath) data, err := os.ReadFile(srcPath)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(destPath, data, 0600) err = os.WriteFile(destPath, data, 0600)
@ -58,7 +58,7 @@ var _ = Describe("MetadataAgent", Ordered, func() {
// Load the agent via manager // Load the agent via manager
var ok2 bool var ok2 bool
agent, ok2 = manager.LoadMediaAgent("test-plugin") agent, ok2 = manager.LoadMediaAgent("fake-metadata-agent")
Expect(ok2).To(BeTrue()) Expect(ok2).To(BeTrue())
DeferCleanup(func() { DeferCleanup(func() {
@ -69,7 +69,7 @@ var _ = Describe("MetadataAgent", Ordered, func() {
Describe("AgentName", func() { Describe("AgentName", func() {
It("returns the plugin name", func() { It("returns the plugin name", func() {
Expect(agent.AgentName()).To(Equal("test-plugin")) Expect(agent.AgentName()).To(Equal("fake-metadata-agent"))
}) })
}) })

View File

@ -1,13 +1,34 @@
//go:build !windows
package plugins package plugins
import ( import (
"os/exec"
"testing" "testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
const testDataDir = "plugins/testdata"
func TestPlugins(t *testing.T) { func TestPlugins(t *testing.T) {
tests.Init(t, false)
buildTestPlugins(t, testDataDir)
log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail) RegisterFailHandler(Fail)
RunSpecs(t, "Plugins Suite") RunSpecs(t, "Plugins Suite")
} }
func buildTestPlugins(t *testing.T, path string) {
t.Helper()
t.Logf("[BeforeSuite] Current working directory: %s", path)
cmd := exec.Command("make", "-C", path)
out, err := cmd.CombinedOutput()
t.Logf("[BeforeSuite] Make output: %s", string(out))
if err != nil {
t.Fatalf("Failed to build test plugins: %v", err)
}
}

11
plugins/testdata/Makefile vendored Normal file
View File

@ -0,0 +1,11 @@
# Build fake sample plugins used for testing
# Auto-discover all plugin folders (folders containing go.mod)
PLUGINS := $(patsubst %/go.mod,%,$(wildcard */go.mod))
all: $(PLUGINS:%=%.wasm)
clean:
rm -f $(PLUGINS:%=%.wasm)
%.wasm: %/main.go %/go.mod
cd $* && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ../$@ .

View File

@ -106,6 +106,38 @@ func (m *Manager) handleWatcherEvent(event notify.EventInfo) {
m.debounceMu.Unlock() m.debounceMu.Unlock()
} }
// pluginAction represents the action to take on a plugin based on a file event
type pluginAction int
const (
actionNone pluginAction = iota // No action needed
actionLoad // Load the plugin
actionUnload // Unload the plugin
actionReload // Reload the plugin
)
// determinePluginAction decides what action to take based on the file event type
// and whether the plugin is currently loaded. This is a pure function with no side effects.
func determinePluginAction(eventType notify.Event, isLoaded bool) pluginAction {
switch {
case eventType&notify.Remove != 0 || eventType&notify.Rename != 0:
// File removed or renamed away - unload if loaded
return actionUnload
case eventType&notify.Create != 0:
// New file - load it
return actionLoad
case eventType&notify.Write != 0:
// File modified - reload if loaded, otherwise load
if isLoaded {
return actionReload
}
return actionLoad
}
return actionNone
}
// processPluginEvent handles the actual plugin load/unload/reload after debouncing // processPluginEvent handles the actual plugin load/unload/reload after debouncing
func (m *Manager) processPluginEvent(pluginName string, eventType notify.Event) { func (m *Manager) processPluginEvent(pluginName string, eventType notify.Event) {
// Don't process if manager is stopping/stopped (atomic check to avoid race with Stop()) // Don't process if manager is stopping/stopped (atomic check to avoid race with Stop())
@ -118,35 +150,25 @@ func (m *Manager) processPluginEvent(pluginName string, eventType notify.Event)
delete(m.debounceTimers, pluginName) delete(m.debounceTimers, pluginName)
m.debounceMu.Unlock() m.debounceMu.Unlock()
switch { // Check if plugin is currently loaded
case eventType&notify.Remove != 0 || eventType&notify.Rename != 0: m.mu.RLock()
// File removed or renamed away - unload if loaded _, isLoaded := m.plugins[pluginName]
m.mu.RUnlock()
// Determine and execute the appropriate action
action := determinePluginAction(eventType, isLoaded)
switch action {
case actionLoad:
if err := m.LoadPlugin(pluginName); err != nil {
log.Error(m.ctx, "Failed to load plugin", "plugin", pluginName, err)
}
case actionUnload:
if err := m.UnloadPlugin(pluginName); err != nil { if err := m.UnloadPlugin(pluginName); err != nil {
// Plugin may not have been loaded, that's okay
log.Debug(m.ctx, "Plugin not loaded, skipping unload", "plugin", pluginName, err) log.Debug(m.ctx, "Plugin not loaded, skipping unload", "plugin", pluginName, err)
} }
case actionReload:
case eventType&notify.Create != 0: if err := m.ReloadPlugin(pluginName); err != nil {
// New file - load it log.Error(m.ctx, "Failed to reload plugin", "plugin", pluginName, err)
if err := m.LoadPlugin(pluginName); err != nil {
log.Error(m.ctx, "Failed to load new plugin", "plugin", pluginName, err)
}
case eventType&notify.Write != 0:
// File modified - check if it's loaded and reload
m.mu.RLock()
_, isLoaded := m.plugins[pluginName]
m.mu.RUnlock()
if isLoaded {
if err := m.ReloadPlugin(pluginName); err != nil {
log.Error(m.ctx, "Failed to reload plugin", "plugin", pluginName, err)
}
} else {
// Not loaded yet, try to load it (might be a new file that was written after create)
if err := m.LoadPlugin(pluginName); err != nil {
log.Error(m.ctx, "Failed to load plugin", "plugin", pluginName, err)
}
} }
} }
} }

View File

@ -0,0 +1,125 @@
package plugins
import (
"context"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/rjeczalik/notify"
)
var _ = Describe("Watcher Integration", Ordered, func() {
var (
manager *Manager
ctx context.Context
testdataDir string
tmpDir string
)
BeforeAll(func() {
if testing.Short() {
Skip("Skipping integration test in short mode")
}
ctx = GinkgoT().Context()
// Get testdata directory
_, currentFile, _, ok := runtime.Caller(0)
Expect(ok).To(BeTrue())
testdataDir = filepath.Join(filepath.Dir(currentFile), "testdata")
// Create temp dir for plugins
var err error
tmpDir, err = os.MkdirTemp("", "plugins-watcher-integration-*")
Expect(err).ToNot(HaveOccurred())
// Setup config (AutoReload disabled - tests inject events directly)
DeferCleanup(configtest.SetupConfig())
conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false
// Create a fresh manager for each test
manager = &Manager{
plugins: make(map[string]*pluginInstance),
}
err = manager.Start(ctx)
Expect(err).ToNot(HaveOccurred())
DeferCleanup(func() {
_ = manager.Stop()
_ = os.RemoveAll(tmpDir)
})
})
// Helper to copy test plugin into the temp folder
copyTestPlugin := func() {
srcPath := filepath.Join(testdataDir, "fake-metadata-agent.wasm")
destPath := filepath.Join(tmpDir, "fake-metadata-agent.wasm")
data, err := os.ReadFile(srcPath)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(destPath, data, 0600)
Expect(err).ToNot(HaveOccurred())
}
Describe("Plugin event processing (integration)", func() {
// These tests verify the full flow with actual WASM plugin loading.
AfterEach(func() {
// Clean up: unload plugin if loaded, remove copied file
_ = manager.UnloadPlugin("fake-metadata-agent")
_ = os.Remove(filepath.Join(tmpDir, "fake-metadata-agent.wasm"))
})
It("loads a plugin on CREATE event", func() {
copyTestPlugin()
manager.processPluginEvent("fake-metadata-agent", notify.Create)
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("fake-metadata-agent"))
})
It("reloads a plugin on WRITE event", func() {
copyTestPlugin()
err := manager.LoadPlugin("fake-metadata-agent")
Expect(err).ToNot(HaveOccurred())
manager.processPluginEvent("fake-metadata-agent", notify.Write)
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("fake-metadata-agent"))
})
It("unloads a plugin on REMOVE event", func() {
copyTestPlugin()
err := manager.LoadPlugin("fake-metadata-agent")
Expect(err).ToNot(HaveOccurred())
manager.processPluginEvent("fake-metadata-agent", notify.Remove)
Expect(manager.PluginNames(string(CapabilityMetadataAgent))).ToNot(ContainElement("fake-metadata-agent"))
})
})
Describe("Watcher lifecycle", func() {
It("does not start file watcher when AutoReload is disabled", func() {
Expect(manager.watcherEvents).To(BeNil())
Expect(manager.watcherDone).To(BeNil())
})
It("starts file watcher when AutoReload is enabled", func() {
_ = manager.Stop()
conf.Server.Plugins.AutoReload = true
manager = &Manager{
plugins: make(map[string]*pluginInstance),
}
err := manager.Start(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(manager.watcherEvents).ToNot(BeNil())
Expect(manager.watcherDone).ToNot(BeNil())
})
})
})

View File

@ -1,156 +1,41 @@
package plugins package plugins
import ( import (
"context"
"os"
"path/filepath"
"runtime"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/rjeczalik/notify"
) )
var _ = Describe("Watcher", func() { var _ = Describe("Watcher", func() {
var ( Describe("determinePluginAction", func() {
manager *Manager // These are fast unit tests for the pure routing logic.
ctx context.Context // No WASM compilation, no file I/O - runs in microseconds.
testdataDir string
tmpDir string
)
BeforeEach(func() { DescribeTable("returns correct action for event type and loaded state",
ctx = GinkgoT().Context() func(eventType notify.Event, isLoaded bool, expected pluginAction) {
Expect(determinePluginAction(eventType, isLoaded)).To(Equal(expected))
},
// CREATE events - always load
Entry("CREATE when not loaded", notify.Create, false, actionLoad),
Entry("CREATE when loaded", notify.Create, true, actionLoad),
// Use shorter debounce for faster tests // WRITE events - reload if loaded, load if not
originalDebounce := debounceDuration Entry("WRITE when not loaded", notify.Write, false, actionLoad),
debounceDuration = 50 * time.Millisecond Entry("WRITE when loaded", notify.Write, true, actionReload),
DeferCleanup(func() { debounceDuration = originalDebounce })
// Get testdata directory // REMOVE events - always unload
_, currentFile, _, ok := runtime.Caller(0) Entry("REMOVE when not loaded", notify.Remove, false, actionUnload),
Expect(ok).To(BeTrue()) Entry("REMOVE when loaded", notify.Remove, true, actionUnload),
testdataDir = filepath.Join(filepath.Dir(currentFile), "testdata")
// Create temp dir for plugins // RENAME events - treated same as REMOVE
var err error Entry("RENAME when not loaded", notify.Rename, false, actionUnload),
tmpDir, err = os.MkdirTemp("", "plugins-watcher-test-*") Entry("RENAME when loaded", notify.Rename, true, actionUnload),
Expect(err).ToNot(HaveOccurred()) )
// Setup config with AutoReload enabled It("returns actionNone for unknown event types", func() {
DeferCleanup(configtest.SetupConfig()) // Event type 0 or other unknown values
conf.Server.Plugins.Enabled = true Expect(determinePluginAction(0, false)).To(Equal(actionNone))
conf.Server.Plugins.Folder = tmpDir Expect(determinePluginAction(0, true)).To(Equal(actionNone))
conf.Server.Plugins.AutoReload = true
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Create a fresh manager for each test
manager = &Manager{
plugins: make(map[string]*pluginInstance),
}
err = manager.Start(ctx)
Expect(err).ToNot(HaveOccurred())
DeferCleanup(func() {
_ = manager.Stop()
_ = os.RemoveAll(tmpDir)
})
})
copyTestPlugin := func(destName string) string {
srcPath := filepath.Join(testdataDir, "test-plugin.wasm")
destPath := filepath.Join(tmpDir, destName+".wasm")
data, err := os.ReadFile(srcPath)
Expect(err).ToNot(HaveOccurred())
err = os.WriteFile(destPath, data, 0600)
Expect(err).ToNot(HaveOccurred())
return destPath
}
Describe("Auto-reload via file watcher", func() {
It("loads a plugin when a new wasm file is created", func() {
// Copy plugin file to trigger CREATE event
copyTestPlugin("watch-create")
// Wait for debounce + processing
Eventually(func() []string {
return manager.PluginNames(string(CapabilityMetadataAgent))
}, 1*time.Second, 50*time.Millisecond).Should(ContainElement("watch-create"))
})
It("reloads a plugin when the wasm file is modified", func() {
// First, load a plugin
copyTestPlugin("watch-modify")
// Wait for it to be loaded
Eventually(func() []string {
return manager.PluginNames(string(CapabilityMetadataAgent))
}, 1*time.Second, 50*time.Millisecond).Should(ContainElement("watch-modify"))
// Get the original plugin info
originalInfo := manager.GetPluginInfo()["watch-modify"]
Expect(originalInfo.Name).ToNot(BeEmpty())
// Modify the file (re-copy to trigger WRITE event)
wasmPath := filepath.Join(tmpDir, "watch-modify.wasm")
data, err := os.ReadFile(wasmPath)
Expect(err).ToNot(HaveOccurred())
// Touch the file to trigger write event
err = os.WriteFile(wasmPath, data, 0600)
Expect(err).ToNot(HaveOccurred())
// Wait for reload - the plugin should still be there
// We can't easily verify it was reloaded without adding tracking,
// but at least verify it's still loaded
Consistently(func() []string {
return manager.PluginNames(string(CapabilityMetadataAgent))
}, 300*time.Millisecond, 50*time.Millisecond).Should(ContainElement("watch-modify"))
})
It("unloads a plugin when the wasm file is removed", func() {
// First, load a plugin
wasmPath := copyTestPlugin("watch-remove")
// Wait for it to be loaded
Eventually(func() []string {
return manager.PluginNames(string(CapabilityMetadataAgent))
}, 1*time.Second, 50*time.Millisecond).Should(ContainElement("watch-remove"))
// Remove the file
err := os.Remove(wasmPath)
Expect(err).ToNot(HaveOccurred())
// Wait for it to be unloaded
Eventually(func() []string {
return manager.PluginNames(string(CapabilityMetadataAgent))
}, 1*time.Second, 50*time.Millisecond).ShouldNot(ContainElement("watch-remove"))
})
})
Describe("Watcher disabled", func() {
BeforeEach(func() {
// Stop existing manager and create one without auto-reload
_ = manager.Stop()
conf.Server.Plugins.AutoReload = false
manager = &Manager{
plugins: make(map[string]*pluginInstance),
}
err := manager.Start(ctx)
Expect(err).ToNot(HaveOccurred())
})
It("does not auto-load plugins when AutoReload is disabled", func() {
// Copy plugin file
copyTestPlugin("no-watch")
// Wait a bit and verify plugin is NOT loaded
Consistently(func() []string {
return manager.PluginNames(string(CapabilityMetadataAgent))
}, 300*time.Millisecond, 50*time.Millisecond).ShouldNot(ContainElement("no-watch"))
}) })
}) })
}) })