Files
HomeAgent/internal/plugin/proc/host_evtunsub_test.go
JianFeeeee 3f834794c5 refactor: SDK module 路径统一改为 hologramagentsdk(59 文件 + go.mod)
## 为什么必须做

SDK 仓(独立仓库)已把 module 从 `github.com/JianFeeeee/homeagentsdk`
改名为 `hologramagentsdk`。主仓通过 `replace` 直接引用它的**物理目录**:

    replace github.com/JianFeeeee/homeagentsdk => ./third_party/homeagent-sdk

一旦 SDK 侧改成新 module 名,主仓立刻炸:

    go: ./third_party/homeagent-sdk@ used for two different module paths
        (github.com/JianFeeeee/hologramagentsdk and github.com/JianFeeeee/homeagentsdk)

Go **不允许**同一目录被两个 module 路径 replace。所以不能"新旧并存",
只能把主仓 59 个文件的 import 全部改到新名,并删掉旧 replace。

## 一个被证伪的前提

我最初按"third_party/homeagent-sdk 是独立仓库,不能在此仓修改其源码"
来处理,并因此判断两个仓的改动可以隔离。实际上主仓是靠 `replace` 指向
该目录的物理副本 —— **两个仓的改动不可能真正隔离**:SDK 侧改 module 名,
主仓必然跟着改 import。

这个前提应该在做任何改动之前用 `go list -m` 验证,而不是假设。

## 连带修掉两处

| 缺陷 | 性质 |
|---|---|
| `TestLuaSDKMockSingleSource` 失败 | SDK 的 `sdk/lua/sdk.lua` 变了,主仓内嵌副本未同步(跑 `scripts/sync-lua-sdk.sh` 修复) |
| `sdkModuleNames` 分散两处 | 已在 SDK 仓合并为单一数据源(见 SDK 仓 725732b) |

第一条是**副本一致性判据抓到的**,不是我自己发现的 —— 这类判据的价值在此:
两个仓必然有副本,靠人记住"改了 A 要同步 B"是做不到的。

## 验证

- `go build ./...` / `go vet ./...` 干净
- `go test ./... -count=1`:**58 包 0 失败**
- `internal/plugin`(Lua SDK 副本一致性)+ `internal/sdk` 全绿
- 主仓 `third_party/homeagent-sdk/` 与 SDK 仓 `main` 无差异
2026-10-23 15:51:02 +08:00

85 lines
2.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 proc
import (
"sync/atomic"
"testing"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// stubSubscriber 是最小 EvtRingSubscriber 实现,用于验证「关停时统一退订」。
//
// 为什么必须桩掉 Host.evtSubscriber:真实实现是 internal/plugin.EventRing,
// 它依赖 Bus(另一个包),在 proc 包内会造成循环依赖。这里只关心
// Host.Close 是否**调用了** closer —— 真实实现拿到 Close 后的行为
// 由 internal/plugin 侧测试覆盖。
type stubSubscriber struct {
closed atomic.Bool
// subCount 记录被登记的订阅数(供断言 tracked 语义)
subCount atomic.Int32
}
func (s *stubSubscriber) EvtRingSubscribe(types []pubsdk.EventType) func() {
s.subCount.Add(1)
return func() {}
}
func (s *stubSubscriber) EvtRingSubscribeTracked(types []pubsdk.EventType) func() {
s.subCount.Add(1)
return func() {}
}
func (s *stubSubscriber) Close() { s.closed.Store(true) }
// TestHost_CloseUnsubscribesEventRing 钉死不变量:
//
// Host.Close() 必须在 munmap 共享段**之前**退订事件环。
//
// 为什么这是安全不变量:事件环订阅的 handler 会 ring.WritePush(写共享内存)。
// Host.Close 会 unmap 那块内存;若订阅还在 Bus 上,munmap 后任意一条事件经过
// Publish 都会让 handler 写已解除映射的内存 ⇒ SIGSEGV。
// Bus.safeCall 虽有 recover,但 SIGSEGV 是 runtime 致命错误、recover 捕不到,
// 后果是整个内核进程被杀。
//
// 修复前:handleEvents 丢弃取消函数、EventsUnsubscribe 是 no-op、
// Host.Close 也从不停订阅 —— 每个订阅过的插件都在 Bus 上永久留了一个
// 写共享内存的 handler,内核关停时必炸。
func TestHost_CloseUnsubscribesEventRing(t *testing.T) {
host, err := NewHost()
if err != nil {
t.Fatalf("NewHost: %v", err)
}
sub := &stubSubscriber{}
host.SetEvtSubscriber(sub)
// 模拟插件订阅(走 tracked 路径,与 handleEvents 一致)
host.evtSubscriber.EvtRingSubscribeTracked([]pubsdk.EventType{pubsdk.EventSystem})
if sub.subCount.Load() != 1 {
t.Fatalf("订阅登记数 = %d, want 1", sub.subCount.Load())
}
if err := host.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
if !sub.closed.Load() {
t.Fatal("Host.Close 未退订事件环:\n" +
" munmap 之后 handler 仍挂在 Bus 上,一条事件就会写已解除映射的内存\n" +
" ⇒ SIGSEGV(recover 捕不到,内核进程被杀)。\n" +
" 修法:Host.Close 在 freeShm 之前调用 evtCloser.Close。")
}
}
// TestHost_CloseWithoutSubscriber 确认没设订阅时 Close 不 panic
// (evtSubscriber 为 nil 是合法状态:未注册任何事件的部署)。
func TestHost_CloseWithoutSubscriber(t *testing.T) {
host, err := NewHost()
if err != nil {
t.Fatalf("NewHost: %v", err)
}
if err := host.Close(); err != nil {
t.Fatalf("无订阅者时 Close 应成功: %v", err)
}
}