mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
## 背景 仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑), gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。 但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源 不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。 ## 关键区分(这是本次改动安全的前提) 两个 module 是**不同**的,影响面差一个数量级: | module | 谁依赖 | 改它的后果 | |---|---|---| | `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 | | `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 | ★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0), 只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串** (`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。 ⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。 SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配, 否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。 ## 改动 - `go.mod` / `go.sum`:两个 module 路径 + require/replace - 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径 - SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板 - 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等) ## 明确**保留** gitcode 的地方(镜像机制,不能动) - `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job) - `gitcode.com/api/v5/...`(release 附件上传) - git remote `gitcode`(镜像拉取/推送) - `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维) ## 验证 - `go build ./...` / `go vet ./...` 绿 - `go test ./... -count=1` → 48 包 ok / 0 FAIL - hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`) - 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0 - 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好 ## 顺带修正 `hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」—— module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**: 离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
404 lines
11 KiB
Go
404 lines
11 KiB
Go
package clawhubadapter
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
sdk "github.com/JianFeeeee/HomeAgent/internal/sdk"
|
|
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
|
|
)
|
|
|
|
// mockSettings implements pubsdk.SettingsAPI for tests
|
|
type mockSettings struct{}
|
|
|
|
func (m *mockSettings) DataDir() string { return "/tmp/mock_data" }
|
|
|
|
func (m *mockSettings) Get(key string) (interface{}, error) { return nil, nil }
|
|
func (m *mockSettings) Set(key string, value interface{}) error { return nil }
|
|
func (m *mockSettings) List(prefix string) ([]string, error) { return nil, nil }
|
|
func (m *mockSettings) GetCore(key string) (interface{}, error) { return nil, nil }
|
|
func (m *mockSettings) SetCore(key string, value interface{}) error { return nil }
|
|
func (m *mockSettings) ListCore(prefix string) ([]string, error) { return nil, nil }
|
|
func (m *mockSettings) GetPlugin(plugin, key string) (interface{}, error) { return nil, nil }
|
|
func (m *mockSettings) SetPlugin(plugin, key string, value interface{}) error { return nil }
|
|
func (m *mockSettings) ListPlugin(plugin, prefix string) ([]string, error) { return nil, nil }
|
|
func (m *mockSettings) RegisterDef(def pubsdk.ConfigDef) {}
|
|
func (m *mockSettings) Defs(prefix string) []*pubsdk.ConfigDef { return nil }
|
|
func (m *mockSettings) Dump() map[string]interface{} { return nil }
|
|
func (m *mockSettings) Plugins() []string { return nil }
|
|
func (m *mockSettings) DefsCore(prefix string) []*sdk.ConfigDef { return nil }
|
|
func (m *mockSettings) DefsPlugin(plugin, prefix string) []*sdk.ConfigDef { return nil }
|
|
func (m *mockSettings) Remove(key string) error { return nil }
|
|
func (m *mockSettings) RemoveCore(key string) error { return nil }
|
|
func (m *mockSettings) RemovePlugin(plugin, key string) error { return nil }
|
|
|
|
func TestLaunchSidecarNoMainJS(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
sp, err := launchSidecar(tmpDir, "nonexistent", "")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if sp != nil {
|
|
t.Fatal("expected nil for dir without main.js")
|
|
}
|
|
}
|
|
|
|
func TestLaunchSidecarAndListTools(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
src := filepath.Join("testdata", "echoplugin", "main.js")
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
t.Fatalf("read test plugin: %v", err)
|
|
}
|
|
dst := filepath.Join(tmpDir, "main.js")
|
|
if err := os.WriteFile(dst, data, 0755); err != nil {
|
|
t.Fatalf("write test plugin: %v", err)
|
|
}
|
|
|
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
|
if err != nil {
|
|
t.Fatalf("launch sidecar: %v", err)
|
|
}
|
|
defer sp.Close()
|
|
|
|
tools, err := sp.ListTools()
|
|
if err != nil {
|
|
t.Fatalf("list tools: %v", err)
|
|
}
|
|
|
|
if len(tools) == 0 {
|
|
t.Fatal("expected at least one tool")
|
|
}
|
|
|
|
found := false
|
|
for _, tool := range tools {
|
|
if tool.Name == "echo" {
|
|
found = true
|
|
if tool.Description == "" {
|
|
t.Error("expected non-empty description for echo tool")
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("expected 'echo' tool in list")
|
|
}
|
|
t.Logf("tools: %+v", tools)
|
|
}
|
|
|
|
func TestCallEchoTool(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
src := filepath.Join("testdata", "echoplugin", "main.js")
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
t.Fatalf("read test plugin: %v", err)
|
|
}
|
|
dst := filepath.Join(tmpDir, "main.js")
|
|
if err := os.WriteFile(dst, data, 0755); err != nil {
|
|
t.Fatalf("write test plugin: %v", err)
|
|
}
|
|
|
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
|
if err != nil {
|
|
t.Fatalf("launch sidecar: %v", err)
|
|
}
|
|
defer sp.Close()
|
|
|
|
result, err := sp.CallTool("echo", map[string]interface{}{
|
|
"text": "hello world",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("call echo tool: %v", err)
|
|
}
|
|
|
|
expected := "Echo: hello world"
|
|
if result != expected {
|
|
t.Fatalf("expected %q, got %q", expected, result)
|
|
}
|
|
t.Logf("echo result: %s", result)
|
|
}
|
|
|
|
func TestCallAddTool(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
src := filepath.Join("testdata", "echoplugin", "main.js")
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
t.Fatalf("read test plugin: %v", err)
|
|
}
|
|
dst := filepath.Join(tmpDir, "main.js")
|
|
if err := os.WriteFile(dst, data, 0755); err != nil {
|
|
t.Fatalf("write test plugin: %v", err)
|
|
}
|
|
|
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
|
if err != nil {
|
|
t.Fatalf("launch sidecar: %v", err)
|
|
}
|
|
defer sp.Close()
|
|
|
|
result, err := sp.CallTool("add", map[string]interface{}{
|
|
"a": 3.0,
|
|
"b": 4.0,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("call add tool: %v", err)
|
|
}
|
|
|
|
expected := "7"
|
|
if result != expected {
|
|
t.Fatalf("expected %q, got %q", expected, result)
|
|
}
|
|
t.Logf("add result: %s", result)
|
|
}
|
|
|
|
func TestCallNonexistentTool(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
src := filepath.Join("testdata", "echoplugin", "main.js")
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
t.Fatalf("read test plugin: %v", err)
|
|
}
|
|
dst := filepath.Join(tmpDir, "main.js")
|
|
if err := os.WriteFile(dst, data, 0755); err != nil {
|
|
t.Fatalf("write test plugin: %v", err)
|
|
}
|
|
|
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
|
if err != nil {
|
|
t.Fatalf("launch sidecar: %v", err)
|
|
}
|
|
defer sp.Close()
|
|
|
|
_, err = sp.CallTool("nonexistent", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for nonexistent tool")
|
|
}
|
|
t.Logf("expected error: %v", err)
|
|
}
|
|
|
|
func TestConcurrentCalls(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
src := filepath.Join("testdata", "echoplugin", "main.js")
|
|
data, err := os.ReadFile(src)
|
|
if err != nil {
|
|
t.Fatalf("read test plugin: %v", err)
|
|
}
|
|
dst := filepath.Join(tmpDir, "main.js")
|
|
if err := os.WriteFile(dst, data, 0755); err != nil {
|
|
t.Fatalf("write test plugin: %v", err)
|
|
}
|
|
|
|
sp, err := launchSidecar(tmpDir, "echoplugin", "")
|
|
if err != nil {
|
|
t.Fatalf("launch sidecar: %v", err)
|
|
}
|
|
defer sp.Close()
|
|
|
|
done := make(chan bool, 5)
|
|
for i := 0; i < 5; i++ {
|
|
go func(n int) {
|
|
result, err := sp.CallTool("add", map[string]interface{}{
|
|
"a": float64(n),
|
|
"b": float64(n * 2),
|
|
})
|
|
if err != nil {
|
|
t.Errorf("concurrent call %d: %v", n, err)
|
|
}
|
|
if result != fmt.Sprintf("%d", n + n*2) {
|
|
t.Errorf("call %d: expected %d, got %s", n, n + n*2, result)
|
|
}
|
|
done <- true
|
|
}(i)
|
|
}
|
|
for i := 0; i < 5; i++ {
|
|
<-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)
|
|
}
|
|
}
|
|
|
|
p := New("openclaw", skillsDir)
|
|
|
|
var registeredTools []string
|
|
registeredHandlers := make(map[string]sdk.ToolHandler)
|
|
sdk := sdk.New("openclaw", sdk.SDKConfig{
|
|
Settings: &mockSettings{},
|
|
RegTool: func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
|
registeredTools = append(registeredTools, name)
|
|
registeredHandlers[name] = handler
|
|
return 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)
|
|
}
|
|
}
|