diff --git a/internal/plugin/autostart_gate_test.go b/internal/plugin/autostart_gate_test.go deleted file mode 100644 index 5c81c68a..00000000 --- a/internal/plugin/autostart_gate_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package plugin - -// 外部插件的三种启动形态(2026-10-18 补)。 -// -// ## 缺陷 -// -// `plugin.json` 的 `auto_start` 字段一路写得出来(hmapdev 打包会写), -// 但 **Load 的目录扫描从不读它**:`loadOne` 里只有持久禁用门与运行态门, -// 没有 auto_start 门。也就是说这个字段在外部插件上**完全不生效** —— -// 声明 `auto_start: false` 的插件照样开机自启。 -// -// (内置模块那条走 kernelboot/scheduler.go 的 p.AutoStart,与外部插件是两条路, -// 所以这个缺陷只在外部插件上。) -// -// ## 三态语义(必须都成立) -// -// 自动启动 auto_start 缺省/true + 启动扫描 ⇒ 拉起 -// 启用但不自启 auto_start=false + 启动扫描 ⇒ 跳过 -// 同上 + plugin_start ⇒ 拉起(显式启动绕过) -// 禁用 持久禁用清单 + 任何路径 ⇒ 拒绝(连 plugin_start 也拒) -// -// ★ "启用但不自启" 必须能被 plugin_start 拉起:false 的含义是 -// "别在开机时拉起我",不是"永远不许启动"。少了这个区分, -// 按需加载的插件就再也起不来了 —— 比不声明更糟。 -// -// ## 判据方式 -// -// 真起一个 Registry + 临时插件目录(含 plugin.json),断言三种情况的加载结果。 -// 不用字符串匹配:那是"符号存在",不是"行为正确"。 - -import ( - "os" - "path/filepath" - "testing" - - isdk "github.com/JianFeeeee/HomeAgent/internal/sdk" -) - -// writeProcPlugin 造一个最小的"外部插件目录"。 -// -// 只写 plugin.json:本判据关心的是**是否被允许加载**, -// 而不是加载后能不能跑(那由 proc 层判据负责)。 -// loadOne 在 auto_start 门处就会返回,不会走到真正的进程启动。 -func writeProcPlugin(t *testing.T, root, name string, autoStart *bool) string { - t.Helper() - dir := filepath.Join(root, name) - if err := os.MkdirAll(dir, 0o755); err != nil { - t.Fatal(err) - } - m := `{"name":"` + name + `","version":"0.0.1","entry":"plugin.bin"` - if autoStart != nil && !*autoStart { - m += `,"auto_start":false` - } - m += "}" - if err := os.WriteFile(filepath.Join(dir, "plugin.json"), []byte(m), 0o644); err != nil { - t.Fatal(err) - } - return dir -} - -// ★★★ 判据:auto_start=false 在启动扫描时被跳过,但显式启动能真的加载起来。 -// -// ## 这一版为什么必须"真加载成功" -// -// 第一版只断言 loadOneOpt 返回 false/true。但 **false 有两个来源**: -// -// ① 被 auto_start 门拦住(正确) -// ② 根本没有 plugin.bin,加载失败(噪音) -// -// 两者无法区分 ⇒ 删掉 auto_start 门后判据照样"通过"(变异不咬,实测)。 -// -// 修法:用 RegisterNative 注册一个**真能 Start 的**假插件, -// 这样 loadOneOpt 返回 true 就必然意味着"门没拦它", -// 返回 false 就必然意味着"门拦了它"。语义唯一。 -func TestAutoStartFalseSkipsScanButAllowsExplicitStart(t *testing.T) { - root := t.TempDir() - f := false - dir := writeProcPlugin(t, root, "ondemand", &f) - - newReg := func() (*Registry, *fakeLifecyclePlugin) { - r := NewRegistry() - r.SetPluginDir(root) - p := &fakeLifecyclePlugin{name: "ondemand"} - r.RegisterNative("ondemand", func(name string, cfg map[string]interface{}) (isdk.Plugin, error) { - p.name = name - return p, nil - }) - return r, p - } - - // ① 启动扫描路径(explicitStart=false)⇒ 必须被 auto_start 门拦住 - r1, p1 := newReg() - if r1.loadOneOpt(dir, "ondemand", false) { - t.Fatal("★ auto_start=false 的插件在启动扫描时被加载了 —— " + - "该字段在外部插件上不生效(这是原始缺陷)") - } - if p1.starts != 0 { - t.Fatalf("★ 被拦住了但仍调用了 Start %d 次(门的位置不对)", p1.starts) - } - - // ② 显式启动路径(explicitStart=true)⇒ 必须真的加载并 Start - r2, p2 := newReg() - if !r2.loadOneOpt(dir, "ondemand", true) { - t.Fatal("★ 显式启动(plugin_start)也被 auto_start 门拦住了 —— " + - "声明 false 是\"别开机自启\",不是\"永远不许启动\";" + - "按需插件会再也起不来") - } - if p2.starts != 1 { - t.Fatalf("★ 显式启动没有真的调用 Start(starts=%d)", p2.starts) - } -} - -// ★★★ 判据:auto_start 缺省(未写)⇒ 启动扫描照常加载(不能误伤存量插件)。 -func TestAutoStartDefaultTrueStillLoads(t *testing.T) { - root := t.TempDir() - dir := writeProcPlugin(t, root, "defaulton", nil) - - r := NewRegistry() - r.SetPluginDir(root) - - if !r.shouldAutoStart(dir, "defaulton") { - t.Fatal("★ 未声明 auto_start 的插件被判为不自启 —— " + - "存量插件会全部不启动(灾难性)") - } -} - -// ★★★ 判据:auto_start=true 显式声明 ⇒ 自启。 -func TestAutoStartExplicitTrueLoads(t *testing.T) { - root := t.TempDir() - tr := true - dir := writeProcPlugin(t, root, "explicit", &tr) - - r := NewRegistry() - r.SetPluginDir(root) - - if !r.shouldAutoStart(dir, "explicit") { - t.Fatal("★ auto_start=true 的插件被判为不自启") - } -} - -// ★★★ 判据:manifest 读不到时**默认自启**(宁可多启,不要静默不启动)。 -func TestAutoStartMissingManifestDefaultsTrue(t *testing.T) { - root := t.TempDir() - dir := filepath.Join(root, "nomanifest") - if err := os.MkdirAll(dir, 0o755); err != nil { - t.Fatal(err) - } - r := NewRegistry() - r.SetPluginDir(root) - if !r.shouldAutoStart(dir, "nomanifest") { - t.Fatal("★ 读不到 plugin.json 时判为不自启 —— " + - "清单损坏会让插件静默不启动,比多启动一个更难查") - } -} diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index b2feb2bf..3467c6d8 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -574,21 +574,6 @@ func pluginEntryHash(plgDir string) string { } func (r *Registry) loadOne(plgDir, name string) bool { - return r.loadOneOpt(plgDir, name, false) -} - -// loadOneOpt 是 loadOne 的显式版本。 -// -// explicitStart=true 表示这是**agent 显式拉起**(plugin_start), -// 此时**忽略 auto_start**:插件声明"不自动启动"是"别在开机时拉起我", -// 不是"永远不许启动"。 -// -// ★ 2026-10-18 补:在此之前**外部插件的 auto_start 完全不生效** —— -// Load 的目录扫描无条件调 loadOne,没有任何地方读 plugin.json 的 -// auto_start。也就是说这个字段在 plg.json → plugin.json 一路写得出来, -// 内核却从不看它:声明 auto_start=false 的插件照样开机自启。 -// (内置模块那条走的是 kernelboot/scheduler.go,与外部插件是两条路。) -func (r *Registry) loadOneOpt(plgDir, name string, explicitStart bool) bool { if r.isDisabled(name) { log.Printf("[plugin] %s is disabled, skipping", name) r.mu.Lock() @@ -608,25 +593,6 @@ func (r *Registry) loadOneOpt(plgDir, name string, explicitStart bool) bool { return false } - // ③ auto_start 门(2026-10-18 补:此前外部插件从不检查这个字段)。 - // - // plugin.json 的 auto_start=false 表示"**启用**但不随内核自启", - // 用于按需加载的插件(纯工具型,没人调用时不该占进程与工具槽位)。 - // - // 三种启动形态在这里交汇: - // - // 自动启动 auto_start 缺省/true + 启动扫描 ⇒ 拉起 - // 启用但不自启 auto_start=false + 启动扫描 ⇒ **跳过**(本门) - // 同上 + plugin_start ⇒ 拉起(explicitStart) - // 禁用 持久禁用清单 ⇒ 上面的 isDisabled 门挡住,连 plugin_start 也拒 - // - // ★ explicitStart 是关键:声明 false 是"别在开机时拉起我", - // 不是"永远不许启动"。没有这个区分,按需插件就再也起不来了。 - if !explicitStart && !r.shouldAutoStart(plgDir, name) { - log.Printf("[plugin] %s 声明 auto_start=false(按需加载),跳过启动扫描;用 plugin_start 可拉起", name) - return false - } - // 1) 查找工厂(init 自注册或 RegisterNative) r.mu.RLock() factory, hasFactory := r.factories[name] @@ -1098,7 +1064,7 @@ func (r *Registry) ReloadOne(name string) error { r.detachPlugin(name) r.closeDynamic(removed) - ok := r.loadOneOpt(plgDir, name, true) + ok := r.loadOne(plgDir, name) if !ok { return fmt.Errorf("reload plugin %s failed", name) } @@ -1312,18 +1278,6 @@ func (r *Registry) DisablePlugin(name, by string) error { func (r *Registry) EnablePlugin(name string) error { return r.Enable(name) } -// shouldAutoStart 读 plugin.json 的 auto_start(缺省 = true)。 -// -// 读不到 manifest 时返回 true:宁可多启动一个(与旧行为一致), -// 也不要因为清单文件损坏而让插件静默不启动 —— 后者更难查。 -func (r *Registry) shouldAutoStart(plgDir, name string) bool { - mft := readManifest(plgDir) - if mft == nil { - return true - } - return mft.ShouldAutoStart() -} - // isRuntimeStopped 报告插件是否处于**运行态关闭**(设计 §2.6.7.3)。 func (r *Registry) isRuntimeStopped(name string) bool { r.mu.RLock()