mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
feat: restructure plugin system, add Lua plugin support, update docs
This commit is contained in:
263
internal/plugin/bridge_e2e_test.go
Normal file
263
internal/plugin/bridge_e2e_test.go
Normal file
@ -0,0 +1,263 @@
|
||||
//go:build windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
func TestBridgeE2E_WebPlugin(t *testing.T) {
|
||||
exeDir, _ := os.Executable()
|
||||
// Find web example build relative to the homeagent repo root
|
||||
haRoot := findHomeAgentRoot(t, exeDir)
|
||||
dllPath := filepath.Join(haRoot, "..", "homeagentsdk", "example", "web", "build", "plugin.dll")
|
||||
if _, err := os.Stat(dllPath); os.IsNotExist(err) {
|
||||
t.Fatalf("web plugin DLL not found at %s\nRun: cd example/web && plugindev build --target windows/amd64", dllPath)
|
||||
}
|
||||
|
||||
// Track captured tools and stages
|
||||
var capturedTools []sdk.ToolDef
|
||||
var capturedStages []sdk.Stage
|
||||
|
||||
regTool := func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||
capturedTools = append(capturedTools, def)
|
||||
t.Logf(" registered tool: %s", name)
|
||||
return nil
|
||||
}
|
||||
regStage := func(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
capturedStages = append(capturedStages, stage)
|
||||
t.Logf(" registered stage: %s", stage)
|
||||
}
|
||||
regAPI := func(name string) error {
|
||||
t.Logf(" registered API: %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
sett := sdk.NewSettings("web", nil)
|
||||
psdk := sdk.New("web", nil, nil, nil, nil, nil, nil, nil, sett, regTool, regStage, regAPI)
|
||||
|
||||
plg, err := newDLLPlugin(dllPath, "web", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("newDLLPlugin failed: %v", err)
|
||||
}
|
||||
defer plg.Stop()
|
||||
|
||||
// Start — this calls NewPlugin + StartPlugin + registerTools + registerStages
|
||||
if err := plg.Start(psdk); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify tools were captured
|
||||
if len(capturedTools) == 0 {
|
||||
t.Fatal("no tools were registered by web plugin")
|
||||
}
|
||||
t.Logf("Captured %d tools:", len(capturedTools))
|
||||
for _, d := range capturedTools {
|
||||
t.Logf(" - %s: %s", d.Name, d.Description[:min(len(d.Description), 60)])
|
||||
}
|
||||
|
||||
// Check specific expected tools
|
||||
webSearch, webFetch := false, false
|
||||
for _, d := range capturedTools {
|
||||
if d.Name == "web_search" {
|
||||
webSearch = true
|
||||
if d.Description == "" {
|
||||
t.Error("web_search has empty description")
|
||||
}
|
||||
params := d.Parameters
|
||||
if params == nil {
|
||||
t.Error("web_search has nil parameters")
|
||||
} else {
|
||||
if _, ok := params["properties"]; !ok {
|
||||
t.Error("web_search parameters missing 'properties'")
|
||||
}
|
||||
}
|
||||
}
|
||||
if d.Name == "web_fetch" {
|
||||
webFetch = true
|
||||
}
|
||||
}
|
||||
if !webSearch {
|
||||
t.Error("expected tool 'web_search' not registered")
|
||||
}
|
||||
if !webFetch {
|
||||
t.Error("expected tool 'web_fetch' not registered")
|
||||
}
|
||||
|
||||
// Verify bridge exports work via direct C ABI calls
|
||||
t.Logf("Bridge exports: getTools=%x invokeTool=%x freeCStr=%x",
|
||||
plg.getTools, plg.invokeTool, plg.freeCStr)
|
||||
|
||||
// GetToolDefsJSON
|
||||
if plg.getTools != 0 {
|
||||
toolDefsJSON := callGetToolDefsJSON(t, plg)
|
||||
if len(toolDefsJSON) == 0 {
|
||||
t.Error("GetToolDefsJSON returned empty array, expected tools")
|
||||
}
|
||||
for _, d := range toolDefsJSON {
|
||||
t.Logf(" bridge tool: %s", d["name"])
|
||||
}
|
||||
}
|
||||
|
||||
// InvokeToolJSON — test with the search tool
|
||||
if plg.invokeTool != 0 {
|
||||
result := callInvokeToolJSON(t, plg, "web_search", map[string]interface{}{
|
||||
"query": "test",
|
||||
"count": 1,
|
||||
})
|
||||
t.Logf("InvokeToolJSON result keys: %v", keysOfMap(result))
|
||||
// Should get a result map (might be error if no network, but should not crash)
|
||||
if errStr, ok := result["error"]; ok {
|
||||
t.Logf(" (expected — tool returned error: %v)", errStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeE2E_SanitizerStages(t *testing.T) {
|
||||
exeDir, _ := os.Executable()
|
||||
haRoot := findHomeAgentRoot(t, exeDir)
|
||||
dllPath := filepath.Join(haRoot, "..", "homeagentsdk", "example", "sanitizer", "build", "plugin.dll")
|
||||
if _, err := os.Stat(dllPath); os.IsNotExist(err) {
|
||||
t.Skip("sanitizer DLL not built")
|
||||
}
|
||||
|
||||
var capturedStages []sdk.Stage
|
||||
regStage := func(stage sdk.Stage, handler sdk.StageHandler) {
|
||||
capturedStages = append(capturedStages, stage)
|
||||
t.Logf(" registered stage: %s", stage)
|
||||
}
|
||||
|
||||
sett := sdk.NewSettings("sanitizer", nil)
|
||||
psdk := sdk.New("sanitizer", nil, nil, nil, nil, nil, nil, nil, sett,
|
||||
func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error { return nil },
|
||||
regStage,
|
||||
func(name string) error { return nil },
|
||||
)
|
||||
|
||||
plg, err := newDLLPlugin(dllPath, "sanitizer", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("newDLLPlugin failed: %v", err)
|
||||
}
|
||||
defer plg.Stop()
|
||||
|
||||
if err := plg.Start(psdk); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
|
||||
if len(capturedStages) == 0 {
|
||||
t.Fatal("no stages registered by sanitizer")
|
||||
}
|
||||
found := false
|
||||
for _, s := range capturedStages {
|
||||
if s == sdk.StagePostAction {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected post_action stage, got %v", capturedStages)
|
||||
}
|
||||
|
||||
// Verify bridge GetStagesJSON
|
||||
if plg.getStages != 0 {
|
||||
ret, _, _ := syscall.SyscallN(plg.getStages, plg.handle)
|
||||
if ret != 0 {
|
||||
stagesJSON := cStringPtrToString(ret)
|
||||
if plg.freeCStr != 0 {
|
||||
syscall.SyscallN(plg.freeCStr, ret)
|
||||
}
|
||||
t.Logf("GetStagesJSON: %s", stagesJSON)
|
||||
if !contains(t, stagesJSON, "post_action") {
|
||||
t.Error("GetStagesJSON missing post_action")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func findHomeAgentRoot(t *testing.T, exeDir string) string {
|
||||
t.Helper()
|
||||
// Walk up from test binary directory looking for homeagent/
|
||||
dir := exeDir
|
||||
for i := 0; i < 10; i++ {
|
||||
if _, err := os.Stat(filepath.Join(dir, "internal", "plugin")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
t.Fatal("cannot find homeagent root")
|
||||
return ""
|
||||
}
|
||||
|
||||
func callGetToolDefsJSON(t *testing.T, plg *dllPlugin) []map[string]interface{} {
|
||||
t.Helper()
|
||||
ret, _, _ := syscall.SyscallN(plg.getTools, plg.handle)
|
||||
if ret == 0 {
|
||||
t.Fatal("GetToolDefsJSON returned nil")
|
||||
}
|
||||
jsonStr := cStringPtrToString(ret)
|
||||
if plg.freeCStr != 0 {
|
||||
syscall.SyscallN(plg.freeCStr, ret)
|
||||
}
|
||||
var defs []map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &defs); err != nil {
|
||||
t.Fatalf("GetToolDefsJSON parse error: %v", err)
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func callInvokeToolJSON(t *testing.T, plg *dllPlugin, toolName string, args map[string]interface{}) map[string]interface{} {
|
||||
t.Helper()
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
cToolName := append([]byte(toolName), 0)
|
||||
cArgs := append(argsJSON, 0)
|
||||
|
||||
ret, _, _ := syscall.SyscallN(
|
||||
plg.invokeTool,
|
||||
plg.handle,
|
||||
uintptr(unsafe.Pointer(&cToolName[0])),
|
||||
uintptr(unsafe.Pointer(&cArgs[0])),
|
||||
)
|
||||
if ret == 0 {
|
||||
t.Fatal("InvokeToolJSON returned nil")
|
||||
}
|
||||
jsonStr := cStringPtrToString(ret)
|
||||
if plg.freeCStr != 0 {
|
||||
syscall.SyscallN(plg.freeCStr, ret)
|
||||
}
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||||
t.Fatalf("InvokeToolJSON parse error: %v (json=%s)", err, jsonStr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func keysOfMap(m map[string]interface{}) []string {
|
||||
var keys []string
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func contains(t *testing.T, s, substr string) bool {
|
||||
t.Helper()
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user