verstak/internal/core/plugins/runtime_test.go

392 lines
9.2 KiB
Go

package plugins
import (
"os"
"path/filepath"
"testing"
lua "github.com/yuin/gopher-lua"
)
func TestLuaVM_HookCall(t *testing.T) {
// Create a temp plugin directory
dir := t.TempDir()
pluginDir := filepath.Join(dir, "testplugin")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
// Write a main.lua
mainLua := `
function on_init()
print("hello from lua")
end
function add(a, b)
return a + b
end
`
if err := os.WriteFile(filepath.Join(pluginDir, "main.lua"), []byte(mainLua), 0644); err != nil {
t.Fatal(err)
}
p := &Plugin{
Meta: Meta{
Name: "testplugin",
Hooks: map[string]string{
"on_init": "on_init",
},
},
Dir: pluginDir,
Active: true,
}
vm, err := NewLuaVM(p)
if err != nil {
t.Fatalf("NewLuaVM: %v", err)
}
defer vm.Close()
// Load main.lua
if err := vm.LoadScript("main.lua"); err != nil {
t.Fatalf("LoadScript: %v", err)
}
// Call on_init hook (should not error)
if err := vm.CallHook("on_init"); err != nil {
t.Fatalf("CallHook(on_init): %v", err)
}
// Call a function with args and check result
// Need to use CallHookWithResult for that
result, err := vm.CallHookWithResult("add", lua.LNumber(3), lua.LNumber(4))
if err != nil {
t.Fatalf("CallHookWithResult(add): %v", err)
}
if num, ok := result.(lua.LNumber); !ok {
t.Fatalf("expected LNumber, got %T", result)
} else if float64(num) != 7 {
t.Fatalf("expected 7, got %v", num)
}
}
func TestLuaVM_MissingHook(t *testing.T) {
dir := t.TempDir()
pluginDir := filepath.Join(dir, "nohooks")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
// Write minimal main.lua (no on_shutdown function)
mainLua := `function on_init() end`
if err := os.WriteFile(filepath.Join(pluginDir, "main.lua"), []byte(mainLua), 0644); err != nil {
t.Fatal(err)
}
p := &Plugin{
Meta: Meta{
Name: "nohooks",
Hooks: map[string]string{
"on_shutdown": "on_shutdown",
},
},
Dir: pluginDir,
Active: true,
}
vm, err := NewLuaVM(p)
if err != nil {
t.Fatalf("NewLuaVM: %v", err)
}
defer vm.Close()
if err := vm.LoadScript("main.lua"); err != nil {
t.Fatalf("LoadScript: %v", err)
}
// Calling a missing hook should return nil error (hooks are optional)
if err := vm.CallHook("on_shutdown"); err != nil {
t.Fatalf("CallHook(on_shutdown) on missing function: %v", err)
}
}
func TestLuaVM_Sandbox(t *testing.T) {
dir := t.TempDir()
pluginDir := filepath.Join(dir, "sandbox")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
// Verify dangerous functions are removed
mainLua := `
function test_sandbox()
local ok, err = pcall(function()
return dofile("/etc/passwd")
end)
return ok, err -- dofile should be nil, so pcall returns false + error
end
`
if err := os.WriteFile(filepath.Join(pluginDir, "main.lua"), []byte(mainLua), 0644); err != nil {
t.Fatal(err)
}
p := &Plugin{
Meta: Meta{Name: "sandbox"},
Dir: pluginDir,
Active: true,
}
vm, err := NewLuaVM(p)
if err != nil {
t.Fatalf("NewLuaVM: %v", err)
}
defer vm.Close()
if err := vm.LoadScript("main.lua"); err != nil {
t.Fatalf("LoadScript: %v", err)
}
result, err := vm.CallHookWithResult("test_sandbox")
if err != nil {
t.Fatalf("CallHook: %v", err)
}
t.Logf("sandbox test result: %v", result)
// dofile is nil, so pcall will catch the error and return false
// The hook returns the first value from Lua, which should be false
if result == lua.LTrue {
t.Error("dofile should not be available in sandbox")
}
}
func TestLuaVM_State(t *testing.T) {
dir := t.TempDir()
pluginDir := filepath.Join(dir, "state")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
// Test verstak.state API
mainLua := `
function test_state()
verstak.state.set("counter", 42)
verstak.state.set("name", "test")
local c = verstak.state.get("counter")
local n = verstak.state.get("name")
return c, n
end
`
if err := os.WriteFile(filepath.Join(pluginDir, "main.lua"), []byte(mainLua), 0644); err != nil {
t.Fatal(err)
}
p := &Plugin{
Meta: Meta{Name: "state"},
Dir: pluginDir,
Active: true,
}
vm, err := NewLuaVM(p)
if err != nil {
t.Fatalf("NewLuaVM: %v", err)
}
defer vm.Close()
if err := vm.LoadScript("main.lua"); err != nil {
t.Fatalf("LoadScript: %v", err)
}
_, err = vm.CallHookWithResult("test_state")
if err != nil {
t.Fatalf("CallHook: %v", err)
}
// verstak.state.set/get should work (first return = counter = 42)
// Currently returns "not implemented" stub error, but we verify the path
t.Log("state test completed (stub — will fail if API wasn't callable)")
}
func TestPluginManager_InitRuntimes(t *testing.T) {
dir := t.TempDir()
pluginsDir := filepath.Join(dir, ".verstak", "plugins", "testp")
if err := os.MkdirAll(pluginsDir, 0755); err != nil {
t.Fatal(err)
}
// Write plugin.json
pj := `{
"name": "testp",
"version": "1.0.0",
"hooks": { "on_init": "on_init" }
}`
if err := os.WriteFile(filepath.Join(pluginsDir, "plugin.json"), []byte(pj), 0644); err != nil {
t.Fatal(err)
}
// Write main.lua
if err := os.WriteFile(filepath.Join(pluginsDir, "main.lua"), []byte(`function on_init() print("manager init ok") end`), 0644); err != nil {
t.Fatal(err)
}
mgr := NewManager(dir)
mgr.Discover()
mgr.Enable("testp")
mgr.InitRuntimes()
defer mgr.CloseRuntimes()
if len(mgr.Plugins()) != 1 {
t.Fatalf("expected 1 plugin, got %d", len(mgr.Plugins()))
}
p := mgr.Plugins()[0]
if p.Meta.Name != "testp" {
t.Fatalf("expected testp, got %s", p.Meta.Name)
}
if !p.Active {
t.Fatal("plugin should be active")
}
if p.vm == nil {
t.Fatal("plugin should have Lua VM")
}
// Call on_init through manager
mgr.CallInitHooks()
}
// TestNodeAPI_CreateAndGet verifies that the verstak_node table is present and callable.
func TestNodeAPI_CreateAndGet(t *testing.T) {
dir := t.TempDir()
pluginDir := filepath.Join(dir, "nodetest")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
p := &Plugin{
Meta: Meta{Name: "nodetest"},
Dir: pluginDir,
}
vm, err := NewLuaVM(p)
if err != nil {
t.Fatalf("NewLuaVM: %v", err)
}
defer vm.Close()
// Verify verstak_node global table exists
nodeTable := vm.L.GetGlobal("verstak_node")
if nodeTable == lua.LNil {
t.Fatal("verstak_node is nil (expected table)")
}
if _, ok := nodeTable.(*lua.LTable); !ok {
t.Fatalf("verstak_node type: %T (expected *lua.LTable)", nodeTable)
}
// Run a Lua snippet that calls verstak_node.get — should error since no services
err = vm.L.DoString(`
local ok, err = pcall(verstak_node.get, "fake-id")
assert(not ok, "expected error from verstak_node.get with no services")
`)
if err != nil {
t.Fatalf("Lua snippet: %v", err)
}
// Verify verstak.node table exists inside main verstak table
mainTbl := vm.L.GetGlobal("verstak")
if mainTbl == lua.LNil {
t.Fatal("verstak global table missing")
}
verstakTbl, ok := mainTbl.(*lua.LTable)
if !ok {
t.Fatalf("verstak type: %T", mainTbl)
}
nodeSubTbl := verstakTbl.RawGetString("node")
if nodeSubTbl == lua.LNil {
t.Fatal("verstak.node is nil")
}
t.Log("node API present and callable without crash")
}
// TestCalendarPlugin_LoadAndRun runs the calendar plugin's Lua test suite
// via the LuaVM to verify migration, CRUD, recurrence, and all other features.
func TestCalendarPlugin_LoadAndRun(t *testing.T) {
dir := t.TempDir()
// Copy the calendar plugin from contrib/plugins/calendar to temp dir
calendarSrc := filepath.Join("..", "..", "..", "contrib", "plugins", "calendar")
pluginDir := filepath.Join(dir, "calendar")
if err := copyDir(calendarSrc, pluginDir); err != nil {
t.Fatalf("copy calendar plugin: %v", err)
}
dataDir := filepath.Join(dir, "calendar_data")
if err := os.MkdirAll(dataDir, 0755); err != nil {
t.Fatal(err)
}
p := &Plugin{
Meta: Meta{
Name: "calendar",
Hooks: map[string]string{
"on_init": "on_init",
"on_install": "on_install",
"on_uninstall": "on_uninstall",
},
},
Dir: pluginDir,
DataDir: dataDir,
Active: true,
Installed: true,
HasInstall: true,
}
vm, err := NewLuaVM(p)
if err != nil {
t.Fatalf("NewLuaVM: %v", err)
}
defer vm.Close()
// Load main.lua
if err := vm.LoadScript("main.lua"); err != nil {
t.Fatalf("LoadScript(main.lua): %v", err)
}
// 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)
}
// Run the Lua test suite
if err := vm.LoadScript("test_calendar.lua"); err != nil {
t.Fatalf("LoadScript(test_calendar.lua): %v", err)
}
t.Log("Calendar plugin: all Lua API tests passed")
}
// copyDir recursively copies src to dst (skips hidden files).
func copyDir(src, dst string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
target := filepath.Join(dst, rel)
if info.IsDir() {
return os.MkdirAll(target, info.Mode())
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(target, data, info.Mode())
})
}