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

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) {
oldInitialize := initializeNativeNotifications
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 (
"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.

13
main.go
View File

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

View File

@ -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"

View File

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

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"
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"