fix: validate bundled plugin requirements

This commit is contained in:
mirivlad 2026-07-14 19:04:03 +08:00
parent 680846da72
commit fc82b3e6b1
8 changed files with 85 additions and 36 deletions

View File

@ -1064,30 +1064,23 @@ func (a *App) ReloadPlugins() (int, string) {
// Unregister all non-core capabilities // Unregister all non-core capabilities
a.capRegistry.UnregisterAll() a.capRegistry.UnregisterAll()
// Re-register core capabilities // Re-register the same core capabilities as initial startup. Keeping this
coreCaps := []string{ // list in the capability package prevents a plugin reload from dropping a
"verstak/core/plugin-manager/v1", // required capability such as native notifications.
"verstak/core/capability-registry/v1", if err := a.capRegistry.Register(capability.CorePluginID, capability.CorePlatformCapabilities()); err != nil {
"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 {
log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err) log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err)
} }
// Re-register vault capability if vault is open // Re-register vault capability if vault is open
if a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen { 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) log.Printf("[api] ReloadPlugins: failed to re-register vault capability: %v", err)
} }
} }
// Re-register workspace capability if workspace is initialized // Re-register workspace capability if workspace is initialized
if a.workspace != nil && a.workspace.IsInitialized() { 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) log.Printf("[api] ReloadPlugins: failed to re-register workspace capability: %v", err)
} }
} }

View File

@ -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) { func TestDomReadyInitializesNotificationsBeforeScheduler(t *testing.T) {
oldInitialize := initializeNativeNotifications oldInitialize := initializeNativeNotifications
defer func() { initializeNativeNotifications = oldInitialize }() defer func() { initializeNativeNotifications = oldInitialize }()

View File

@ -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...)
}

View File

@ -3,25 +3,16 @@ package plugin
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
"github.com/verstak/verstak-desktop/internal/core/capability" "github.com/verstak/verstak-desktop/internal/core/capability"
) )
// coreCapabilities lists the 5 core capabilities that the platform registers // registerCoreCapabilities registers the desktop's core capabilities on a registry.
// 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.
func registerCoreCapabilities(t *testing.T, reg *capability.Registry) { func registerCoreCapabilities(t *testing.T, reg *capability.Registry) {
t.Helper() 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) 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 // TestLifecycle_MissingRequiredCapability verifies that when the required
// capability is NOT registered, CheckRequired reports it as missing and the // capability is NOT registered, CheckRequired reports it as missing and the
// plugin status should be set to StatusMissingRequiredCapability. // plugin status should be set to StatusMissingRequiredCapability.

13
main.go
View File

@ -86,17 +86,8 @@ func main() {
} }
// ─── Register Core Capabilities ───────────────────────── // ─── Register Core Capabilities ─────────────────────────
corePluginID := "verstak-desktop" corePluginID := capability.CorePluginID
coreCaps := []string{ coreCaps := capability.CorePlatformCapabilities()
"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",
}
if err := capRegistry.Register(corePluginID, coreCaps); err != nil { if err := capRegistry.Register(corePluginID, coreCaps); err != nil {
log.Fatalf("[main] failed to register core capabilities: %v", err) log.Fatalf("[main] failed to register core capabilities: %v", err)
} }

View File

@ -13,6 +13,7 @@ fi
echo "=== verstak desktop Linux amd64 bundle ===" echo "=== verstak desktop Linux amd64 bundle ==="
(cd "$OFFICIAL_PLUGINS" && ./scripts/build.sh) (cd "$OFFICIAL_PLUGINS" && ./scripts/build.sh)
"$ROOT/scripts/install-dev-plugins.sh" "$ROOT/scripts/install-dev-plugins.sh"
(cd "$ROOT" && go test ./internal/core/plugin -run TestBundledOfficialPluginRequirementsResolve -count=1)
"$ROOT/scripts/build.sh" "$ROOT/scripts/build.sh"
BINARY="$ROOT/build/bin/verstak-desktop" BINARY="$ROOT/build/bin/verstak-desktop"

View File

@ -52,6 +52,7 @@ if [[ ! -d "$WINDOWS_PLUGIN_DIST" ]]; then
echo "Windows plugin packages were not produced: $WINDOWS_PLUGIN_DIST" >&2 echo "Windows plugin packages were not produced: $WINDOWS_PLUGIN_DIST" >&2
exit 1 exit 1
fi 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 # Wails' -compiler option selects a Go binary, not a C compiler. Cross-CGO
# therefore has to be supplied through the standard Go environment instead. # therefore has to be supplied through the standard Go environment instead.

View File

@ -41,5 +41,7 @@ grep -Fq 'package-appimage.sh' "$ROOT/scripts/release.sh"
grep -Fq 'package-windows-portable.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 git -C "$ROOT" check-ignore -q verstak-desktop-res.syso
grep -Fq 'chmod -R a+rX' "$ROOT/scripts/build-linux-bundle.sh" 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" echo "desktop package script contracts passed"