Files
HomeAgent/internal/plugins/pluginmgr/lifecycle_tools_test.go
JianFeeeee 4e70b8d5d8 refactor(module): 核心仓 Go module 路径改为 github.com/JianFeeeee/HologramAgent
批 1,不含 SDK 仓与目录改名。

- go.mod 第 1 行 module 路径
- 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换
  (857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5)
- 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、
  homed/waiter 可执行名、.hmap 后缀
2026-10-21 12:17:19 +08:00

154 lines
5.9 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
// plugin_start/stop/enable/disable 工具的判据(2026-10-16)。
//
// 设计见 docs/zh/kernel-modularization-design.md §2.6.7.4。
//
// ## 判据重点:**工具的措辞必须让 agent 选对**
//
// 四个工具里两两一对,差别只在**是否持久**:
//
// plugin_stop vs plugin_disable (停掉 vs 停掉且以后也不自启)
// plugin_start vs plugin_enable (拉起 vs 拉起且以后开机也自启)
//
// agent 只能靠 Description 分辨。若描述不区分,agent 会在"临时关一下"
// 的场景误用 plugin_disable ⇒ 用户下次开机发现插件不自启(真实缺陷场景)。
//
// ## 判据必须走真实 registerTools
//
// 本仓教训:只查"源码里有这个字符串"会被 `if false &&` 之类的
// 写法骗过(E-3 那次实测)。所以这里真的调 registerTools 并捕获注册结果。
import (
"os"
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/sdk"
)
// collectToolDefs 走真实 registerTools 收集工具定义。
//
// 捕获点是 `SDKConfig.RegTool`(真实注册落点),不是别处的副本 ——
// 这样"某工具被删掉/改名"必然反映在这里。
func collectToolDefs(t *testing.T) map[string]sdk.ToolDef {
t.Helper()
defs := map[string]sdk.ToolDef{}
sdkInst := sdk.New("pluginmgr", sdk.SDKConfig{
Settings: &pmSettings{},
EventBus: events.NewBus(),
PluginMgr: &fakePluginMgr{},
RegTool: func(name string, def sdk.ToolDef, h sdk.ToolHandler) error {
defs[name] = def
return nil
},
})
p := &Plugin{name: "pluginmgr"}
p.registerTools(sdkInst)
if len(defs) == 0 {
t.Fatal("registerTools 未注册任何工具 —— 捕获点失效,本文件全部判据会假绿")
}
return defs
}
// ★★★ 判据:**四个生命周期工具全部注册**(走真实注册路径)。
func TestLifecycleToolsAreRegistered(t *testing.T) {
defs := collectToolDefs(t)
for _, want := range []string{"plugin_start", "plugin_stop", "plugin_enable", "plugin_disable"} {
if _, ok := defs[want]; !ok {
t.Errorf("★ 缺少工具 %s —— agent 无法%s插件", want, want)
}
}
// 原有 6 个不得因新增而丢失(回归)
for _, want := range []string{"plugin_install", "plugin_list", "plugin_status",
"plugin_restart", "plugin_remove", "plugin_info"} {
if _, ok := defs[want]; !ok {
t.Errorf("★ 原有工具 %s 丢失(回归)", want)
}
}
}
// ★★★ 判据:**运行态与持久态工具的描述必须互相点名**。
//
// 这是防止 agent 误用的**唯一手段**(工具选择完全靠描述)。
func TestRuntimeVsPersistentToolDescriptionsDisambiguate(t *testing.T) {
defs := collectToolDefs(t)
// ★ 两条**独立**断言(不能用 || 合并):
//
// 第一版写成 `!A && !B` ⇒ 去掉 A("不改持久状态")后 B("不会影响开机自启")
// 仍在 ⇒ 整条通过 ⇒ **变异没咬住**。两个措辞各有作用:
// A = 直接陈述"不改持久"(正面定义)
// B = 描述用户可观察的后果(背面印证)
// 缺任一都让 agent 更难判断,所以**两条都要在**。
stop := defs["plugin_stop"].Description
if !strings.Contains(stop, "不改持久状态") {
t.Errorf("★ plugin_stop 描述未出现\"不改持久状态\"(正面定义缺失)\n%s", stop)
}
if !strings.Contains(stop, "不会影响开机自启") {
t.Errorf("★ plugin_stop 描述未出现\"不会影响开机自启\"(后果说明缺失)\n%s", stop)
}
if !strings.Contains(stop, "plugin_disable") {
t.Errorf("★ plugin_stop 描述未指向 plugin_disable(持久关闭)作为对比\n%s", stop)
}
dis := defs["plugin_disable"].Description
if !strings.Contains(dis, "跨重启保留") && !strings.Contains(dis, "持久") {
t.Errorf("★ plugin_disable 描述未说明它是**持久**的\n%s", dis)
}
if !strings.Contains(dis, "plugin_stop") {
t.Errorf("★ plugin_disable 描述未指向 plugin_stop(临时关闭)作为对比\n%s", dis)
}
if st := defs["plugin_start"].Description; !strings.Contains(st, "持久") {
t.Errorf("★ plugin_start 描述未提持久禁用的处理\n%s", st)
}
if en := defs["plugin_enable"].Description; !strings.Contains(en, "清除") || !strings.Contains(en, "自启") {
t.Errorf("★ plugin_enable 描述未说明它会让之后开机自启\n%s", en)
}
}
// ★★★ 判据:**四个工具各自转调不同的 API**(不是别名)。
//
// 若 plugin_stop 内部转调 DisablePlugin(或反过来),两者行为相同
// ⇒ 上述描述全是谎话。这里对每个工具的 handler 做"源码块内包含期望调用"检查。
//
// ★ 为何能接受源码检查(本仓曾因此吃亏):这里检查的是**期望调用一定存在**,
// 是单向的(存在性),不会被 `if false &&` 骗过 —— 那种写法仍需写出该调用。
// 真正的行为验证在 internal/plugin 包的 runtime_state_test.go(真加载真启停)。
func TestRuntimeToolsCallDistinctAPIs(t *testing.T) {
src := readPluginSource(t)
cases := []struct{ tool, want string }{
{"plugin_start", "mgr.StartPlugin("},
{"plugin_stop", "mgr.StopPlugin("},
{"plugin_enable", "mgr.EnablePlugin("},
{"plugin_disable", "mgr.DisablePlugin("},
}
for _, c := range cases {
idx := strings.Index(src, `s.RegisterTool("`+c.tool+`"`)
if idx < 0 {
t.Fatalf("未找到 %s 的注册", c.tool)
}
rest := src[idx:]
// 截到下一个 RegisterTool
if nxt := strings.Index(rest[len(c.tool)+20:], `s.RegisterTool(`); nxt >= 0 {
rest = rest[:len(c.tool)+20+nxt]
}
if !strings.Contains(rest, c.want) {
t.Errorf("★ %s 的 handler 未调用 %s —— 可能误指向另一个 API(行为与描述不符)",
c.tool, c.want)
}
}
}
// readPluginSource 读本包源码(判据的静态检查用)。
func readPluginSource(t *testing.T) string {
t.Helper()
b, err := os.ReadFile("plugin.go")
if err != nil {
t.Fatalf("读 plugin.go: %v", err)
}
return string(b)
}