feat: plugin install/uninstall lifecycle + UI buttons
- AppConfig: add InstalledPlugins []string - Manager.Discover(): no config dependency, all plugins start inactive - Manager.SyncConfig(): apply installed/enabled state from AppConfig - Manager.Enable(): works for plugins without on_install hook - Manager.Install/Uninstall(): run on_install/on_uninstall hooks - ActivatePlugin: skip if HasInstall && !Installed - ReloadPlugins: Discover → SyncConfig → InitRuntimes - Bindings: InstallPlugin, UninstallPlugin - SettingsPlugins: install/uninstall buttons, toggle only after install - Calendar: migration moved from on_init to on_install, on_uninstall drops tables - Tests: all 12 pass (manager + runtime + calendar)
This commit is contained in:
parent
b80941f908
commit
e99ff984b1
|
|
@ -18,6 +18,8 @@ type PluginDTO struct {
|
|||
Author string `json:"author,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
Installed bool `json:"installed"`
|
||||
HasInstall bool `json:"hasInstall"`
|
||||
HasPanel bool `json:"hasPanel"`
|
||||
HasSettings bool `json:"hasSettings"`
|
||||
UIContribs UIContribDTO `json:"uiContribs"`
|
||||
|
|
@ -89,6 +91,8 @@ func (a *App) ListPlugins() []PluginDTO {
|
|||
Author: p.Meta.Author,
|
||||
Description: p.Meta.Description,
|
||||
Active: active,
|
||||
Installed: p.Installed,
|
||||
HasInstall: p.HasInstall,
|
||||
HasPanel: hasPanel,
|
||||
UIContribs: contribs,
|
||||
})
|
||||
|
|
@ -97,11 +101,22 @@ func (a *App) ListPlugins() []PluginDTO {
|
|||
}
|
||||
|
||||
// SetPluginEnabled persists the enabled/disabled state and applies it to the runtime.
|
||||
// Returns error if the plugin is not installed but has install lifecycle.
|
||||
func (a *App) SetPluginEnabled(name string, enabled bool) error {
|
||||
if a.plugins == nil {
|
||||
return fmt.Errorf("plugin manager not ready")
|
||||
}
|
||||
|
||||
if enabled {
|
||||
if err := a.plugins.Enable(name); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := a.plugins.Disable(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
appCfg = config.DefaultAppConfig()
|
||||
|
|
@ -207,26 +222,39 @@ func (a *App) ReloadPlugins() error {
|
|||
log.Print("[plugins] reload requested")
|
||||
a.plugins.CloseRuntimes()
|
||||
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
appCfg = config.DefaultAppConfig()
|
||||
}
|
||||
enabledSet := make(map[string]bool)
|
||||
for _, name := range appCfg.EnabledPlugins {
|
||||
enabledSet[name] = true
|
||||
}
|
||||
a.plugins.Discover()
|
||||
a.plugins.InitRuntimes()
|
||||
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
a.plugins.SyncConfig(appCfg)
|
||||
|
||||
// Apply enable/disable state from config: deactivate everything not in enabled set
|
||||
for _, p := range a.plugins.Plugins() {
|
||||
if !enabledSet[p.Meta.Name] {
|
||||
if !p.Active || !p.Installed {
|
||||
a.plugins.DeactivatePlugin(p.Meta.Name)
|
||||
}
|
||||
}
|
||||
|
||||
a.plugins.InitRuntimes()
|
||||
a.plugins.CallInitHooks()
|
||||
a.plugins.StartSchedulers()
|
||||
log.Print("[plugins] reload complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallPlugin creates plugin's database tables and defaults via on_install hook.
|
||||
// Does NOT activate the plugin — use SetPluginEnabled after.
|
||||
func (a *App) InstallPlugin(name string) error {
|
||||
if a.plugins == nil {
|
||||
return fmt.Errorf("plugin manager not ready")
|
||||
}
|
||||
return a.plugins.Install(name)
|
||||
}
|
||||
|
||||
// UninstallPlugin drops plugin's database tables and cleans data via on_uninstall hook.
|
||||
// Disables the plugin first if active. Does NOT delete plugin files from disk.
|
||||
func (a *App) UninstallPlugin(name string) error {
|
||||
if a.plugins == nil {
|
||||
return fmt.Errorf("plugin manager not ready")
|
||||
}
|
||||
return a.plugins.Uninstall(name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,10 +649,9 @@ end
|
|||
-- Hooks
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function on_init()
|
||||
print("Calendar: on_init — running migration")
|
||||
function on_install()
|
||||
print("Calendar: on_install — creating tables")
|
||||
|
||||
-- Run migration SQL
|
||||
local ok, err = pcall(function()
|
||||
verstak.db.exec([[
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
|
|
@ -696,13 +695,40 @@ function on_init()
|
|||
|
||||
if not ok then
|
||||
print("Calendar: migration error: " .. tostring(err))
|
||||
error(err)
|
||||
else
|
||||
print("Calendar: migration complete")
|
||||
end
|
||||
|
||||
-- Ensure default categories
|
||||
-- Insert default categories
|
||||
M.ensure_categories()
|
||||
|
||||
print("Calendar: install complete")
|
||||
end
|
||||
|
||||
function on_uninstall()
|
||||
print("Calendar: on_uninstall — dropping tables")
|
||||
|
||||
local ok, err = pcall(function()
|
||||
verstak.db.exec("DROP TABLE IF EXISTS events")
|
||||
verstak.db.exec("DROP TABLE IF EXISTS categories")
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
print("Calendar: uninstall error: " .. tostring(err))
|
||||
else
|
||||
print("Calendar: tables dropped")
|
||||
end
|
||||
|
||||
-- Clean up config
|
||||
pcall(verstak.config.set, "categories", nil)
|
||||
|
||||
print("Calendar: uninstall complete")
|
||||
end
|
||||
|
||||
function on_init()
|
||||
print("Calendar: on_init — registering API")
|
||||
|
||||
-- Register global API for panel access
|
||||
_G.calendar = M
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
"hooks": {
|
||||
"on_init": "on_init",
|
||||
"on_shutdown": "on_shutdown"
|
||||
"on_shutdown": "on_shutdown",
|
||||
"on_install": "on_install",
|
||||
"on_uninstall": "on_uninstall"
|
||||
},
|
||||
|
||||
"ui": {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function install(p) {
|
||||
error = ''
|
||||
try {
|
||||
await wailsCall('InstallPlugin', p.name)
|
||||
p.installed = true
|
||||
// Refresh list to reflect new state
|
||||
plugins = await wailsCall('ListPlugins') || []
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstall(p) {
|
||||
if (!confirm(`Удалить плагин «${p.name}»? Все данные плагина будут очищены. Файлы плагина сохранятся.`)) return
|
||||
error = ''
|
||||
try {
|
||||
await wailsCall('UninstallPlugin', p.name)
|
||||
p.installed = false
|
||||
p.active = false
|
||||
plugins = await wailsCall('ListPlugins') || []
|
||||
} catch (e) {
|
||||
error = String(e)
|
||||
}
|
||||
}
|
||||
|
||||
$: sidebarCount = plugins.reduce((n, p) => n + (p.uiContribs?.sidebarItems?.length || 0), 0)
|
||||
$: tabCount = plugins.reduce((n, p) => n + (p.uiContribs?.nodeTabs?.length || 0), 0)
|
||||
</script>
|
||||
|
|
@ -82,15 +107,36 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="plugin-toggle">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
class:active={p.active}
|
||||
on:click={() => toggle(p)}
|
||||
role="switch"
|
||||
aria-checked={p.active}
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
{#if p.hasInstall && !p.installed}
|
||||
<button class="install-btn" on:click={() => install(p)}>
|
||||
📦 Установить
|
||||
</button>
|
||||
{:else if p.hasInstall && p.installed}
|
||||
<div class="toggle-group">
|
||||
<button
|
||||
class="toggle-btn"
|
||||
class:active={p.active}
|
||||
on:click={() => toggle(p)}
|
||||
role="switch"
|
||||
aria-checked={p.active}
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
<button class="uninstall-btn btn-sm" on:click={() => uninstall(p)} title="Удалить плагин (очистить данные)">
|
||||
🗑
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="toggle-btn"
|
||||
class:active={p.active}
|
||||
on:click={() => toggle(p)}
|
||||
role="switch"
|
||||
aria-checked={p.active}
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
@ -201,4 +247,42 @@
|
|||
.toggle-btn.active .toggle-knob {
|
||||
left: 22px;
|
||||
}
|
||||
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.install-btn {
|
||||
padding: 6px 14px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.install-btn:hover { filter: brightness(1.15); }
|
||||
|
||||
.uninstall-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--surface, #1a1a2e);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239,68,68,0.3);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.6;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.uninstall-btn:hover {
|
||||
opacity: 1;
|
||||
background: rgba(239,68,68,0.1);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type AppConfig struct {
|
|||
Language string `json:"language"`
|
||||
EnabledTemplates []string `json:"enabled_templates"`
|
||||
EnabledPlugins []string `json:"enabled_plugins"`
|
||||
InstalledPlugins []string `json:"installed_plugins"`
|
||||
FirstRunCompleted bool `json:"first_run_completed"`
|
||||
Window WindowConfig `json:"window,omitempty"`
|
||||
Vault VaultAppConfig `json:"vault,omitempty"`
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package plugins
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"verstak/internal/core/config"
|
||||
)
|
||||
|
||||
// Meta is the plugin.json descriptor.
|
||||
|
|
@ -75,6 +77,8 @@ type Plugin struct {
|
|||
Dir string // absolute path to plugin directory
|
||||
DataDir string // .verstak/plugins/<name>/data — plugin's own SQLite storage
|
||||
Active bool
|
||||
Installed bool
|
||||
HasInstall bool
|
||||
|
||||
// Runtime (set after InitRuntime)
|
||||
vm *LuaVM
|
||||
|
|
@ -96,6 +100,8 @@ func NewManager(vaultRoot string) *Manager {
|
|||
}
|
||||
|
||||
// Discover scans .verstak/plugins/* for plugin.json files.
|
||||
// Sets Installed=true for all plugins (they need Install call to set up DB).
|
||||
// Active is always false after Discover — call SyncConfig or Enable to activate.
|
||||
func (m *Manager) Discover() {
|
||||
pluginsDir := filepath.Join(m.vaultRoot, ".verstak", "plugins")
|
||||
entries, err := os.ReadDir(pluginsDir)
|
||||
|
|
@ -120,15 +126,44 @@ func (m *Manager) Discover() {
|
|||
if meta.Name == "" {
|
||||
meta.Name = e.Name()
|
||||
}
|
||||
dataDir := filepath.Join(pluginsDir, e.Name(), "data")
|
||||
os.MkdirAll(dataDir, 0o750)
|
||||
dataDir := filepath.Join(pluginsDir, e.Name(), "data")
|
||||
os.MkdirAll(dataDir, 0o750)
|
||||
|
||||
m.plugins = append(m.plugins, Plugin{
|
||||
Meta: meta,
|
||||
Dir: filepath.Join(pluginsDir, e.Name()),
|
||||
DataDir: dataDir,
|
||||
Active: true,
|
||||
})
|
||||
hasInstall := meta.Hooks["on_install"] != ""
|
||||
|
||||
m.plugins = append(m.plugins, Plugin{
|
||||
Meta: meta,
|
||||
Dir: filepath.Join(pluginsDir, e.Name()),
|
||||
DataDir: dataDir,
|
||||
Active: false,
|
||||
Installed: false,
|
||||
HasInstall: hasInstall,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SyncConfig applies installed and enabled states from AppConfig.
|
||||
// Call after Discover() and before InitRuntimes().
|
||||
func (m *Manager) SyncConfig(cfg *config.AppConfig) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
installedSet := make(map[string]bool)
|
||||
enabledSet := make(map[string]bool)
|
||||
for _, name := range cfg.InstalledPlugins {
|
||||
installedSet[name] = true
|
||||
}
|
||||
for _, name := range cfg.EnabledPlugins {
|
||||
enabledSet[name] = true
|
||||
}
|
||||
for i := range m.plugins {
|
||||
installed := installedSet[m.plugins[i].Meta.Name]
|
||||
// Plugins without on_install hook are always "installed"
|
||||
if !m.plugins[i].HasInstall {
|
||||
installed = true
|
||||
}
|
||||
m.plugins[i].Installed = installed
|
||||
m.plugins[i].Active = installed && enabledSet[m.plugins[i].Meta.Name]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,23 +353,177 @@ type NodeMeta struct {
|
|||
}
|
||||
|
||||
// Enable activates a plugin by name.
|
||||
func (m *Manager) Enable(name string) {
|
||||
// If the plugin has on_install hook, it must be installed first.
|
||||
// Plugins without on_install hook are always considered "installed".
|
||||
func (m *Manager) Enable(name string) error {
|
||||
for i := range m.plugins {
|
||||
if m.plugins[i].Meta.Name == name {
|
||||
m.plugins[i].Active = true
|
||||
return
|
||||
p := &m.plugins[i]
|
||||
if p.HasInstall && !p.Installed {
|
||||
return fmt.Errorf("plugin %q must be installed first (use Install)", name)
|
||||
}
|
||||
p.Active = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("plugin %q not found", name)
|
||||
}
|
||||
|
||||
// Disable deactivates a plugin by name.
|
||||
func (m *Manager) Disable(name string) {
|
||||
func (m *Manager) Disable(name string) error {
|
||||
for i := range m.plugins {
|
||||
if m.plugins[i].Meta.Name == name {
|
||||
m.plugins[i].Active = false
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("plugin %q not found", name)
|
||||
}
|
||||
|
||||
// IsInstalled returns true if a plugin is marked installed.
|
||||
func (m *Manager) IsInstalled(name string) bool {
|
||||
for _, p := range m.plugins {
|
||||
if p.Meta.Name == name {
|
||||
return p.Installed
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Install runs a plugin's on_install hook (creates tables, defaults, etc.)
|
||||
// and marks it as installed in the config.
|
||||
func (m *Manager) Install(name string) error {
|
||||
for i := range m.plugins {
|
||||
if m.plugins[i].Meta.Name != name {
|
||||
continue
|
||||
}
|
||||
p := &m.plugins[i]
|
||||
if p.Installed {
|
||||
return fmt.Errorf("plugin %q is already installed", name)
|
||||
}
|
||||
hookName := p.Meta.Hooks["on_install"]
|
||||
if hookName == "" {
|
||||
return fmt.Errorf("plugin %q does not support install lifecycle", name)
|
||||
}
|
||||
|
||||
// Create a temporary VM to run on_install
|
||||
vm, err := NewLuaVM(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create VM: %w", err)
|
||||
}
|
||||
defer vm.Close()
|
||||
if m.Services != nil {
|
||||
vm.SetServices(m.Services)
|
||||
}
|
||||
|
||||
// Load main.lua so functions are available
|
||||
mainPath := filepath.Join(p.Dir, "main.lua")
|
||||
if _, err := os.Stat(mainPath); err == nil {
|
||||
if err := vm.LoadScript("main.lua"); err != nil {
|
||||
return fmt.Errorf("load main.lua: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Call on_install hook
|
||||
if err := vm.CallHook(hookName); err != nil {
|
||||
return fmt.Errorf("on_install: %w", err)
|
||||
}
|
||||
|
||||
// Mark installed in config
|
||||
p.Installed = true
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
appCfg = config.DefaultAppConfig()
|
||||
}
|
||||
appCfg.InstalledPlugins = append(appCfg.InstalledPlugins, name)
|
||||
if err := config.SaveAppConfig(appCfg); err != nil {
|
||||
return fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("plugin %q not found", name)
|
||||
}
|
||||
|
||||
// Uninstall runs a plugin's on_uninstall hook (drops tables, cleans data),
|
||||
// disables it first, and removes it from the installed list.
|
||||
// Does NOT delete plugin files from disk.
|
||||
func (m *Manager) Uninstall(name string) error {
|
||||
for i := range m.plugins {
|
||||
if m.plugins[i].Meta.Name != name {
|
||||
continue
|
||||
}
|
||||
p := &m.plugins[i]
|
||||
if !p.Installed {
|
||||
return fmt.Errorf("plugin %q is not installed", name)
|
||||
}
|
||||
hookName := p.Meta.Hooks["on_uninstall"]
|
||||
if hookName == "" {
|
||||
return fmt.Errorf("plugin %q does not support install lifecycle", name)
|
||||
}
|
||||
|
||||
// First disable if active
|
||||
if p.Active {
|
||||
p.Active = false
|
||||
}
|
||||
|
||||
// Close existing runtime if any
|
||||
if p.vm != nil {
|
||||
p.vm.Close()
|
||||
p.vm = nil
|
||||
}
|
||||
|
||||
// Create a temporary VM to run on_uninstall
|
||||
vm, err := NewLuaVM(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create VM: %w", err)
|
||||
}
|
||||
defer vm.Close()
|
||||
if m.Services != nil {
|
||||
vm.SetServices(m.Services)
|
||||
}
|
||||
|
||||
mainPath := filepath.Join(p.Dir, "main.lua")
|
||||
if _, err := os.Stat(mainPath); err == nil {
|
||||
if err := vm.LoadScript("main.lua"); err != nil {
|
||||
return fmt.Errorf("load main.lua: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Call on_uninstall hook
|
||||
if err := vm.CallHook(hookName); err != nil {
|
||||
return fmt.Errorf("on_uninstall: %w", err)
|
||||
}
|
||||
|
||||
// Clean plugin data directory
|
||||
os.RemoveAll(p.DataDir + ".db") // remove SQLite file
|
||||
os.MkdirAll(p.DataDir, 0o750) // recreate for future install
|
||||
|
||||
// Remove from installed list in config
|
||||
p.Installed = false
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg != nil {
|
||||
var updated []string
|
||||
for _, n := range appCfg.InstalledPlugins {
|
||||
if n != name {
|
||||
updated = append(updated, n)
|
||||
}
|
||||
}
|
||||
appCfg.InstalledPlugins = updated
|
||||
// Also remove from enabled (can't be enabled if not installed)
|
||||
var enabled []string
|
||||
for _, n := range appCfg.EnabledPlugins {
|
||||
if n != name {
|
||||
enabled = append(enabled, n)
|
||||
}
|
||||
}
|
||||
appCfg.EnabledPlugins = enabled
|
||||
config.SaveAppConfig(appCfg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("plugin %q not found", name)
|
||||
}
|
||||
|
||||
// ActiveNames returns names of active plugins.
|
||||
|
|
|
|||
|
|
@ -9,12 +9,17 @@ import (
|
|||
)
|
||||
|
||||
// ActivatePlugin fully activates a plugin: creates Lua VM, loads main.lua, starts scheduler.
|
||||
// Only works if plugin is installed (if it has on_install hook, must be installed first).
|
||||
func (m *Manager) ActivatePlugin(name string) {
|
||||
for i := range m.plugins {
|
||||
p := &m.plugins[i]
|
||||
if p.Meta.Name != name || p.Active {
|
||||
continue
|
||||
}
|
||||
if p.HasInstall && !p.Installed {
|
||||
log.Printf("[plugins] %s: cannot activate — not installed", name)
|
||||
return
|
||||
}
|
||||
p.Active = true
|
||||
|
||||
vm, err := NewLuaVM(p)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ func TestDiscover(t *testing.T) {
|
|||
t.Errorf("plugin name = %q", plugins[0].Meta.Name)
|
||||
}
|
||||
|
||||
// Enable plugin to load templates
|
||||
mgr.Enable("client")
|
||||
|
||||
// Templates.
|
||||
tmpls := mgr.Templates()
|
||||
if len(tmpls) != 1 {
|
||||
|
|
@ -118,9 +121,16 @@ func TestEnableDisable(t *testing.T) {
|
|||
t.Fatalf("plugins = %d, want 2", len(mgr.Plugins()))
|
||||
}
|
||||
|
||||
// All active by default.
|
||||
// All inactive by default after Discover.
|
||||
if len(mgr.Active()) != 0 {
|
||||
t.Errorf("active after discover = %d, want 0", len(mgr.Active()))
|
||||
}
|
||||
|
||||
// Enable both.
|
||||
mgr.Enable("a")
|
||||
mgr.Enable("b")
|
||||
if len(mgr.Active()) != 2 {
|
||||
t.Errorf("active = %d, want 2", len(mgr.Active()))
|
||||
t.Errorf("active after enable = %d, want 2", len(mgr.Active()))
|
||||
}
|
||||
|
||||
// Disable one.
|
||||
|
|
@ -132,7 +142,7 @@ func TestEnableDisable(t *testing.T) {
|
|||
// Re-enable.
|
||||
mgr.Enable("a")
|
||||
if len(mgr.Active()) != 2 {
|
||||
t.Errorf("active after enable = %d, want 2", len(mgr.Active()))
|
||||
t.Errorf("active after re-enable = %d, want 2", len(mgr.Active()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,6 +154,8 @@ func TestActiveNames(t *testing.T) {
|
|||
|
||||
mgr := NewManager(root)
|
||||
mgr.Discover()
|
||||
mgr.Enable("p1")
|
||||
mgr.Enable("p2")
|
||||
mgr.Disable("p1")
|
||||
|
||||
names := mgr.ActiveNames()
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ func TestPluginManager_InitRuntimes(t *testing.T) {
|
|||
|
||||
mgr := NewManager(dir)
|
||||
mgr.Discover()
|
||||
mgr.Enable("testp")
|
||||
mgr.InitRuntimes()
|
||||
defer mgr.CloseRuntimes()
|
||||
|
||||
|
|
@ -325,12 +326,16 @@ func TestCalendarPlugin_LoadAndRun(t *testing.T) {
|
|||
Meta: Meta{
|
||||
Name: "calendar",
|
||||
Hooks: map[string]string{
|
||||
"on_init": "on_init",
|
||||
"on_init": "on_init",
|
||||
"on_install": "on_install",
|
||||
"on_uninstall": "on_uninstall",
|
||||
},
|
||||
},
|
||||
Dir: pluginDir,
|
||||
DataDir: dataDir,
|
||||
Active: true,
|
||||
Dir: pluginDir,
|
||||
DataDir: dataDir,
|
||||
Active: true,
|
||||
Installed: true,
|
||||
HasInstall: true,
|
||||
}
|
||||
|
||||
vm, err := NewLuaVM(p)
|
||||
|
|
@ -344,7 +349,12 @@ func TestCalendarPlugin_LoadAndRun(t *testing.T) {
|
|||
t.Fatalf("LoadScript(main.lua): %v", err)
|
||||
}
|
||||
|
||||
// Run on_init hook — this runs the migration + default categories
|
||||
// Run on_install — creates tables + default categories (skipped on re-install)
|
||||
if err := vm.CallHook("on_install"); err != nil {
|
||||
t.Fatalf("CallHook(on_install): %v", err)
|
||||
}
|
||||
|
||||
// Run on_init — registers API + state
|
||||
if err := vm.CallHook("on_init"); err != nil {
|
||||
t.Fatalf("CallHook(on_init): %v", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue