mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 23:07:30 +00:00
批 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 后缀
154 lines
5.9 KiB
Go
154 lines
5.9 KiB
Go
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)
|
||
}
|