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 形态)。
This commit is contained in:
JianFeeeee
2026-09-20 09:02:42 +08:00
parent 9b26db45bc
commit a35f2126a1

View File

@ -0,0 +1,140 @@
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
}