Files
HomeAgent/internal/plugin/entry_dispatch_test.go
JianFeeeee c0274b71d5 docs: 删除迁移期临时文档,现行内容搬进正式文档
用户指出迁移评估那批是**过程性临时文档**,迁移已完成就该退场。

## 删除(38 个文件)
- docs/zh/架构迁移评估.md(1621 行)—— 评估稿。开头的「现网正在发生的问题」
  (output_send 永远成功 / cgo 超时泄漏 26 次 / stage 污染)**全部已修复**,
  留着是误导性告警。其 §三「目标架构」已被 ARCHITECTURE.md 完整覆盖
  (且后者更细,含子进程生命周期管理)。
- docs/zh/plugin-interface-matrix.md(428 行)—— 迁移基线矩阵。
- docs/zh/experiments/(36 文件)—— 18 项可行性实验,验证的是"该不该迁移",
  迁移早已完成;实测无任何构建/测试依赖它。

## 现行内容先搬走(不能随临时文档一起丢)
- plugin-interface-matrix §九「接口扩展规则」→ 搬进 docs/git-branching.md 新增 §八
  (只增不减/签名不改、新增必须"插件调用内核实现"方向、hmapdev 模板必须同步接线
  否则全体插件编译失败、"接口纯追加"≠"无需重编"、合回 main 的同步清单)。
- git-branching §六 原写「接口冻结是合回门禁」—— 冻结是**迁移期**约束,v1.1.x 起
  已到期,改为标注失效并指向 §八。

## 引用清理
8 处引用全部改指现行文档:plan.md ×3、两篇设计文档各 ×1、
4 处源码注释(proc/shm.go、proc/process.go、dynamic_proc.go、entry_dispatch_test.go、
proc/bench_test.go)。仅 third_party(SDK 独立仓)保留 1 处,不动。

## 验证
- `go build ./...` 通过;`go test ./internal/plugin/...` 两个包全绿
- 本项目文档**断链 0**(另 2 处断链在 oh_modules 第三方依赖内)
2026-09-19 19:22:48 +08:00

201 lines
6.3 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 plugin
import (
"os"
"path/filepath"
"strings"
"testing"
)
// entry 分派:按 manifest 的 entry 把插件分派到 proc / lua / skill 三条通道。
//
// C ABI 通道(.so/.dll/.dylib已整体退场外部插件统一走子进程 + stdio RPC。
// 这些测试守住的是「旧产物给明确错误」而非「静默跳过」——后者会让
// 「插件目录在但没加载」看起来像配置问题。
func TestClassifyEntry(t *testing.T) {
cases := []struct {
entry string
want entryKind
}{
{"plugin.bin", entryProc},
{"main.lua", entryLua},
{"SKILL.md", entrySkill},
{"", entryUnknown},
{"plugin.wasm", entryUnknown},
// 已退场的 C ABI 产物不再是有效通道
{"plugin.so", entryUnknown},
{"plugin.dll", entryUnknown},
{"plugin.dylib", entryUnknown},
}
for _, c := range cases {
if got := classifyEntry(c.entry); got != c.want {
t.Errorf("classifyEntry(%q) = %v, want %v", c.entry, got, c.want)
}
}
}
// manifest 显式声明的 entry 优先级最高。
func TestDetectEntryKind_ManifestWins(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "main.lua"), "fake lua")
mustWrite(t, filepath.Join(dir, "plugin.bin"), "fake bin")
mustWrite(t, filepath.Join(dir, metaEntry), `{"name":"x","entry":"main.lua"}`)
if got := detectEntryKind(dir); got != entryLua {
t.Fatalf("manifest 声明 main.lua 应走 lua实际 %v", got)
}
}
// 存量插件的 plugin.json 仍写着 "plugin.so"(历史产物),
// 此时 classifyEntry 返回 unknown须靠目录探测找到 plugin.bin。
//
// 这是「外部插件零改动」的直接后果17 个插件的 manifest 没人去改。
func TestDetectEntryKind_LegacyManifestFallsBackToProbe(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "plugin.bin"), "fake bin")
mustWrite(t, filepath.Join(dir, metaEntry), `{"name":"x","entry":"plugin.so"}`)
if got := detectEntryKind(dir); got != entryProc {
t.Fatalf("manifest 写 plugin.so 但目录有 plugin.bin 时应走 proc实际 %v", got)
}
}
func TestDetectEntryKind_ProbeFallbacks(t *testing.T) {
t.Run("only bin", func(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "plugin.bin"), "x")
if got := detectEntryKind(dir); got != entryProc {
t.Fatalf("got %v", got)
}
})
t.Run("only lua", func(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "main.lua"), "x")
if got := detectEntryKind(dir); got != entryLua {
t.Fatalf("got %v", got)
}
})
t.Run("only skill", func(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "SKILL.md"), "x")
if got := detectEntryKind(dir); got != entrySkill {
t.Fatalf("got %v", got)
}
})
t.Run("empty dir", func(t *testing.T) {
if got := detectEntryKind(t.TempDir()); got != entryUnknown {
t.Fatalf("空目录应为 unknown实际 %v", got)
}
})
t.Run("only legacy so", func(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "plugin.so"), "x")
if got := detectEntryKind(dir); got != entryUnknown {
t.Fatalf("只有 .so 时应为 unknownC ABI 已退场),实际 %v", got)
}
})
}
// C ABI 残留必须能被识别,供 tryDynamic 给出「需要重编」的明确错误。
func TestHasLegacyCABIEntry(t *testing.T) {
for _, name := range []string{"plugin.so", "plugin.dll", "plugin.dylib"} {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, name), "x")
if !hasLegacyCABIEntry(dir) {
t.Errorf("%s 应被识别为 C ABI 残留", name)
}
}
t.Run("clean dir", func(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "plugin.bin"), "x")
if hasLegacyCABIEntry(dir) {
t.Error("只有 plugin.bin 的目录不应被判为 C ABI 残留")
}
})
}
// 旧 .so 插件必须报「用新 hmapdev 重编」而非静默跳过。
func TestTryDynamic_LegacyCABIGivesActionableError(t *testing.T) {
r := NewRegistry()
defer r.closeProcHost()
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "plugin.so"), "old cabi binary")
_, err := r.tryDynamic(dir, "legacy", nil)
if err == nil {
t.Fatal("旧 C ABI 产物应报错,不得静默跳过")
}
// 错误消息须指向解决办法,且明确业务代码无需改
msg := err.Error()
for _, want := range []string{"hmapdev", "plugin.bin", "业务代码"} {
if !strings.Contains(msg, want) {
t.Errorf("错误消息应含 %q实际: %v", want, err)
}
}
}
// entry 声明 plugin.bin 但二进制缺失时返回 nil,nil交由后续探测
func TestTryLoadProc_MissingBinaryReturnsNil(t *testing.T) {
dir := t.TempDir()
plg, err := tryLoadProc(dir, "demo", nil)
if plg != nil || err != nil {
t.Fatalf("无 plugin.bin 应返回 nil,nil实际 plg=%v err=%v", plg, err)
}
}
func TestTryLoadProc_NonExecutableRejected(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, binEntry)
mustWrite(t, path, "not executable")
if err := os.Chmod(path, 0o644); err != nil {
t.Fatalf("chmod: %v", err)
}
_, err := tryLoadProc(dir, "demo", nil)
if err == nil {
t.Fatal("缺少可执行权限应报错")
}
}
// pluginEntryHash 的候选顺序须与 detectEntryKind 一致plugin.bin 优先),
// 否则增量重载会用错文件算 hash导致"换了 .bin 但内核以为没变"。
func TestPluginEntryHash_UsesBin(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "plugin.bin"), "bin content")
h1 := pluginEntryHash(dir)
if h1 == "" {
t.Fatal("应算出 hash")
}
mustWrite(t, filepath.Join(dir, "plugin.bin"), "bin content CHANGED")
if h2 := pluginEntryHash(dir); h2 == h1 {
t.Error("plugin.bin 变化必须反映到 hash否则增量重载失效")
}
}
// C ABI 产物不再参与 hash 计算:内核已不认它,把它算进去会让
// 「换了 .so」触发一次无意义的重载尝试。
func TestPluginEntryHash_IgnoresLegacyCABI(t *testing.T) {
dir := t.TempDir()
mustWrite(t, filepath.Join(dir, "plugin.so"), "so content")
if h := pluginEntryHash(dir); h != "" {
t.Errorf("只有 .so 时应返回空串C ABI 已退场),实际 %q", h)
}
}
func TestPluginEntryHash_EmptyForFactoryOnlyPlugin(t *testing.T) {
if h := pluginEntryHash(t.TempDir()); h != "" {
t.Errorf("无入口文件应返回空串(内置纯工厂插件),实际 %q", h)
}
}
func mustWrite(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
t.Fatalf("写 %s: %v", path, err)
}
}