From b58493141c69cdf3c4c9d7a85a518f413f942f71 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 27 Dec 2025 10:22:25 -0500 Subject: [PATCH] feat: extend plugin watcher with improved logging and debounce duration adjustment Signed-off-by: Deluan --- plugins/watcher.go | 6 +- plugins/watcher_integration_test.go | 102 ---------------------------- plugins/watcher_test.go | 93 ++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 104 deletions(-) delete mode 100644 plugins/watcher_integration_test.go diff --git a/plugins/watcher.go b/plugins/watcher.go index a2e51909f..6be377719 100644 --- a/plugins/watcher.go +++ b/plugins/watcher.go @@ -13,7 +13,7 @@ import ( // debounceDuration is the time to wait before acting on file events // to handle multiple rapid events for the same file. -const debounceDuration = 500 * time.Millisecond +const debounceDuration = 2 * time.Second // startWatcher starts the file watcher for the plugins folder. // It watches for CREATE, WRITE, and REMOVE events on .wasm files. @@ -156,16 +156,20 @@ func (m *Manager) processPluginEvent(pluginName string, eventType notify.Event) // Determine and execute the appropriate action action := determinePluginAction(eventType, isLoaded) + log.Debug("Plugin event action", "plugin", pluginName, "action", action) switch action { case actionLoad: + log.Debug("Loading new Plugin", "plugin", pluginName) if err := m.LoadPlugin(pluginName); err != nil { log.Error(m.ctx, "Failed to load plugin", "plugin", pluginName, err) } case actionUnload: + log.Debug("Unloading removed Plugin", "plugin", pluginName) if err := m.UnloadPlugin(pluginName); err != nil { log.Debug(m.ctx, "Plugin not loaded, skipping unload", "plugin", pluginName, err) } case actionReload: + log.Debug("Reloading modified Plugin", "plugin", pluginName) if err := m.ReloadPlugin(pluginName); err != nil { log.Error(m.ctx, "Failed to reload plugin", "plugin", pluginName, err) } diff --git a/plugins/watcher_integration_test.go b/plugins/watcher_integration_test.go deleted file mode 100644 index c7432198e..000000000 --- a/plugins/watcher_integration_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package plugins - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/navidrome/navidrome/conf" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/rjeczalik/notify" -) - -var _ = Describe("Watcher Integration", Ordered, func() { - // Uses testdataDir and createTestManager from BeforeSuite - var ( - manager *Manager - tmpDir string - ctx context.Context - ) - - BeforeAll(func() { - if testing.Short() { - Skip("Skipping integration test in short mode") - } - ctx = GinkgoT().Context() - - // Create manager for watcher lifecycle tests (no plugin preloaded - tests copy plugin as needed) - manager, tmpDir = createTestManager(nil) - - // Remove the auto-loaded plugin so tests can control loading - _ = manager.UnloadPlugin("test-metadata-agent") - _ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm")) - }) - - // Helper to copy test plugin into the temp folder - copyTestPlugin := func() { - srcPath := filepath.Join(testdataDir, "test-metadata-agent.wasm") - destPath := filepath.Join(tmpDir, "test-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("test-metadata-agent") - _ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm")) - }) - - It("loads a plugin on CREATE event", func() { - copyTestPlugin() - manager.processPluginEvent("test-metadata-agent", notify.Create) - Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent")) - }) - - It("reloads a plugin on WRITE event", func() { - copyTestPlugin() - err := manager.LoadPlugin("test-metadata-agent") - Expect(err).ToNot(HaveOccurred()) - - manager.processPluginEvent("test-metadata-agent", notify.Write) - Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent")) - }) - - It("unloads a plugin on REMOVE event", func() { - copyTestPlugin() - err := manager.LoadPlugin("test-metadata-agent") - Expect(err).ToNot(HaveOccurred()) - - manager.processPluginEvent("test-metadata-agent", notify.Remove) - Expect(manager.PluginNames(string(CapabilityMetadataAgent))).ToNot(ContainElement("test-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 - autoReloadManager := &Manager{ - plugins: make(map[string]*plugin), - } - err := autoReloadManager.Start(ctx) - Expect(err).ToNot(HaveOccurred()) - DeferCleanup(autoReloadManager.Stop) - - Expect(autoReloadManager.watcherEvents).ToNot(BeNil()) - Expect(autoReloadManager.watcherDone).ToNot(BeNil()) - }) - }) -}) diff --git a/plugins/watcher_test.go b/plugins/watcher_test.go index 8d05940ed..8114e5a44 100644 --- a/plugins/watcher_test.go +++ b/plugins/watcher_test.go @@ -1,12 +1,103 @@ package plugins import ( + "context" + "os" + "path/filepath" + + "github.com/navidrome/navidrome/conf" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/rjeczalik/notify" ) -var _ = Describe("Watcher", func() { +var _ = Describe("Plugin Watcher", func() { + Describe("Integration Tests", Ordered, func() { + // Uses testdataDir and createTestManager from BeforeSuite + var ( + manager *Manager + tmpDir string + ctx context.Context + ) + + BeforeAll(func() { + ctx = GinkgoT().Context() + + // Create manager for watcher lifecycle tests (no plugin preloaded - tests copy plugin as needed) + manager, tmpDir = createTestManager(nil) + + // Remove the auto-loaded plugin so tests can control loading + _ = manager.UnloadPlugin("test-metadata-agent") + _ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm")) + }) + + // Helper to copy test plugin into the temp folder + copyTestPlugin := func() { + srcPath := filepath.Join(testdataDir, "test-metadata-agent.wasm") + destPath := filepath.Join(tmpDir, "test-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("test-metadata-agent") + _ = os.Remove(filepath.Join(tmpDir, "test-metadata-agent.wasm")) + }) + + It("loads a plugin on CREATE event", func() { + copyTestPlugin() + manager.processPluginEvent("test-metadata-agent", notify.Create) + Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent")) + }) + + It("reloads a plugin on WRITE event", func() { + copyTestPlugin() + err := manager.LoadPlugin("test-metadata-agent") + Expect(err).ToNot(HaveOccurred()) + + manager.processPluginEvent("test-metadata-agent", notify.Write) + Expect(manager.PluginNames(string(CapabilityMetadataAgent))).To(ContainElement("test-metadata-agent")) + }) + + It("unloads a plugin on REMOVE event", func() { + copyTestPlugin() + err := manager.LoadPlugin("test-metadata-agent") + Expect(err).ToNot(HaveOccurred()) + + manager.processPluginEvent("test-metadata-agent", notify.Remove) + Expect(manager.PluginNames(string(CapabilityMetadataAgent))).ToNot(ContainElement("test-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 + autoReloadManager := &Manager{ + plugins: make(map[string]*plugin), + } + err := autoReloadManager.Start(ctx) + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(autoReloadManager.Stop) + + Expect(autoReloadManager.watcherEvents).ToNot(BeNil()) + Expect(autoReloadManager.watcherDone).ToNot(BeNil()) + }) + }) + }) + Describe("determinePluginAction", func() { // These are fast unit tests for the pure routing logic. // No WASM compilation, no file I/O - runs in microseconds.