Files
HomeAgent/internal/plugins/pluginmgr/bundle_install_test.go
JianFeeeee a35f2126a1 test(pluginmgr): 校验发布用插件包能被内核真实安装
配套 SDK 仓新增的 scripts/build_plugin_bundles.sh:**能构建出来 ≠ 内核装得上**,
这个测试用内核自己的 extractPackage 把产物真解一遍,验证三种包形态都落成规范入口。

覆盖的三种形态(都由真实产物验证过):
- 多平台 bundle:`plugin.bin.<os>.<arch>` → 按当前平台挑出并**重命名为 plugin.bin**
- 单平台包(qq 的 plg.json 是 bundle:false):只有 `plugin.bin`
- Lua 包(luademo):入口是 `main.lua`,不编译 Go

不设 HMAP_BUNDLE_DIR 时 skip(不作为常规 CI 的必跑项,避免依赖 hmapdev 工具链):

    HMAP_BUNDLE_DIR=/path/to/plugins go test ./internal/plugins/pluginmgr/ \
      -run TestBuildPluginBundlesInstallable -v

实测 21 个真实产物全部通过(含 Lua 与单平台两种非 bundle 形态)。
2026-09-20 09:02:42 +08:00

141 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pluginmgr
import (
"archive/zip"
"bytes"
"os"
"path/filepath"
"testing"
)
// TestBuildPluginBundlesInstallable 校验「发布用插件包」能被内核真实解包安装。
//
// 为什么要这个测试:release 此前只发 homed/waiter 与 hmapdev 工具链,**不发插件包**,
// 用户得自己装 Go + hmapdev 逐插件构建。现在发布流程会带上 .hmap(见 SDK 仓的
// scripts/build_plugin_bundles.sh),但「能构建出来」不等于「内核装得上」——
// 包内可能是 bundle 形态(plugin.bin.<os>.<arch>),也可能是单平台或 Lua 形态,
// 三种都要能被 extractPackage 正确处理并落到规范名 plugin.bin。
//
// 测试用内核自己的 extractPackage(而非测试里另写一份解包),所以它验证的是
// 真实安装路径。
func TestBuildPluginBundlesInstallable(t *testing.T) {
dir := os.Getenv("HMAP_BUNDLE_DIR")
if dir == "" {
t.Skip("未设 HMAP_BUNDLE_DIR(指向 build_plugin_bundles.sh 的产出目录)")
}
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("读产物目录: %v", err)
}
n := 0
for _, e := range entries {
name := e.Name()
if filepath.Ext(name) != ".hmap" {
continue
}
data, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
t.Fatalf("%s: 读文件: %v", name, err)
}
// 每个包解到独立目录,互不干扰
tmp := t.TempDir()
if err := extractPackage(data, tmp); err != nil {
t.Errorf("%s: extractPackage 失败: %v", name, err)
continue
}
// 解包后必须有一个规范入口可执行文件(plugin.bin / main.lua / SKILL.md)
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
t.Errorf("%s: 重新读 zip: %v", name, err)
continue
}
var pkgName string
for _, f := range zr.File {
if f.Name == "plugin.json" {
rc, _ := f.Open()
buf := new(bytes.Buffer)
buf.ReadFrom(rc)
rc.Close()
// 只要 name 字段,避免为此引 json(本文件保持轻量)
pkgName = extractField(buf.String(), "name")
break
}
}
if pkgName == "" {
t.Errorf("%s: 包内 plugin.json 无 name", name)
continue
}
installed := filepath.Join(tmp, pkgName)
var okEntry string
for _, cand := range []string{"plugin.bin", "main.lua", "SKILL.md"} {
if _, err := os.Stat(filepath.Join(installed, cand)); err == nil {
okEntry = cand
break
}
}
if okEntry == "" {
// 列出实际落了什么,便于定位
got, _ := os.ReadDir(installed)
var names []string
for _, g := range got {
names = append(names, g.Name())
}
t.Errorf("%s: 安装目录 %s 下无规范入口(plugin.bin/main.lua/SKILL.md),实际: %v",
name, pkgName, names)
continue
}
// 入口必须非空且可执行位(内核按子进程 spawn)
fi, err := os.Stat(filepath.Join(installed, okEntry))
if err != nil || fi.Size() == 0 {
t.Errorf("%s: 入口 %s 为空或不可读", name, okEntry)
continue
}
n++
t.Logf("✓ %-46s → %s/%s (%.1f MB)", name, pkgName, okEntry, float64(fi.Size())/1048576)
}
if n == 0 {
t.Fatal("没有任何 .hmap 通过安装校验")
}
t.Logf("共 %d 个插件包通过内核解包校验", n)
}
// extractField 从 JSON 文本里取一个字符串字段(本文件不想为此引 encoding/json)。
func extractField(js, key string) string {
pat := `"` + key + `"`
i := indexOf(js, pat)
if i < 0 {
return ""
}
rest := js[i+len(pat):]
j := indexOf(rest, ":")
if j < 0 {
return ""
}
rest = rest[j+1:]
k := indexOf(rest, `"`)
if k < 0 {
return ""
}
rest = rest[k+1:]
m := indexOf(rest, `"`)
if m < 0 {
return ""
}
return rest[:m]
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}