1041 lines
31 KiB
Go
1041 lines
31 KiB
Go
// Package api provides Wails-bound methods for the frontend.
|
||
package main
|
||
|
||
import (
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/verstak/verstak-desktop/internal/core/capability"
|
||
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
|
||
"github.com/verstak/verstak-desktop/internal/core/vault"
|
||
"github.com/verstak/verstak-desktop/internal/core/workspace"
|
||
)
|
||
|
||
func main() {
|
||
testEnableDisable := flag.Bool("test-enable-disable", false, "Test enable/disable lifecycle")
|
||
testWorkspace := flag.Bool("test-workspace", false, "Test workspace/cases lifecycle")
|
||
testContributions := flag.Bool("test-contributions", false, "Test contribution registry lifecycle")
|
||
flag.Parse()
|
||
exitCode := 0
|
||
defer func() {
|
||
os.Exit(exitCode)
|
||
}()
|
||
|
||
root, _ := os.Getwd()
|
||
pluginDir := filepath.Join(root, "plugins")
|
||
|
||
if *testContributions {
|
||
runContributionsTest(root)
|
||
return
|
||
}
|
||
|
||
if *testWorkspace {
|
||
runWorkspaceTest(root)
|
||
return
|
||
}
|
||
|
||
if *testEnableDisable {
|
||
runEnableDisableTest(root)
|
||
return
|
||
}
|
||
|
||
fmt.Printf("=== smoke-platform: headless plugin verification ===\n\n")
|
||
fmt.Printf(" plugin dir: %s\n", pluginDir)
|
||
|
||
// ── 1. Discover plugins ──
|
||
fmt.Printf("\n[discovery]\n")
|
||
plugins, errs := plugin.DiscoverPlugins([]string{pluginDir})
|
||
|
||
if len(errs) > 0 {
|
||
for _, e := range errs {
|
||
fmt.Printf(" ⚠️ discovery warning: %v\n", e)
|
||
}
|
||
}
|
||
|
||
if len(plugins) == 0 {
|
||
fmt.Printf(" ❌ no plugins discovered\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
|
||
fmt.Printf(" ✅ discovered %d plugin(s)\n", len(plugins))
|
||
|
||
// ── 2. Find platform-test ──
|
||
fmt.Printf("\n[platform-test lookup]\n")
|
||
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 (id=verstak.platform-test) not found among discovered plugins\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ found: %s\n", target.Manifest.ID)
|
||
|
||
// ── 3. Validate manifest fields ──
|
||
fmt.Printf("\n[manifest validation]\n")
|
||
allGood := true
|
||
m := &target.Manifest
|
||
|
||
checks := []struct {
|
||
name string
|
||
value string
|
||
}{
|
||
{"name", m.Name},
|
||
{"version", m.Version},
|
||
{"apiVersion", m.APIVersion},
|
||
}
|
||
for _, c := range checks {
|
||
if c.value == "" {
|
||
fmt.Printf(" ❌ manifest.%s is empty\n", c.name)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ %s: %s\n", c.name, c.value)
|
||
}
|
||
}
|
||
|
||
if m.SchemaVersion != 1 {
|
||
fmt.Printf(" ❌ schemaVersion: expected 1, got %d\n", m.SchemaVersion)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ schemaVersion: 1\n")
|
||
}
|
||
|
||
// ── 4. provides ──
|
||
fmt.Printf("\n[provides]\n")
|
||
if len(m.Provides) == 0 {
|
||
fmt.Printf(" ❌ provides is empty\n")
|
||
allGood = false
|
||
} else {
|
||
for _, p := range m.Provides {
|
||
fmt.Printf(" ✅ provides: %s\n", p)
|
||
}
|
||
}
|
||
|
||
// ── 5. requires / optionalRequires ──
|
||
fmt.Printf("\n[requires]\n")
|
||
if len(m.Requires) > 0 {
|
||
for _, r := range m.Requires {
|
||
fmt.Printf(" ✅ requires: %s\n", r)
|
||
}
|
||
} else {
|
||
fmt.Printf(" ℹ️ requires: none\n")
|
||
}
|
||
|
||
fmt.Printf("\n[optionalRequires]\n")
|
||
if len(m.OptionalRequires) > 0 {
|
||
for _, r := range m.OptionalRequires {
|
||
fmt.Printf(" ✅ optionalRequires: %s\n", r)
|
||
}
|
||
} else {
|
||
fmt.Printf(" ℹ️ optionalRequires: none\n")
|
||
}
|
||
|
||
// ── 6. contributions ──
|
||
fmt.Printf("\n[contributions]\n")
|
||
if m.Contributes != nil {
|
||
c := m.Contributes
|
||
if len(c.Views) > 0 {
|
||
for _, v := range c.Views {
|
||
fmt.Printf(" ✅ view: %s (%s)\n", v.ID, v.Title)
|
||
}
|
||
}
|
||
if len(c.Commands) > 0 {
|
||
for _, cmd := range c.Commands {
|
||
fmt.Printf(" ✅ command: %s (%s)\n", cmd.ID, cmd.Title)
|
||
}
|
||
}
|
||
if len(c.SidebarItems) > 0 {
|
||
for _, s := range c.SidebarItems {
|
||
fmt.Printf(" ✅ sidebarItem: %s (%s)\n", s.ID, s.Title)
|
||
}
|
||
}
|
||
if len(c.StatusBarItems) > 0 {
|
||
for _, s := range c.StatusBarItems {
|
||
fmt.Printf(" ✅ statusBarItem: %s (%s)\n", s.ID, s.Label)
|
||
}
|
||
}
|
||
if len(c.Views)+len(c.Commands)+len(c.SidebarItems)+len(c.StatusBarItems) == 0 {
|
||
fmt.Printf(" ℹ️ contributes: empty sections only\n")
|
||
}
|
||
} else {
|
||
fmt.Printf(" ℹ️ contributes: none\n")
|
||
}
|
||
|
||
// ── 7. Capability registration (core + plugin) ──
|
||
fmt.Printf("\n[capability registration]\n")
|
||
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",
|
||
}
|
||
if err := reg.Register("verstak-desktop", coreCaps); err != nil {
|
||
fmt.Printf(" ❌ register core capabilities: %v\n", err)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ registered %d core capabilities\n", len(coreCaps))
|
||
}
|
||
|
||
if err := reg.Register("verstak-desktop", []string{"verstak/core/vault/v1"}); err != nil {
|
||
fmt.Printf(" ❌ register vault capability: %v\n", err)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ registered vault capability\n")
|
||
}
|
||
|
||
if err := reg.Register("verstak-desktop", []string{"verstak/core/workspace/v1"}); err != nil {
|
||
fmt.Printf(" ❌ register workspace capability: %v\n", err)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ registered workspace capability\n")
|
||
}
|
||
|
||
for _, p := range m.Provides {
|
||
if err := reg.Register(m.ID, []string{p}); err != nil {
|
||
fmt.Printf(" ❌ register capability %s: %v\n", p, err)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ registered plugin capability: %s\n", p)
|
||
}
|
||
}
|
||
|
||
// ── 8. Verify core capabilities present ──
|
||
fmt.Printf("\n[core capability verification]\n")
|
||
for _, capName := range coreCaps {
|
||
if reg.Has(capName) {
|
||
fmt.Printf(" ✅ core capability present: %s\n", capName)
|
||
} else {
|
||
fmt.Printf(" ❌ core capability MISSING: %s\n", capName)
|
||
allGood = false
|
||
}
|
||
}
|
||
|
||
// ── 9. Verify workspace capability by name ──
|
||
fmt.Printf("\n[workspace capability verification]\n")
|
||
if reg.Has("verstak/core/workspace/v1") {
|
||
fmt.Printf(" ✅ workspace capability present: verstak/core/workspace/v1\n")
|
||
} else {
|
||
fmt.Printf(" ❌ workspace capability MISSING: verstak/core/workspace/v1\n")
|
||
allGood = false
|
||
}
|
||
|
||
// ── 10. Verify required capabilities resolved ──
|
||
fmt.Printf("\n[required capability resolution]\n")
|
||
missingRequired := reg.CheckRequired(m.Requires)
|
||
if len(missingRequired) > 0 {
|
||
for _, miss := range missingRequired {
|
||
fmt.Printf(" ❌ MISSING required: %s\n", miss)
|
||
}
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ all required capabilities resolved\n")
|
||
}
|
||
|
||
// ── 11. Check optional capabilities ──
|
||
fmt.Printf("\n[optional capability resolution]\n")
|
||
missingOptional := reg.CheckRequired(m.OptionalRequires)
|
||
if len(missingOptional) > 0 {
|
||
for _, miss := range missingOptional {
|
||
fmt.Printf(" ⚠️ missing optional (degraded): %s\n", miss)
|
||
}
|
||
}
|
||
|
||
// ── 12. Determine expected status ──
|
||
fmt.Printf("\n[plugin status]\n")
|
||
expectedStatus := "loaded"
|
||
if len(missingOptional) > 0 {
|
||
expectedStatus = "degraded"
|
||
}
|
||
if len(missingRequired) > 0 {
|
||
expectedStatus = "missing-required-capability"
|
||
}
|
||
fmt.Printf(" ℹ️ expected status: %s\n", expectedStatus)
|
||
if expectedStatus == "degraded" {
|
||
fmt.Printf(" ✅ degraded is correct (optional capabilities missing, required OK)\n")
|
||
} else if expectedStatus == "loaded" {
|
||
fmt.Printf(" ✅ loaded is correct (all capabilities resolved)\n")
|
||
} else {
|
||
fmt.Printf(" ❌ unexpected: required capabilities should be resolved\n")
|
||
allGood = false
|
||
}
|
||
|
||
// ── 13. Total capability count ──
|
||
fmt.Printf("\n[capability count]\n")
|
||
totalCaps := len(reg.List())
|
||
fmt.Printf(" total capabilities: %d\n", totalCaps)
|
||
if totalCaps >= 9 {
|
||
fmt.Printf(" ✅ total capabilities >= 9 (%d)\n", totalCaps)
|
||
} else {
|
||
fmt.Printf(" ❌ total capabilities < 9 (got %d, expected >= 9)\n", totalCaps)
|
||
allGood = false
|
||
}
|
||
|
||
// ── 14. Summary ──
|
||
fmt.Printf("\n=== summary ===\n")
|
||
if allGood {
|
||
fmt.Printf("✅ smoke-platform passed\n")
|
||
} else {
|
||
fmt.Printf("❌ smoke-platform failed\n")
|
||
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)
|
||
|
||
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")
|
||
|
||
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)
|
||
|
||
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))
|
||
|
||
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")
|
||
|
||
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))
|
||
|
||
fmt.Printf("\n=== summary ===\n")
|
||
fmt.Printf("✅ enable/disable test passed\n")
|
||
}
|
||
|
||
// runContributionsTest verifies the full manifest → discovery → registry pipeline,
|
||
// mirroring the real ReloadPlugins() flow in internal/api/app.go.
|
||
//
|
||
// Flow:
|
||
// 1. plugin.json on disk → DiscoverPlugins() → parsed manifest
|
||
// 2. Manifest contributions fragment displayed (proof it comes from real plugin.json)
|
||
// 3. Capability registration + resolution → determine plugin status
|
||
// 4. Contribution registration (gated by status: only loaded/degraded get contributions)
|
||
// 5. Verify all 4 contribution types present by name, data matches manifest
|
||
// 6. Disable plugin → Unregister → contributions gone
|
||
// 7. Re-enable → Unregister+Register (ReloadPlugins) → contributions return
|
||
// 8. Reload → Unregister+Register → no duplicates
|
||
func runContributionsTest(root string) {
|
||
exitCode := 0
|
||
defer func() {
|
||
os.Exit(exitCode)
|
||
}()
|
||
|
||
fmt.Printf("=== smoke-platform: contribution registry test ===\n\n")
|
||
|
||
// Create 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)
|
||
|
||
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")
|
||
|
||
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)
|
||
|
||
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")
|
||
|
||
// ── Step 1: Discover plugins from disk (reads real plugin.json) ──
|
||
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: %s@%s\n", target.Manifest.ID, target.Manifest.Version)
|
||
|
||
// ── Step 2: Show manifest contributions fragment (proof it comes from plugin.json) ──
|
||
fmt.Printf("\n[manifest contributions from plugin.json]\n")
|
||
fmt.Printf(" plugin: %s\n", target.Manifest.ID)
|
||
if c := target.Manifest.Contributes; c != nil {
|
||
fmt.Printf(" sidebarItems: %d\n", len(c.SidebarItems))
|
||
for _, s := range c.SidebarItems {
|
||
fmt.Printf(" - id=%q title=%q view=%q\n", s.ID, s.Title, s.View)
|
||
}
|
||
fmt.Printf(" views: %d\n", len(c.Views))
|
||
for _, vw := range c.Views {
|
||
fmt.Printf(" - id=%q title=%q component=%q\n", vw.ID, vw.Title, vw.Component)
|
||
}
|
||
fmt.Printf(" settingsPanels: %d\n", len(c.SettingsPanels))
|
||
for _, s := range c.SettingsPanels {
|
||
fmt.Printf(" - id=%q title=%q component=%q\n", s.ID, s.Title, s.Component)
|
||
}
|
||
fmt.Printf(" commands: %d\n", len(c.Commands))
|
||
for _, cmd := range c.Commands {
|
||
fmt.Printf(" - id=%q title=%q\n", cmd.ID, cmd.Title)
|
||
}
|
||
}
|
||
|
||
// ── Step 2b: Frontend bundle verification ──
|
||
fmt.Printf("\n[frontend bundle verification]\n")
|
||
frontendOk := true
|
||
|
||
// 2b.1 — Check manifest declares frontend.entry
|
||
if target.Manifest.Frontend == nil {
|
||
fmt.Printf(" ❌ manifest.frontend is nil — plugin has no frontend config\n")
|
||
frontendOk = false
|
||
} else {
|
||
expectedEntry := "frontend/dist/index.js"
|
||
if target.Manifest.Frontend.Entry != expectedEntry {
|
||
fmt.Printf(" ❌ frontend.entry: expected %q, got %q\n", expectedEntry, target.Manifest.Frontend.Entry)
|
||
frontendOk = false
|
||
} else {
|
||
fmt.Printf(" ✅ frontend.entry: %s\n", target.Manifest.Frontend.Entry)
|
||
}
|
||
|
||
// 2b.2 — Resolve entry path and check file exists on disk
|
||
resolvedEntry := filepath.Join(target.RootPath, target.Manifest.Frontend.Entry)
|
||
if _, err := os.Stat(resolvedEntry); os.IsNotExist(err) {
|
||
fmt.Printf(" ❌ frontend bundle NOT FOUND at resolved path: %s\n", resolvedEntry)
|
||
frontendOk = false
|
||
} else if err != nil {
|
||
fmt.Printf(" ❌ frontend bundle stat error: %v\n", err)
|
||
frontendOk = false
|
||
} else {
|
||
fmt.Printf(" ✅ frontend bundle exists: %s\n", resolvedEntry)
|
||
|
||
// 2b.3 — Read file and check for "VerstakPluginRegister"
|
||
data, err := os.ReadFile(resolvedEntry)
|
||
if err != nil {
|
||
fmt.Printf(" ❌ failed to read frontend bundle: %v\n", err)
|
||
frontendOk = false
|
||
} else {
|
||
content := string(data)
|
||
|
||
if strings.Contains(content, "VerstakPluginRegister") {
|
||
fmt.Printf(" ✅ bundle contains VerstakPluginRegister contract\n")
|
||
} else {
|
||
fmt.Printf(" ❌ bundle MISSING VerstakPluginRegister contract\n")
|
||
frontendOk = false
|
||
}
|
||
|
||
// 2b.4 — Check for expected component names
|
||
expectedComponents := []string{"DiagnosticsPanel", "PlatformTestSettings"}
|
||
for _, comp := range expectedComponents {
|
||
if strings.Contains(content, comp) {
|
||
fmt.Printf(" ✅ component registered: %s\n", comp)
|
||
} else {
|
||
fmt.Printf(" ❌ component MISSING from bundle: %s\n", comp)
|
||
frontendOk = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if frontendOk {
|
||
fmt.Printf(" ✅ frontend bundle checks passed\n")
|
||
} else {
|
||
fmt.Printf(" ❌ frontend bundle checks FAILED\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
|
||
// ── Step 3: Register capabilities (simulates main.go + ReloadPlugins) ──
|
||
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 _, capID := range target.Manifest.Provides {
|
||
_ = reg.Register(target.Manifest.ID, []string{capID})
|
||
}
|
||
fmt.Printf("\n[capability registration]\n")
|
||
fmt.Printf(" registered %d capabilities\n", len(reg.List()))
|
||
|
||
// ── Step 4: ReloadPlugins flow with capability gating ──
|
||
fmt.Printf("\n[register contributions (ReloadPlugins flow)]\n")
|
||
contribReg := contribution.NewRegistry()
|
||
|
||
// (a) Check disabled → skip
|
||
if target.Status == plugin.StatusDisabled {
|
||
fmt.Printf(" ❌ plugin is disabled — contributions should NOT be registered\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
|
||
// (b) Capability resolution (same as ReloadPlugins)
|
||
missingRequired := reg.CheckRequired(target.Manifest.Requires)
|
||
missingOptional := reg.CheckRequired(target.Manifest.OptionalRequires)
|
||
status := plugin.StatusLoaded
|
||
if len(missingRequired) > 0 {
|
||
status = plugin.StatusMissingRequiredCapability
|
||
} else if len(missingOptional) > 0 {
|
||
status = plugin.StatusDegraded
|
||
}
|
||
|
||
// (c) Only loaded/degraded get contributions
|
||
if status == plugin.StatusLoaded || status == plugin.StatusDegraded {
|
||
fmt.Printf(" plugin status=%s → contributions WILL be registered\n", status)
|
||
contribReg.Register(target.Manifest.ID, target.Manifest.Contributes)
|
||
fmt.Printf(" contributions registered for %s\n", target.Manifest.ID)
|
||
} else {
|
||
fmt.Printf(" ❌ plugin status=%s — contributions should NOT be registered\n", status)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
|
||
// ── Step 5: Verify contributions by name, data matches manifest ──
|
||
fmt.Printf("\n[verify contributions by name]\n")
|
||
allGood := true
|
||
|
||
sidebarItems := contribReg.SidebarItems()
|
||
if len(sidebarItems) != 1 || sidebarItems[0].Item.Title != "Platform Test" {
|
||
fmt.Printf(" ❌ sidebarItems mismatch: got %v\n", sidebarItems)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ sidebarItem: plugin=%s id=%s title=%s\n",
|
||
sidebarItems[0].PluginID, sidebarItems[0].Item.ID, sidebarItems[0].Item.Title)
|
||
}
|
||
|
||
views := contribReg.Views()
|
||
if len(views) != 1 || views[0].Item.Title != "Platform Diagnostics" {
|
||
fmt.Printf(" ❌ views mismatch: got %v\n", views)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ view: plugin=%s id=%s title=%s component=%s\n",
|
||
views[0].PluginID, views[0].Item.ID, views[0].Item.Title, views[0].Item.Component)
|
||
}
|
||
|
||
settingsPanels := contribReg.SettingsPanels()
|
||
if len(settingsPanels) != 1 || settingsPanels[0].Item.Title != "Platform Test Settings" {
|
||
fmt.Printf(" ❌ settingsPanels mismatch: got %v\n", settingsPanels)
|
||
allGood = false
|
||
} else {
|
||
fmt.Printf(" ✅ settingsPanel: plugin=%s id=%s title=%s\n",
|
||
settingsPanels[0].PluginID, settingsPanels[0].Item.ID, settingsPanels[0].Item.Title)
|
||
}
|
||
|
||
commands := contribReg.Commands()
|
||
if len(commands) != 2 {
|
||
fmt.Printf(" ❌ expected 2 commands, got %d\n", len(commands))
|
||
allGood = false
|
||
} else {
|
||
for _, c := range commands {
|
||
fmt.Printf(" command: plugin=%s id=%s title=%s\n", c.PluginID, c.Item.ID, c.Item.Title)
|
||
}
|
||
}
|
||
|
||
if !allGood {
|
||
fmt.Printf(" ❌ some contribution types missing or data mismatch\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ all 4 contribution types present, data matches manifest\n")
|
||
|
||
// ── Step 6: Disable → Unregister → contributions removed ──
|
||
fmt.Printf("\n[disable plugin → Unregister → contributions removed]\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")
|
||
|
||
// ReloadPlugins: Unregister before next scan, disabled plugins don't get re-registered
|
||
contribReg.Unregister("verstak.platform-test")
|
||
remaining := len(contribReg.Views()) + len(contribReg.SidebarItems()) +
|
||
len(contribReg.Commands()) + len(contribReg.SettingsPanels())
|
||
fmt.Printf(" remaining contributions: %d\n", remaining)
|
||
if remaining != 0 {
|
||
fmt.Printf(" ❌ contributions not removed after disable\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ all contributions removed\n")
|
||
|
||
// ── Step 7: Re-enable → contributions return via Unregister+Register ──
|
||
fmt.Printf("\n[re-enable → Unregister+Register → contributions return]\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")
|
||
|
||
// ReloadPlugins: Unregister before Register (prevents duplicates)
|
||
contribReg.Unregister(target.Manifest.ID)
|
||
contribReg.Register(target.Manifest.ID, target.Manifest.Contributes)
|
||
count := len(contribReg.Views()) + len(contribReg.SidebarItems()) +
|
||
len(contribReg.Commands()) + len(contribReg.SettingsPanels())
|
||
fmt.Printf(" contributions after re-enable: %d\n", count)
|
||
if count < 4 {
|
||
fmt.Printf(" ❌ expected >= 4 contributions, got %d\n", count)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ contributions returned (count=%d)\n", count)
|
||
|
||
// ── Step 8: Reload → Unregister+Register → no duplicates ──
|
||
fmt.Printf("\n[reload → Unregister+Register → no duplicates]\n")
|
||
beforeCount := count
|
||
contribReg.Unregister(target.Manifest.ID)
|
||
contribReg.Register(target.Manifest.ID, target.Manifest.Contributes)
|
||
afterCount := len(contribReg.Views()) + len(contribReg.SidebarItems()) +
|
||
len(contribReg.Commands()) + len(contribReg.SettingsPanels())
|
||
fmt.Printf(" before reload: %d, after reload: %d\n", beforeCount, afterCount)
|
||
if afterCount != beforeCount {
|
||
fmt.Printf(" ❌ duplicate after reload: before=%d after=%d\n", beforeCount, afterCount)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ no duplicate contributions after reload (count=%d)\n", afterCount)
|
||
|
||
fmt.Printf("\n=== summary ===\n")
|
||
if exitCode == 0 {
|
||
fmt.Printf("✅ contribution registry test passed\n")
|
||
} else {
|
||
fmt.Printf("❌ contribution registry test failed\n")
|
||
}
|
||
}
|
||
|
||
func runWorkspaceTest(root string) {
|
||
exitCode := 0
|
||
defer func() {
|
||
os.Exit(exitCode)
|
||
}()
|
||
|
||
fmt.Printf("=== smoke-platform: workspace test ===\n\n")
|
||
|
||
tmpDir, err := os.MkdirTemp("", "verstak-ws-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)
|
||
|
||
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")
|
||
|
||
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)
|
||
|
||
fmt.Printf("\n[workspace init]\n")
|
||
ws := workspace.NewManager(openedPath)
|
||
if err := ws.Load(); err != nil {
|
||
fmt.Printf(" ❌ load workspace: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ workspace loaded\n")
|
||
|
||
tree := ws.GetTree()
|
||
if len(tree.Nodes) != 1 {
|
||
fmt.Printf(" ❌ expected 1 root node, got %d\n", len(tree.Nodes))
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ root node exists (id=%s)\n", tree.Nodes[0].ID)
|
||
rootID := tree.Nodes[0].ID
|
||
|
||
fmt.Printf("\n[workspace capability]\n")
|
||
reg := capability.NewRegistry()
|
||
reg.Register("verstak-desktop", []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", []string{"verstak/core/vault/v1"})
|
||
reg.Register("verstak-desktop", []string{"verstak/core/workspace/v1"})
|
||
if !reg.Has("verstak/core/workspace/v1") {
|
||
fmt.Printf(" ❌ workspace capability not registered\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ workspace capability registered\n")
|
||
totalCaps := len(reg.List())
|
||
if totalCaps < 7 {
|
||
fmt.Printf(" ❌ expected >= 7 capabilities, got %d\n", totalCaps)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ total capabilities >= 7 (%d)\n", totalCaps)
|
||
|
||
fmt.Printf("\n[create case]\n")
|
||
caseNode, err := ws.CreateNode(rootID, workspace.TypeCase, "Test Case")
|
||
if err != nil {
|
||
fmt.Printf(" ❌ create case: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ case created: %s\n", caseNode.Title)
|
||
|
||
fmt.Printf("\n[create folder]\n")
|
||
folderNode, err := ws.CreateNode(rootID, workspace.TypeFolder, "Test Folder")
|
||
if err != nil {
|
||
fmt.Printf(" ❌ create folder: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ folder created: %s\n", folderNode.Title)
|
||
|
||
fmt.Printf("\n[create nested case]\n")
|
||
nestedCase, err := ws.CreateNode(folderNode.ID, workspace.TypeCase, "Nested Case")
|
||
if err != nil {
|
||
fmt.Printf(" ❌ create nested case: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ nested case created: %s\n", nestedCase.Title)
|
||
|
||
fmt.Printf("\n[tree structure]\n")
|
||
tree = ws.GetTree()
|
||
if len(tree.Nodes) != 4 {
|
||
fmt.Printf(" ❌ expected 4 nodes, got %d\n", len(tree.Nodes))
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ tree has 4 nodes\n")
|
||
children := ws.ListChildren(rootID)
|
||
if len(children) != 2 {
|
||
fmt.Printf(" ❌ expected 2 root children, got %d\n", len(children))
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ root has 2 children\n")
|
||
|
||
fmt.Printf("\n[rename]\n")
|
||
if err := ws.RenameNode(caseNode.ID, "Renamed Case"); err != nil {
|
||
fmt.Printf(" ❌ rename: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
renamed, _ := ws.GetNode(caseNode.ID)
|
||
if renamed.Title != "Renamed Case" {
|
||
fmt.Printf(" ❌ rename failed: got %q\n", renamed.Title)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ renamed to %q\n", renamed.Title)
|
||
|
||
fmt.Printf("\n[set current node]\n")
|
||
if err := ws.SetCurrentNode(caseNode.ID); err != nil {
|
||
fmt.Printf(" ❌ set current: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
current, err := ws.GetCurrentNode()
|
||
if err != nil {
|
||
fmt.Printf(" ❌ get current: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
if current.ID != caseNode.ID {
|
||
fmt.Printf(" ❌ current node mismatch\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ current node: %s\n", current.Title)
|
||
|
||
fmt.Printf("\n[archive]\n")
|
||
if err := ws.ArchiveNode(folderNode.ID); err != nil {
|
||
fmt.Printf(" ❌ archive: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
archived, _ := ws.GetNode(folderNode.ID)
|
||
if archived.Status != workspace.StatusArchived {
|
||
fmt.Printf(" ❌ archive failed: status=%s\n", archived.Status)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ archived %s\n", archived.Title)
|
||
|
||
fmt.Printf("\n[reopen persistence]\n")
|
||
ws2 := workspace.NewManager(openedPath)
|
||
if err := ws2.Load(); err != nil {
|
||
fmt.Printf(" ❌ reopen: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
tree2 := ws2.GetTree()
|
||
if len(tree2.Nodes) != 4 {
|
||
fmt.Printf(" ❌ expected 4 nodes after reopen, got %d\n", len(tree2.Nodes))
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ tree persisted: %d nodes\n", len(tree2.Nodes))
|
||
current2, err := ws2.GetCurrentNode()
|
||
if err != nil {
|
||
fmt.Printf(" ❌ get current after reopen: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
if current2.ID != caseNode.ID {
|
||
fmt.Printf(" ❌ current node not persisted\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ current node persisted\n")
|
||
|
||
fmt.Printf("\n[workspace.json verification]\n")
|
||
wsPath := filepath.Join(openedPath, ".verstak", "workspace.json")
|
||
wsData, err := os.ReadFile(wsPath)
|
||
if err != nil {
|
||
fmt.Printf(" ❌ read workspace.json: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ workspace.json exists on disk\n")
|
||
fmt.Printf(" content:\n%s\n", string(wsData))
|
||
|
||
// ── Test 4-level deep tree ──
|
||
fmt.Printf("\n[4-level deep tree]\n")
|
||
folder1, err := ws.CreateNode(rootID, workspace.TypeFolder, "Level 1 Folder")
|
||
if err != nil {
|
||
fmt.Printf(" ❌ create folder1: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ created: %s\n", folder1.Title)
|
||
|
||
folder2, err := ws.CreateNode(folder1.ID, workspace.TypeFolder, "Level 2 Folder")
|
||
if err != nil {
|
||
fmt.Printf(" ❌ create folder2: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ created: %s\n", folder2.Title)
|
||
|
||
deepCase, err := ws.CreateNode(folder2.ID, workspace.TypeCase, "Deep Case")
|
||
if err != nil {
|
||
fmt.Printf(" ❌ create deep case: %v\n", err)
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ created: %s (depth 4)\n", deepCase.Title)
|
||
|
||
tree = ws.GetTree()
|
||
if len(tree.Nodes) != 7 {
|
||
fmt.Printf(" ❌ expected 7 nodes, got %d\n", len(tree.Nodes))
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ tree has 7 nodes (4 levels deep)\n")
|
||
|
||
deepNode, _ := ws.GetNode(deepCase.ID)
|
||
if deepNode.ParentID != folder2.ID {
|
||
fmt.Printf(" ❌ deep case parent mismatch\n")
|
||
exitCode = 1
|
||
return
|
||
}
|
||
fmt.Printf(" ✅ deep case parent chain correct\n")
|
||
|
||
fmt.Printf("\n=== summary ===\n")
|
||
fmt.Printf("✅ workspace test passed\n")
|
||
}
|