mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
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:
140
internal/plugins/pluginmgr/bundle_install_test.go
Normal file
140
internal/plugins/pluginmgr/bundle_install_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user