mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
fix: complete OpenClaw plugin compatibility with test coverage
- Add test OpenClaw plugins: oc-simple (openclaw.plugin.json) and oc-pkg (package.json#extensions) - Fix plugin.go Start() to detect package.json#openclaw.extensions as OC manifest - Add hasOCExtensions() helper for package.json OC field detection - Tests: simulator with manifest entry discovery, package.json extension discovery, full Plugin.Start() -> loadOCPlugin() -> register -> call pipeline
This commit is contained in:
@ -2,6 +2,7 @@ package openclaw
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@ -73,12 +74,15 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
|
|
||||||
hasMainJS := false
|
hasMainJS := false
|
||||||
hasOCManifest := false
|
hasOCManifest := false
|
||||||
|
hasOCPackage := false
|
||||||
for _, f := range subs {
|
for _, f := range subs {
|
||||||
switch f.Name() {
|
switch f.Name() {
|
||||||
case "main.js":
|
case "main.js":
|
||||||
hasMainJS = true
|
hasMainJS = true
|
||||||
case "openclaw.plugin.json":
|
case "openclaw.plugin.json":
|
||||||
hasOCManifest = true
|
hasOCManifest = true
|
||||||
|
case "package.json":
|
||||||
|
hasOCPackage = hasOCExtensions(filepath.Join(skillPath, "package.json"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +91,7 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
if err := p.loadSidecar(s, skillPath, entry.Name()); err != nil {
|
if err := p.loadSidecar(s, skillPath, entry.Name()); err != nil {
|
||||||
log.Printf("[openclaw] sidecar %s: %v", entry.Name(), err)
|
log.Printf("[openclaw] sidecar %s: %v", entry.Name(), err)
|
||||||
}
|
}
|
||||||
case hasOCManifest:
|
case hasOCManifest || hasOCPackage:
|
||||||
if err := p.loadOCPlugin(s, skillPath, entry.Name()); err != nil {
|
if err := p.loadOCPlugin(s, skillPath, entry.Name()); err != nil {
|
||||||
log.Printf("[openclaw] ocplugin %s: %v", entry.Name(), err)
|
log.Printf("[openclaw] ocplugin %s: %v", entry.Name(), err)
|
||||||
}
|
}
|
||||||
@ -221,3 +225,21 @@ func (p *Plugin) Stop() error {
|
|||||||
p.skills = nil
|
p.skills = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasOCExtensions 检测 package.json 中是否有 openclaw.extensions 或 openclaw.runtimeExtensions
|
||||||
|
func hasOCExtensions(pkgPath string) bool {
|
||||||
|
data, err := os.ReadFile(pkgPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var pkg struct {
|
||||||
|
OpenClaw *struct {
|
||||||
|
Extensions interface{} `json:"extensions"`
|
||||||
|
RuntimeExtensions interface{} `json:"runtimeExtensions"`
|
||||||
|
} `json:"openclaw"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &pkg); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pkg.OpenClaw != nil && (pkg.OpenClaw.Extensions != nil || pkg.OpenClaw.RuntimeExtensions != nil)
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLaunchSidecarNoMainJS(t *testing.T) {
|
func TestLaunchSidecarNoMainJS(t *testing.T) {
|
||||||
@ -188,3 +190,190 @@ func TestConcurrentCalls(t *testing.T) {
|
|||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Simulator + OpenClaw plugin tests ----
|
||||||
|
|
||||||
|
func launchSimulator(t *testing.T, pluginDir, name string) *sidecarProcess {
|
||||||
|
t.Helper()
|
||||||
|
simPath, err := filepath.Abs(filepath.Join("simulator", "main.js"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("abs simulator path: %v", err)
|
||||||
|
}
|
||||||
|
sp, err := launchProcess("node", simPath, pluginDir, name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("launch simulator for %s: %v", name, err)
|
||||||
|
}
|
||||||
|
return sp
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimulatorWithOCSimple(t *testing.T) {
|
||||||
|
pluginDir, err := filepath.Abs(filepath.Join("testdata", "oc-simple"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("abs testdata: %v", err)
|
||||||
|
}
|
||||||
|
sp := launchSimulator(t, pluginDir, "oc-simple")
|
||||||
|
defer sp.Close()
|
||||||
|
|
||||||
|
tools, err := sp.ListTools()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list tools: %v", err)
|
||||||
|
}
|
||||||
|
if len(tools) != 2 {
|
||||||
|
t.Fatalf("expected 2 tools, got %d: %+v", len(tools), tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := map[string]bool{"greet": false, "ping": false}
|
||||||
|
for _, tool := range tools {
|
||||||
|
found[tool.Name] = true
|
||||||
|
}
|
||||||
|
if !found["greet"] || !found["ping"] {
|
||||||
|
t.Fatalf("expected greet and ping tools, got %+v", tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := sp.CallTool("greet", map[string]interface{}{"name": "Test"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("call greet: %v", err)
|
||||||
|
}
|
||||||
|
if result != "Hello, Test!" {
|
||||||
|
t.Fatalf("expected 'Hello, Test!', got %q", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimulatorWithOCPackage(t *testing.T) {
|
||||||
|
pluginDir, err := filepath.Abs(filepath.Join("testdata", "oc-pkg"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("abs testdata: %v", err)
|
||||||
|
}
|
||||||
|
sp := launchSimulator(t, pluginDir, "oc-pkg")
|
||||||
|
defer sp.Close()
|
||||||
|
|
||||||
|
tools, err := sp.ListTools()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list tools: %v", err)
|
||||||
|
}
|
||||||
|
if len(tools) != 2 {
|
||||||
|
t.Fatalf("expected 2 tools, got %d: %+v", len(tools), tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := map[string]bool{"add": false, "info": false}
|
||||||
|
for _, tool := range tools {
|
||||||
|
found[tool.Name] = true
|
||||||
|
if tool.Name == "add" {
|
||||||
|
if tool.InputSchema == nil {
|
||||||
|
t.Error("add tool should have inputSchema")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found["add"] || !found["info"] {
|
||||||
|
t.Fatalf("expected add and info tools, got %+v", tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := sp.CallTool("add", map[string]interface{}{"a": 10.0, "b": 20.0})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("call add: %v", err)
|
||||||
|
}
|
||||||
|
if result != "30" {
|
||||||
|
t.Fatalf("expected '30', got %q", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
infoResult, err := sp.CallTool("info", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("call info: %v", err)
|
||||||
|
}
|
||||||
|
if infoResult == "" {
|
||||||
|
t.Fatal("expected non-empty info result")
|
||||||
|
}
|
||||||
|
t.Logf("info result: %s", infoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadOCPluginViaPluginStart(t *testing.T) {
|
||||||
|
skillsDir := t.TempDir()
|
||||||
|
|
||||||
|
ocSimpleDir := filepath.Join(skillsDir, "oc-simple")
|
||||||
|
if err := os.MkdirAll(ocSimpleDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"openclaw.plugin.json", "index.js"} {
|
||||||
|
src := filepath.Join("testdata", "oc-simple", name)
|
||||||
|
data, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", name, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(ocSimpleDir, name), data, 0644); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ocPkgDir := filepath.Join(skillsDir, "oc-pkg")
|
||||||
|
if err := os.MkdirAll(filepath.Join(ocPkgDir, "lib"), 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"package.json", "lib/entry.js"} {
|
||||||
|
src := filepath.Join("testdata", "oc-pkg", name)
|
||||||
|
data, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", name, err)
|
||||||
|
}
|
||||||
|
dst := filepath.Join(ocPkgDir, name)
|
||||||
|
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||||
|
t.Fatalf("write %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SimulatorDir = filepath.Join(t.TempDir(), ".simulator")
|
||||||
|
|
||||||
|
p := New("openclaw", skillsDir)
|
||||||
|
|
||||||
|
var registeredTools []string
|
||||||
|
registeredHandlers := make(map[string]sdk.ToolHandler)
|
||||||
|
sdk := sdk.New("openclaw", nil, nil, nil, nil, nil, nil, nil, nil,
|
||||||
|
func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||||
|
registeredTools = append(registeredTools, name)
|
||||||
|
registeredHandlers[name] = handler
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
nil, nil)
|
||||||
|
|
||||||
|
if err := p.Start(sdk); err != nil {
|
||||||
|
t.Fatalf("start plugin: %v", err)
|
||||||
|
}
|
||||||
|
defer p.Stop()
|
||||||
|
|
||||||
|
expected := []string{"oc-simple_greet", "oc-simple_ping", "oc-pkg_add", "oc-pkg_info"}
|
||||||
|
for _, exp := range expected {
|
||||||
|
found := false
|
||||||
|
for _, name := range registeredTools {
|
||||||
|
if name == exp {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected tool %q not registered. got: %v", exp, registeredTools)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, ok := registeredHandlers["oc-simple_greet"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("greet handler not registered")
|
||||||
|
}
|
||||||
|
result, err := handler(map[string]interface{}{"name": "OpenClaw"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("exec greet: %v", err)
|
||||||
|
}
|
||||||
|
if result != "Hello, OpenClaw!" {
|
||||||
|
t.Fatalf("expected 'Hello, OpenClaw!', got %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, ok = registeredHandlers["oc-pkg_add"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("add handler not registered")
|
||||||
|
}
|
||||||
|
result, err = handler(map[string]interface{}{"a": 7.0, "b": 8.0})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("exec add: %v", err)
|
||||||
|
}
|
||||||
|
if result != "15" {
|
||||||
|
t.Fatalf("expected '15', got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
33
internal/plugins/openclaw/testdata/oc-pkg/lib/entry.js
vendored
Normal file
33
internal/plugins/openclaw/testdata/oc-pkg/lib/entry.js
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
default: {
|
||||||
|
id: 'oc-pkg',
|
||||||
|
name: 'OC Package',
|
||||||
|
version: '2.0.0',
|
||||||
|
description: 'Test plugin discovered via package.json extensions',
|
||||||
|
register(api) {
|
||||||
|
api.registerTool({
|
||||||
|
name: 'add',
|
||||||
|
description: 'Add two numbers',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
a: { type: 'number', description: 'First number' },
|
||||||
|
b: { type: 'number', description: 'Second number' },
|
||||||
|
},
|
||||||
|
required: ['a', 'b'],
|
||||||
|
},
|
||||||
|
execute(id, params) {
|
||||||
|
return String(params.a + params.b);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerTool({
|
||||||
|
name: 'info',
|
||||||
|
description: 'Return plugin info',
|
||||||
|
execute(id, params) {
|
||||||
|
return JSON.stringify({ id: this.id, name: this.name, version: this.version });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
8
internal/plugins/openclaw/testdata/oc-pkg/package.json
vendored
Normal file
8
internal/plugins/openclaw/testdata/oc-pkg/package.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "oc-pkg",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "OC plugin with package.json entry discovery",
|
||||||
|
"openclaw": {
|
||||||
|
"extensions": ["lib/entry.js"]
|
||||||
|
}
|
||||||
|
}
|
||||||
31
internal/plugins/openclaw/testdata/oc-simple/index.js
vendored
Normal file
31
internal/plugins/openclaw/testdata/oc-simple/index.js
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
module.exports = {
|
||||||
|
default: {
|
||||||
|
id: 'oc-simple',
|
||||||
|
name: 'OC Simple',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'Simple test plugin for OpenClaw simulator',
|
||||||
|
register(api) {
|
||||||
|
api.registerTool({
|
||||||
|
name: 'greet',
|
||||||
|
description: 'Greet someone',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string', description: 'Name to greet' },
|
||||||
|
},
|
||||||
|
required: ['name'],
|
||||||
|
},
|
||||||
|
execute(id, params) {
|
||||||
|
return `Hello, ${params.name}!`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.registerTool({
|
||||||
|
name: 'ping',
|
||||||
|
description: 'Health check ping',
|
||||||
|
execute(id, params) {
|
||||||
|
return 'pong';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
6
internal/plugins/openclaw/testdata/oc-simple/openclaw.plugin.json
vendored
Normal file
6
internal/plugins/openclaw/testdata/oc-simple/openclaw.plugin.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "oc-simple",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"entry": "index.js",
|
||||||
|
"description": "Simple OpenClaw test plugin"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user