mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +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 后缀
309 lines
11 KiB
Go
309 lines
11 KiB
Go
package plugin
|
||
|
||
// 插件**运行态 vs 持久态**分离的判据(2026-10-16)。
|
||
//
|
||
// 设计见 docs/zh/kernel-modularization-design.md §2.6.7。
|
||
//
|
||
// ## 这组判据守的是什么
|
||
//
|
||
// 用户指出"只有 reload" —— 实测确认现有 API 全都是
|
||
// "改运行态 **顺带** 改持久态":
|
||
//
|
||
// DisablePlugin → cfgReg.AddDisabledPlugin (持久禁用)
|
||
// Enable → cfgReg.RemoveDisabledPlugin (解除持久禁用)
|
||
//
|
||
// 于是 agent **无法**"只是现在关掉它"而不影响开机自启。
|
||
// 这组判据要求:运行态操作**绝不**触碰持久状态。
|
||
//
|
||
// ## 判据必须测真实加载路径
|
||
//
|
||
// 本仓教训(重复 10+ 次):只测 helper 会假绿。
|
||
// 这里用**真工厂 + 真 loadOne**(经 Registry.Load),并断言
|
||
// ① 持久配置真的没被写 ② 插件真的被停/起。
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
|
||
isdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
|
||
)
|
||
|
||
// fakeLifecyclePlugin 记录生命周期调用(真工厂产出的插件)。
|
||
type fakeLifecyclePlugin struct {
|
||
name string
|
||
starts int
|
||
stops int
|
||
}
|
||
|
||
func (p *fakeLifecyclePlugin) Name() string { return p.name }
|
||
func (p *fakeLifecyclePlugin) Start(s *isdk.PluginSDK) error {
|
||
p.starts++
|
||
return nil
|
||
}
|
||
func (p *fakeLifecyclePlugin) Stop() error {
|
||
p.stops++
|
||
return nil
|
||
}
|
||
|
||
// newRuntimeStateReg 建一个可真实加载 `alpha` 的 Registry(含临时数据目录)。
|
||
func newRuntimeStateReg(t *testing.T) (*Registry, string, *fakeLifecyclePlugin) {
|
||
t.Helper()
|
||
dir := t.TempDir()
|
||
plgDir := filepath.Join(dir, "plugins")
|
||
if err := os.MkdirAll(plgDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r := NewRegistry()
|
||
r.SetPluginDir(plgDir)
|
||
r.SetDataDir(dir)
|
||
// ★ 必须装一个**真** cfgReg:持久禁用状态就存在它里面,
|
||
// 而 isDisabled 在 cfgReg==nil 时恒返回 false(判据会假绿)。
|
||
// 这是"必须测真实入口"的又一具体落点。
|
||
reg := internalConfig.NewConfigRegistry(filepath.Join(dir, "config.db"))
|
||
t.Cleanup(func() { reg.Close() })
|
||
r.SetConfigRegistry(reg)
|
||
|
||
fp := &fakeLifecyclePlugin{name: "alpha"}
|
||
r.RegisterNative("alpha", func(name string, cfg map[string]interface{}) (isdk.Plugin, error) {
|
||
return fp, nil
|
||
})
|
||
return r, dir, fp
|
||
}
|
||
|
||
// ★★★ 判据:**StopPlugin 不写持久禁用配置**(运行态 ≠ 持久态)。
|
||
//
|
||
// 这是本组最核心的一条。若实现里误调了 DisablePlugin(或它内部的
|
||
// AddDisabledPlugin),用户下次开机会发现"我只是关了一下,怎么不启动了"。
|
||
func TestStopPluginDoesNotTouchPersistentState(t *testing.T) {
|
||
r, dir, fp := newRuntimeStateReg(t)
|
||
if err := r.Load(dir + "/plugins"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if fp.starts == 0 {
|
||
t.Fatal("前置失败:插件未被真实加载")
|
||
}
|
||
|
||
if err := r.StopPlugin("alpha"); err != nil {
|
||
t.Fatalf("StopPlugin: %v", err)
|
||
}
|
||
|
||
// ① 真的停了
|
||
if fp.stops == 0 {
|
||
t.Fatal("★ StopPlugin 没有真的停掉插件(只改了标记)")
|
||
}
|
||
// ② 运行态门置位
|
||
if !r.IsPluginRuntimeStopped("alpha") {
|
||
t.Fatal("StopPlugin 后应处于运行态关闭")
|
||
}
|
||
// ③ ★ 持久态**未变**:IsPluginDisabled 必须仍为 false
|
||
if r.IsPluginDisabled("alpha") {
|
||
t.Fatal("★ StopPlugin 写了持久禁用 —— 用户下次开机会发现插件不自启了" +
|
||
"(这正是要修的行为)")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**StartPlugin 不解除持久禁用**(且被持久禁用时拒绝)。
|
||
//
|
||
// 反向的误用:若 StartPlugin 调了 Enable(它内部 RemoveDisabledPlugin),
|
||
// 就会把用户"明确禁止这个插件"的决定悄悄改掉。
|
||
func TestStartPluginRefusesPersistentlyDisabled(t *testing.T) {
|
||
r, dir, _ := newRuntimeStateReg(t)
|
||
if err := r.Load(dir + "/plugins"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// 持久禁用(模拟用户的明确决定)
|
||
if err := r.DisablePlugin("alpha", "test"); err != nil {
|
||
t.Fatalf("DisablePlugin: %v", err)
|
||
}
|
||
if !r.IsPluginDisabled("alpha") {
|
||
t.Fatal("前置失败:应已持久禁用")
|
||
}
|
||
|
||
// ★ StartPlugin 必须**拒绝**(不能绕过用户的明确决定)。
|
||
//
|
||
// ★ 断言错误**原因**,不能只看 err != nil:
|
||
// 去掉我那句显式检查后,loadOne 里还有一道 isDisabled 门会挡住 ⇒
|
||
// StartPlugin 仍返回错误("启动插件失败")⇒ 只查非 nil 就**空转**了。
|
||
// 必须要求原因指向"持久禁用",才能区分"按预期拒绝"与"因别的原因失败"。
|
||
err := r.StartPlugin("alpha")
|
||
if err == nil {
|
||
t.Fatal("★ StartPlugin 加载了被持久禁用的插件 —— 绕过了用户的明确决定")
|
||
}
|
||
if !strings.Contains(err.Error(), "持久禁用") {
|
||
t.Fatalf("★ StartPlugin 拒绝的原因不是\"持久禁用\"(判据空转,无法区分真正原因): %v", err)
|
||
}
|
||
// 且必须仍处于持久禁用(不得被这次尝试清掉)
|
||
if !r.IsPluginDisabled("alpha") {
|
||
t.Fatal("★ StartPlugin 悄悄清除了持久禁用标记")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**StopPlugin → StartPlugin 能往返**(运行态关闭可被清除)。
|
||
func TestStopThenStartRoundTrip(t *testing.T) {
|
||
r, dir, fp := newRuntimeStateReg(t)
|
||
if err := r.Load(dir + "/plugins"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
if err := r.StopPlugin("alpha"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !r.IsPluginRuntimeStopped("alpha") {
|
||
t.Fatal("应处于运行态关闭")
|
||
}
|
||
|
||
// 启动:应清门并重新加载
|
||
startsBefore := fp.starts
|
||
if err := r.StartPlugin("alpha"); err != nil {
|
||
t.Fatalf("StartPlugin: %v", err)
|
||
}
|
||
if r.IsPluginRuntimeStopped("alpha") {
|
||
t.Fatal("★ StartPlugin 没有清掉运行态门(下次 loadOne 会被它挡住)")
|
||
}
|
||
if fp.starts <= startsBefore {
|
||
t.Fatal("★ StartPlugin 没有真的重新加载")
|
||
}
|
||
// 持久态全程未变
|
||
if r.IsPluginDisabled("alpha") {
|
||
t.Fatal("往返过程动了持久态")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**运行态关闭不持久**(重启后插件正常加载)。
|
||
//
|
||
// 这是有意设计:运行态关闭 = "这一次不跑",不是"以后都不跑"。
|
||
// 若它持久,agent 关掉一个插件后就再也没法让它随内核起来。
|
||
func TestRuntimeStopIsNotPersistent(t *testing.T) {
|
||
r, dir, _ := newRuntimeStateReg(t)
|
||
if err := r.Load(dir + "/plugins"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := r.StopPlugin("alpha"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !r.IsPluginRuntimeStopped("alpha") {
|
||
t.Fatal("前置失败")
|
||
}
|
||
|
||
// 模拟"重启":新建一个 Registry(不共享任何内存状态)
|
||
r2, dir2, fp2 := newRuntimeStateReg(t)
|
||
_ = dir2
|
||
r2.SetPluginDir(r.PluginDir())
|
||
r2.RegisterNative("alpha", func(name string, cfg map[string]interface{}) (isdk.Plugin, error) {
|
||
return fp2, nil
|
||
})
|
||
if r2.IsPluginRuntimeStopped("alpha") {
|
||
t.Fatal("★ 运行态关闭跨实例保留了 —— 它必须是会话级的(重启即复位)")
|
||
}
|
||
// 重启后应能正常加载
|
||
if err := r2.Load(r.PluginDir()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if fp2.starts == 0 {
|
||
t.Fatal("★ 重启后被运行态关闭过的插件未能加载 —— " +
|
||
"agent 关掉它之后就再也起不来了(正是要修的毛病)")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**持久禁用仍走原来的门**(不能因新门而失效)。
|
||
//
|
||
// 持久禁用优先于运行态:被持久禁用的插件即使开始未运行态关闭,也不得加载。
|
||
func TestPersistentDisableStillBlocksLoad(t *testing.T) {
|
||
r, dir, fp := newRuntimeStateReg(t)
|
||
if err := r.DisablePlugin("alpha", "test"); err != nil {
|
||
t.Fatalf("DisablePlugin: %v", err)
|
||
}
|
||
if err := r.Load(dir + "/plugins"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if fp.starts != 0 {
|
||
t.Fatal("★ 持久禁用的插件被加载了(旧门失效)")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**pluginmgr 与 webui 不能被关闭/禁用**(防自锁)。
|
||
//
|
||
// 为何必须在内核层面拦:agent 可以犯错,而这两个操作一旦执行,
|
||
// agent **无法从自己这侧恢复**(要启动 pluginmgr 得先有 pluginmgr)。
|
||
func TestRefuseSelfLockOnCriticalPlugins(t *testing.T) {
|
||
r := NewRegistry()
|
||
|
||
// ★ 判据要断言三件事,不能只看 err != nil:
|
||
//
|
||
// 我第一次只查 `err == nil` ⇒ **空转**:去掉守卫后 StopPlugin 仍会报
|
||
// "plugin pluginmgr 未在运行"(因为没注册工厂),判据照样通过。
|
||
// 错误**原因**不同却无法区分,是判据失焦。
|
||
//
|
||
// 正确的断言:
|
||
// ① 返回错误 ② 错误信息点明"拒绝"(自锁守卫的措辞)
|
||
// ③ **状态未被改动**(这才是自锁的实质危害)
|
||
checkRefused := func(t *testing.T, err error, what string) {
|
||
t.Helper()
|
||
if err == nil {
|
||
t.Fatalf("★ 允许%s pluginmgr —— agent 无法从自己这侧恢复", what)
|
||
}
|
||
if !strings.Contains(err.Error(), "拒绝") {
|
||
t.Fatalf("★ %s 返回的错误不是自锁守卫给的(原因不明确,判据空转): %v", what, err)
|
||
}
|
||
}
|
||
|
||
checkRefused(t, r.StopPlugin("pluginmgr"), "运行态关闭")
|
||
// ③ 实质危害:门**不得**被置位
|
||
if r.IsPluginRuntimeStopped("pluginmgr") {
|
||
t.Fatal("★ 自锁守卫没拦住:pluginmgr 已被标记运行态关闭(它再也启动不回来了)")
|
||
}
|
||
|
||
checkRefused(t, r.DisablePlugin("pluginmgr", "agent"), "持久禁用")
|
||
// ③ 更糟的实质危害:持久禁用不得被写入
|
||
if r.IsPluginDisabled("pluginmgr") {
|
||
t.Fatal("★ 自锁守卫没拦住:pluginmgr 已被**持久禁用**(重启也不恢复)")
|
||
}
|
||
|
||
checkRefused(t, r.StopPlugin("webui"), "运行态关闭")
|
||
|
||
// 普通插件**不受**自锁守卫影响(守卫不能不问对象)(守卫不能误伤)
|
||
err := r.StopPlugin("someplugin")
|
||
if err == nil {
|
||
t.Fatal("未安装的普通插件应返回错误")
|
||
}
|
||
if strings.Contains(err.Error(), "拒绝") {
|
||
t.Fatalf("★ 自锁守卫误伤了普通插件: %v", err)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**StopPlugin 摸掉注册面**(否则旧闭包指向已死实例)。
|
||
//
|
||
// 与 ReloadOne/Disable 同一处置:不摸的后果是模型仍能看到旧工具并调用,
|
||
// 只能拿到 ErrProcessExited(通道/工具永久指向死实例)。
|
||
func TestStopPluginDetachesRegistrationSurface(t *testing.T) {
|
||
r, dir, _ := newRuntimeStateReg(t)
|
||
if err := r.Load(dir + "/plugins"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r.mu.Lock()
|
||
_, hadChannels := r.pluginChannels["alpha"]
|
||
_, hadSDK := r.sdkRefs["alpha"]
|
||
r.mu.Unlock()
|
||
|
||
if err := r.StopPlugin("alpha"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
r.mu.Lock()
|
||
_, stillLoaded := r.plugins["alpha"]
|
||
_, stillSDK := r.sdkRefs["alpha"]
|
||
r.mu.Unlock()
|
||
if stillLoaded {
|
||
t.Fatal("★ StopPlugin 后插件仍在注册表里")
|
||
}
|
||
if stillSDK {
|
||
t.Fatal("★ StopPlugin 后 sdkRefs 未清 —— 旧 SDK 引用指向已死实例")
|
||
}
|
||
// 记录一下前置状态(避免 unused 警告同时留下诊断信息)
|
||
t.Logf("前置: channels=%v sdk=%v", hadChannels, hadSDK)
|
||
}
|