Files
HomeAgent/internal/kernelmod/pluginhost/toolsready_test.go
JianFeeeee d65b742bd7 fix(plugin): 修两处持锁自死锁(CI 双红真因)+ tools_ready 事件声明
## 死锁(这是 CI 连续两次红的真因)

`StopAll` 与 `StopPlugin` 都在持有 `r.mu` **写锁**时调用
`RemoveBuiltinSkill` → `skillDirOf` → `r.mu.RLock()` ⇒ **自死锁**。

后果不是报错而是**卡死**:
- internal/plugins 卡 300s 超时(TestCapability_RestrictedPluginStillLoads)
- internal/plugin  卡 840s 超时(TestStopPluginDoesNotTouchPersistentState)
- CI 的 `go test ./...` 与 `go test -race` 双双红

修法:拆出**不加锁**的内部版本,持锁路径只走它:
- `skillDirFrom(dataDir, name)`(纯函数)
- `removeBuiltinSkillLocked(pluginName, p, dataDir)`(显式传 dataDir,
  避免"看起来没取锁、一改就取锁"的隐患)
- `announceSkillRemovedLocked`(持锁下直读 r.evBus)

修后:internal/plugins 300s → 12s,internal/plugin 840s → 0.7s。

## 防回归判据

`TestStopAllAndStopPluginDoNotDeadlock`:死锁**不返回**,普通断言抓不到,
所以用**超时**当判据(5 秒未返回即失败),并指明应改用 Locked 版本。

## tools_ready 事件声明

新增的 tools_ready 触发 `TestEveryPublishedEventHasAnOutwardPath`
(该判据防"事件发了、客户端收不到"的静默失效)。

tools_ready 是**内核内部**就绪信号(plugin-host → seq 等订阅者),
不是对话流程事件,前端无需感知 ⇒ 在 allowed 里显式声明理由。

## 验证

go build ./... / go vet ./... / go test -p 1 ./... / -race 全通过。
2026-10-25 18:25:37 +08:00

51 lines
1.7 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 pluginhost
// tools_ready 的发布判据。
//
// 语义边界(用户裁定):seq 自动运行**不依赖 agent** —— 它只是一段工具调用
// 序列,返回值排队等 agent 起来处理即可。所以事件的正确时机是
// "全部插件加载完、工具都注册了"(ks.PluginsReady = true 处),
// 而不是 agent.Start() 之后。
//
// 放在 kernelmod 而不是 main:时机语义属于内核;外包给编排层会让
// 换个入口(e2e / 测试 / 别的发行版 main)就不一致。
import (
"testing"
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
)
// 没有 EventBus 时必须**响亮报错而不是静默返回** ——
// 静默失败会让"为什么开机序列没跑"完全无从查起。
func TestPublishToolsReadyWithoutBusDoesNotPanic(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("publishToolsReady 在 nil EventBus 下不能 panic:%v", r)
}
}()
publishToolsReady(nil)
publishToolsReady(&kernelboot.KernelState{})
}
// 有 EventBus 时必须真的发布一条 tools_ready,且带 plugin_count。
func TestPublishToolsReadyPublishes(t *testing.T) {
bus := events.NewBus()
var got []*events.Event
bus.Subscribe(events.EventToolsReady, func(e *events.Event) { got = append(got, e) })
ks := &kernelboot.KernelState{EventBus: bus}
publishToolsReady(ks)
if len(got) != 1 {
t.Fatalf("应发布恰好一条 tools_ready,实际 %d 条", len(got))
}
if got[0].Source != "plugin-host" {
t.Fatalf("Source 应为 plugin-host,实际 %q", got[0].Source)
}
if _, ok := got[0].Payload["plugin_count"]; !ok {
t.Fatal("payload 应含 plugin_count")
}
}