fix: validate bundled plugin requirements

This commit is contained in:
2026-07-14 19:04:03 +08:00
parent 680846da72
commit fc82b3e6b1
8 changed files with 85 additions and 36 deletions
+6 -13
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)
}
}
+13
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 }()
+20
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...)
}
+40 -12
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.