feat(m4b): add vault selection UI, enable/disable toggle, missing-installed UI

- Add VaultSelection.svelte: first-run vault create/open/recent UI
- Update App.svelte: vault check on startup, show VaultSelection when needed
- Update PluginCard.svelte: enable/disable buttons, vault state awareness
- Update PluginManager.svelte: enable/disable handlers, missing-installed section
- Add SetCurrentVault Wails API binding
- Add RecordDesiredPlugin Wails API binding
- Record desired plugins on discovery (only when vault open)
- Fix addRecent: remove duplicate sort, clean up unused import
- Update smoke-platform.sh: enable/disable lifecycle test
- Add runEnableDisableTest: vault create/open, disable/enable, plugins.json verify
This commit is contained in:
2026-06-17 04:19:13 +08:00
parent c8d2560bb2
commit a6f9e85f13
9 changed files with 829 additions and 77 deletions
+178
View File
@@ -3,15 +3,20 @@
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"github.com/verstak/verstak-desktop/internal/core/capability"
"github.com/verstak/verstak-desktop/internal/core/plugin"
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
"github.com/verstak/verstak-desktop/internal/core/vault"
)
func main() {
testEnableDisable := flag.Bool("test-enable-disable", false, "Test enable/disable lifecycle")
flag.Parse()
exitCode := 0
defer func() {
os.Exit(exitCode)
@@ -20,6 +25,11 @@ func main() {
root, _ := os.Getwd()
pluginDir := filepath.Join(root, "plugins")
if *testEnableDisable {
runEnableDisableTest(root)
return
}
fmt.Printf("=== smoke-platform: headless plugin verification ===\n\n")
fmt.Printf(" plugin dir: %s\n", pluginDir)
@@ -256,3 +266,171 @@ func main() {
exitCode = 1
}
}
// runEnableDisableTest tests the enable/disable lifecycle with vault plugin state.
func runEnableDisableTest(root string) {
exitCode := 0
defer func() {
os.Exit(exitCode)
}()
fmt.Printf("=== smoke-platform: enable/disable test ===\n\n")
// Create a temp vault
tmpDir, err := os.MkdirTemp("", "verstak-smoke-*")
if err != nil {
fmt.Printf(" ❌ failed to create temp dir: %v\n", err)
exitCode = 1
return
}
defer os.RemoveAll(tmpDir)
vaultPath := filepath.Join(tmpDir, "testvault")
fmt.Printf(" vault path: %s\n", vaultPath)
// Initialize vault
v := vault.NewVault(nil)
if err := v.CreateVault(vaultPath); err != nil {
fmt.Printf(" ❌ create vault: %v\n", err)
exitCode = 1
return
}
fmt.Printf(" ✅ vault created\n")
// Open the vault at the path returned by CreateVault (path/VerstakVault)
openedPath := v.GetVaultPath()
if err := v.OpenVault(openedPath); err != nil {
fmt.Printf(" ❌ open vault: %v\n", err)
exitCode = 1
return
}
fmt.Printf(" ✅ vault opened at %s\n", openedPath)
// Initialize plugin state
psm := pluginstate.NewManager(v)
if err := psm.Load(); err != nil {
fmt.Printf(" ❌ load plugin state: %v\n", err)
exitCode = 1
return
}
fmt.Printf(" ✅ plugin state loaded\n")
// Discover plugins
pluginDir := filepath.Join(root, "plugins")
plugins, _ := plugin.DiscoverPlugins([]string{pluginDir})
if len(plugins) == 0 {
fmt.Printf(" ❌ no plugins discovered\n")
exitCode = 1
return
}
fmt.Printf(" ✅ discovered %d plugin(s)\n", len(plugins))
// Find platform-test
var target *plugin.Plugin
for i, p := range plugins {
if p.Manifest.ID == "verstak.platform-test" {
target = &plugins[i]
break
}
}
if target == nil {
fmt.Printf(" ❌ platform-test not found\n")
exitCode = 1
return
}
fmt.Printf(" ✅ platform-test found\n")
// Register capabilities
reg := capability.NewRegistry()
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",
}
reg.Register("verstak-desktop", coreCaps)
reg.Register("verstak-desktop", []string{"verstak/core/vault/v1"})
for _, cap := range target.Manifest.Provides {
reg.Register(target.Manifest.ID, []string{cap})
}
totalCaps := len(reg.List())
fmt.Printf(" ✅ registered %d capabilities (core + plugin)\n", totalCaps)
// ── Test 1: Disable platform-test ──
fmt.Printf("\n[disable]\n")
if err := psm.DisablePlugin("verstak.platform-test"); err != nil {
fmt.Printf(" ❌ disable: %v\n", err)
exitCode = 1
return
}
fmt.Printf(" ✅ disabled platform-test\n")
if !psm.IsDisabled("verstak.platform-test") {
fmt.Printf(" ❌ IsDisabled returned false after disable\n")
exitCode = 1
return
}
fmt.Printf(" ✅ IsDisabled: true\n")
if psm.IsEnabled("verstak.platform-test") {
fmt.Printf(" ❌ IsEnabled returned true after disable\n")
exitCode = 1
return
}
fmt.Printf(" ✅ IsEnabled: false\n")
// Check plugins.json
state := psm.Get()
found := false
for _, dp := range state.DesiredPlugins {
if dp.ID == "verstak.platform-test" {
found = true
break
}
}
if found {
fmt.Printf(" ✅ platform-test in desiredPlugins\n")
} else {
fmt.Printf(" ️ platform-test not in desiredPlugins (ok if not recorded)\n")
}
// ── Test 2: Enable platform-test ──
fmt.Printf("\n[enable]\n")
if err := psm.EnablePlugin("verstak.platform-test"); err != nil {
fmt.Printf(" ❌ enable: %v\n", err)
exitCode = 1
return
}
fmt.Printf(" ✅ enabled platform-test\n")
if !psm.IsEnabled("verstak.platform-test") {
fmt.Printf(" ❌ IsEnabled returned false after enable\n")
exitCode = 1
return
}
fmt.Printf(" ✅ IsEnabled: true\n")
if psm.IsDisabled("verstak.platform-test") {
fmt.Printf(" ❌ IsDisabled returned true after enable\n")
exitCode = 1
return
}
fmt.Printf(" ✅ IsDisabled: false\n")
// ── Test 3: Verify plugins.json on disk ──
fmt.Printf("\n[plugins.json verification]\n")
statePath := filepath.Join(v.GetVaultPath(), ".verstak", "plugins.json")
data, err := os.ReadFile(statePath)
if err != nil {
fmt.Printf(" ❌ read plugins.json: %v\n", err)
exitCode = 1
return
}
fmt.Printf(" ✅ plugins.json exists on disk\n")
fmt.Printf(" content:\n%s\n", string(data))
// ── Summary ──
fmt.Printf("\n=== summary ===\n")
fmt.Printf("✅ enable/disable test passed\n")
}