Files
HomeAgent/internal/plugin/entry_dispatch_test.go
JianFeeeee 7213edd181 docs: 全面按当前源码更新文档 + 删除已过时文档
## 删除(内容已落地/已被替换,保留只会误导)
- demo.md ................... failback 与 recoverydiag 均已实现,0 引用
- docs/defect-qq-output-send-loop.md .. 已修复(本身也标了「已修复」),0 引用
- docs/embedding-comparison.md ....... 一次性选型报告,仅被 agent 产物引用
- docs/zh/plan.md ............ 描述的旧 nav 布局已重写、死配置已清,全部完成
- docs/zh/plugin-migration-plan.md ... 迁移已上生产,纯过程稿(Part 0~6 全完成)

## 更新(按当前源码核对)
- assets/docs/{zh,en}/ARCHITECTURE.md(README 指向的用户文档,最重要):
  把只讲 cancel/intercept 的旧「中断机制」章节重写为「输入调度器与中断机制」——
  补上两类别 + 四级中断(L1~L4,默认 L1、外部插件 L4 夹到 L3)+ 抢占/挂起/中断栈
  + 饥饿防护(PreemptCount 提升,封顶 L4)+ 抢占冷却(2s)+ 停止语义(cancelBudget)
  + 驻留子/分诊助手/残余任务;新增「上下文预算」章节(窗口 ≠ 工作面,600K 封顶,
  预算是上限非填充目标)。中英章节数现已对齐(各 13 节)。
- assets/docs/{zh,en}/PLUGIN_DEV.md:插件示例表补 6 个缺失项
  (acp/deepsearch/plugindev/recoverydiag/vanblog/vikunja);qq 工具数 17 → 20(实测)。
- README.md / README_EN.md:补 v1.3.x 线(此前只到 v1.2.0,而 1.3.x 已发布 12 个 patch)——
  驻留式子 agent、输出通道寻址、输入调度器、轻量内核 profile、积压及时反馈。
- plan.md:开头两个「⚠️ 紧急/正在持续污染」是过期告警(实测  残留 = 0),
  改为「已解决」并加文档定位说明;§13 仍是活跃路线图故保留。
- docs/zh/plugin-interface-matrix.md + 两处源码注释:清理指向已删文档的断链。

全仓 md 断链检查:仅剩 1 处,位于 third_party 的 oh_modules(第三方依赖,非本项目)。
2026-09-19 19:14:55 +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 分派(见 docs/zh/架构迁移评估.md 的入口双通道章节)。
//
// 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)
}
}