From fc82b3e6b1f3e70e8b7d7d8565d8f4839044a705 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 14 Jul 2026 19:04:03 +0800 Subject: [PATCH] fix: validate bundled plugin requirements --- internal/api/app.go | 19 +++------- internal/api/app_test.go | 13 +++++++ internal/core/capability/platform.go | 20 ++++++++++ internal/core/plugin/lifecycle_test.go | 52 ++++++++++++++++++++------ main.go | 13 +------ scripts/build-linux-bundle.sh | 1 + scripts/build-windows.sh | 1 + scripts/test-package-formats.sh | 2 + 8 files changed, 85 insertions(+), 36 deletions(-) create mode 100644 internal/core/capability/platform.go diff --git a/internal/api/app.go b/internal/api/app.go index 68b0145..9923168 100644 --- a/internal/api/app.go +++ b/internal/api/app.go @@ -1064,30 +1064,23 @@ func (a *App) ReloadPlugins() (int, string) { // Unregister all non-core capabilities a.capRegistry.UnregisterAll() - // Re-register core capabilities - coreCaps := []string{ - "verstak/core/plugin-manager/v1", - "verstak/core/capability-registry/v1", - "verstak/core/contribution-registry/v1", - "verstak/core/permissions/v1", - "verstak/core/events/v1", - "verstak/core/files/v1", - "verstak/core/workbench/v1", - } - if err := a.capRegistry.Register("verstak-desktop", coreCaps); err != nil { + // Re-register the same core capabilities as initial startup. Keeping this + // list in the capability package prevents a plugin reload from dropping a + // required capability such as native notifications. + if err := a.capRegistry.Register(capability.CorePluginID, capability.CorePlatformCapabilities()); err != nil { log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err) } // Re-register vault capability if vault is open if a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen { - if err := a.capRegistry.Register("verstak-desktop", []string{"verstak/core/vault/v1"}); err != nil { + if err := a.capRegistry.Register(capability.CorePluginID, []string{"verstak/core/vault/v1"}); err != nil { log.Printf("[api] ReloadPlugins: failed to re-register vault capability: %v", err) } } // Re-register workspace capability if workspace is initialized if a.workspace != nil && a.workspace.IsInitialized() { - if err := a.capRegistry.Register("verstak-desktop", []string{"verstak/core/workspace/v1"}); err != nil { + if err := a.capRegistry.Register(capability.CorePluginID, []string{"verstak/core/workspace/v1"}); err != nil { log.Printf("[api] ReloadPlugins: failed to re-register workspace capability: %v", err) } } diff --git a/internal/api/app_test.go b/internal/api/app_test.go index 5746784..257498b 100644 --- a/internal/api/app_test.go +++ b/internal/api/app_test.go @@ -225,6 +225,19 @@ func TestReplaceAndClearPluginNotificationsStayWithinPluginNamespace(t *testing. } } +func TestReloadPluginsRestoresNotificationCoreCapability(t *testing.T) { + app := &App{ + capRegistry: capability.NewRegistry(), + contribRegistry: contribution.NewRegistry(), + } + + app.ReloadPlugins() + + if !app.capRegistry.Has("verstak/core/notifications/v1") { + t.Fatal("ReloadPlugins did not restore verstak/core/notifications/v1") + } +} + func TestDomReadyInitializesNotificationsBeforeScheduler(t *testing.T) { oldInitialize := initializeNativeNotifications defer func() { initializeNativeNotifications = oldInitialize }() diff --git a/internal/core/capability/platform.go b/internal/core/capability/platform.go new file mode 100644 index 0000000..0ecba60 --- /dev/null +++ b/internal/core/capability/platform.go @@ -0,0 +1,20 @@ +package capability + +const CorePluginID = "verstak-desktop" + +var platformCapabilities = []string{ + "verstak/core/plugin-manager/v1", + "verstak/core/capability-registry/v1", + "verstak/core/contribution-registry/v1", + "verstak/core/permissions/v1", + "verstak/core/events/v1", + "verstak/core/files/v1", + "verstak/core/workbench/v1", + "verstak/core/notifications/v1", +} + +// CorePlatformCapabilities returns a copy of the capabilities registered by +// the desktop before dynamic plugins are resolved. +func CorePlatformCapabilities() []string { + return append([]string(nil), platformCapabilities...) +} diff --git a/internal/core/plugin/lifecycle_test.go b/internal/core/plugin/lifecycle_test.go index 68b4b08..28cfe48 100644 --- a/internal/core/plugin/lifecycle_test.go +++ b/internal/core/plugin/lifecycle_test.go @@ -3,25 +3,16 @@ package plugin import ( "os" "path/filepath" + "runtime" "testing" "github.com/verstak/verstak-desktop/internal/core/capability" ) -// coreCapabilities lists the 5 core capabilities that the platform registers -// before any plugins are loaded. -var coreCapabilities = []string{ - "verstak/core/plugin-manager/v1", - "verstak/core/capability-registry/v1", - "verstak/core/contribution-registry/v1", - "verstak/core/permissions/v1", - "verstak/core/events/v1", -} - -// registerCoreCapabilities registers the 5 core capabilities on a registry. +// registerCoreCapabilities registers the desktop's core capabilities on a registry. func registerCoreCapabilities(t *testing.T, reg *capability.Registry) { t.Helper() - if err := reg.Register("verstak-core", coreCapabilities); err != nil { + if err := reg.Register(capability.CorePluginID, capability.CorePlatformCapabilities()); err != nil { t.Fatalf("failed to register core capabilities: %v", err) } } @@ -76,6 +67,43 @@ func TestLifecycle_CoreCapabilitiesRegisteredBeforePlugins(t *testing.T) { } } +func TestBundledOfficialPluginRequirementsResolve(t *testing.T) { + plugins, errs := DiscoverPlugins([]string{bundledPluginDir(t)}) + if len(errs) > 0 { + t.Fatalf("discover bundled plugins: %v", errs) + } + + reg := capability.NewRegistry() + registerCoreCapabilities(t, reg) + ResolveLifecycle(plugins, reg, nil) + + foundTodo := false + for _, loaded := range plugins { + if loaded.Manifest.ID == "verstak.todo" { + foundTodo = true + } + if loaded.Status == StatusMissingRequiredCapability { + t.Fatalf("bundled plugin %q is missing required capabilities: %s", loaded.Manifest.ID, loaded.Error) + } + } + + if !foundTodo { + t.Fatal("bundled Todo manifest was not discovered") + } +} + +func bundledPluginDir(t *testing.T) string { + t.Helper() + if configured := os.Getenv("VERSTAK_RELEASE_PLUGIN_DIR"); configured != "" { + return configured + } + _, sourceFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("could not resolve lifecycle test path") + } + return filepath.Join(filepath.Dir(sourceFile), "../../../plugins") +} + // TestLifecycle_MissingRequiredCapability verifies that when the required // capability is NOT registered, CheckRequired reports it as missing and the // plugin status should be set to StatusMissingRequiredCapability. diff --git a/main.go b/main.go index 929fc81..4cf7161 100644 --- a/main.go +++ b/main.go @@ -86,17 +86,8 @@ func main() { } // ─── Register Core Capabilities ───────────────────────── - corePluginID := "verstak-desktop" - coreCaps := []string{ - "verstak/core/plugin-manager/v1", - "verstak/core/capability-registry/v1", - "verstak/core/contribution-registry/v1", - "verstak/core/permissions/v1", - "verstak/core/events/v1", - "verstak/core/files/v1", - "verstak/core/workbench/v1", - "verstak/core/notifications/v1", - } + corePluginID := capability.CorePluginID + coreCaps := capability.CorePlatformCapabilities() if err := capRegistry.Register(corePluginID, coreCaps); err != nil { log.Fatalf("[main] failed to register core capabilities: %v", err) } diff --git a/scripts/build-linux-bundle.sh b/scripts/build-linux-bundle.sh index 2eda918..7b5ba4c 100755 --- a/scripts/build-linux-bundle.sh +++ b/scripts/build-linux-bundle.sh @@ -13,6 +13,7 @@ fi echo "=== verstak desktop Linux amd64 bundle ===" (cd "$OFFICIAL_PLUGINS" && ./scripts/build.sh) "$ROOT/scripts/install-dev-plugins.sh" +(cd "$ROOT" && go test ./internal/core/plugin -run TestBundledOfficialPluginRequirementsResolve -count=1) "$ROOT/scripts/build.sh" BINARY="$ROOT/build/bin/verstak-desktop" diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh index 9c2ce40..c85d79e 100755 --- a/scripts/build-windows.sh +++ b/scripts/build-windows.sh @@ -52,6 +52,7 @@ if [[ ! -d "$WINDOWS_PLUGIN_DIST" ]]; then echo "Windows plugin packages were not produced: $WINDOWS_PLUGIN_DIST" >&2 exit 1 fi +VERSTAK_RELEASE_PLUGIN_DIR="$WINDOWS_PLUGIN_DIST" go test ./internal/core/plugin -run TestBundledOfficialPluginRequirementsResolve -count=1 # Wails' -compiler option selects a Go binary, not a C compiler. Cross-CGO # therefore has to be supplied through the standard Go environment instead. diff --git a/scripts/test-package-formats.sh b/scripts/test-package-formats.sh index f42c63b..0ae0bf1 100755 --- a/scripts/test-package-formats.sh +++ b/scripts/test-package-formats.sh @@ -41,5 +41,7 @@ grep -Fq 'package-appimage.sh' "$ROOT/scripts/release.sh" grep -Fq 'package-windows-portable.sh' "$ROOT/scripts/release.sh" git -C "$ROOT" check-ignore -q verstak-desktop-res.syso grep -Fq 'chmod -R a+rX' "$ROOT/scripts/build-linux-bundle.sh" +grep -Fq 'TestBundledOfficialPluginRequirementsResolve' "$ROOT/scripts/build-linux-bundle.sh" +grep -Fq 'TestBundledOfficialPluginRequirementsResolve' "$ROOT/scripts/build-windows.sh" echo "desktop package script contracts passed"