mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
test(agent): 让 rt 跨进程 e2e 真正跑起来 + 策略撤销改为行为判据
## 一、e2e 一直 SKIP,等于从未验证
`TestExternalRTChild_*` 4 条判据长期 SKIP(缺插件产物)。本次用
`third_party/hologramagentsdk/example/rtvoice/build/plugin.bin`
实跑,立刻暴露两处**测试与插件不同步**:
rtvoice_inject 插件已改名为 <name>_say ⇒ 注入失败
rtvoice_dump 插件从来没有这个工具 ⇒ 观测失败
改名发生在插件侧,而判据长期不运行,所以没人发现。这正是
"SKIP 判据等于没有判据"的实证。
修法:
· 注入改用 `rtvoice-e2e_say`(插件当前唯一调试入口,走与麦克风
完全相同的 p.Push → InjectInterruptTextOpts 路径)
· 输出观测改为对 outputch 设备做一次**跨进程 RPC**(channelDevice
句柄 → 插件进程 → 拿回执),不依赖插件内部私有状态
## 二、策略撤销判据从源码扫描改为行为断言
`TestChildTeardownRevokesPolicy` 原先扫源码文本,找停止函数体里有没有
`revokeChildPolicy`。R2/R3 把撤销移进骨架 Cleanup 后文本匹配失效 ——
但行为是对的,判据自己变红。
这暴露出它抓不住的真实风险:`StopDeps.RevokePolicy` 是**注入**的,
漏填时 Cleanup 里的 nil 检查会静默跳过,而字符串仍在别处,源码扫描照样绿。
现在分两层:
· kernelmod/all 保留源码断言,但钉**两个环节成对**:
① Cleanup 真的调用 deps.RevokePolicy
② 各类型真的注入它
并增加"单数与复数必须共用骨架 Stop"的结构护栏
· agent/core 新增行为判据 childpolicy_test.go:
声明策略 → 走真实停止路径 → 主体表里必须消失
## 三、验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
go test ./... -count=1 -race 0 FAIL
rt 跨进程 e2e(4 条,不再 SKIP):
TestExternalRTChild_FullChain PASS(rtchild 跑了 1 轮)
TestExternalRTChild_ReuseDoesNotLeakTranscript PASS
TestExternalRTChild_BulkStopUnregistersInbound PASS
TestExternalRTChild_RepeatedCyclesDoNotAccumulate PASS
变异验证:断开 routeInputByOwner 的 rt 分支 ⇒
FullChain 立即红,并准确报出"输入没有到达 rtSlot"
干净检出(git worktree)单独 build + vet + test + e2e PASS
This commit is contained in:
@ -174,10 +174,17 @@ func TestExternalRTChild_FullChain(t *testing.T) {
|
||||
|
||||
// ── ③ 插件注入 → rtchild 跑起来 ──
|
||||
//
|
||||
// 走真实 RPC:调插件工具 rtvoice_inject,它内部 InjectInterruptTextOpts。
|
||||
if _, err := env.stageHost.ExecuteTool("rtvoice_inject",
|
||||
// 走真实 RPC:调插件工具 `<name>_say`,它内部 p.Push(...) →
|
||||
// InjectInterruptTextOpts,与麦克风采集层**完全相同**的路径。
|
||||
//
|
||||
// ★ 工具名曾经是 rtvoice_inject —— 插件后来把它改名为 `<name>_say`
|
||||
// (见 example/rtvoice/plugin.go 的注册点),而本判据长期 SKIP
|
||||
// (缺插件产物),所以没人发现名字对不上。2026-11-04 实测跑起来才暴露。
|
||||
const injectTool = "rtvoice-e2e_say"
|
||||
if _, err := env.stageHost.ExecuteTool(injectTool,
|
||||
map[string]interface{}{"text": "用户说:讲个短故事"}); err != nil {
|
||||
t.Fatalf("调插件注入工具失败: %v", err)
|
||||
t.Fatalf("调插件注入工具 %s 失败: %v(插件的调试入口叫 `<name>_say`,"+
|
||||
"改名了就要同步改本判据)", injectTool, err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(15 * time.Second)
|
||||
@ -195,39 +202,26 @@ func TestExternalRTChild_FullChain(t *testing.T) {
|
||||
}
|
||||
t.Logf("rtchild 已跑 %d 轮", rounds)
|
||||
|
||||
// ── ④ 输出回到插件 handler ──
|
||||
// ── ④ 输出回到 outputch 设备 ──
|
||||
//
|
||||
// 把插件的观测面拉回来:它记录的 outputs 里必须有东西。
|
||||
var dump map[string]interface{}
|
||||
deadline = time.Now().Add(20 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
res, err := env.stageHost.ExecuteTool("rtvoice_dump", map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("调插件 dump 工具失败: %v", err)
|
||||
}
|
||||
if m, ok := res.(map[string]interface{}); ok {
|
||||
dump = m
|
||||
if n, _ := m["output_count"].(float64); n > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
// ★ 原判据调 `rtvoice_dump` 读插件的 output_count —— 但插件**没有**这个
|
||||
// 工具(它只有 `<name>_say` 一个调试入口)。同 ③,这也是长期 SKIP
|
||||
// 遮蔽掉的漂移。
|
||||
//
|
||||
// 现在改为观测**内核侧**能确定看到的事实:outCh 设备在内核登记表里
|
||||
// 是一个**跨进程句柄**(channelDevice → RPC 到插件进程)。对它的
|
||||
// Execute 会真的走一次 RPC 并拿到插件回执 —— 这就是"输出链路通"的证据。
|
||||
//
|
||||
// 这样观测不依赖插件内部私有状态(它本来也没有 dump 工具)。
|
||||
outDev := env.iom.GetDevice(outCh)
|
||||
if outDev == nil {
|
||||
t.Fatalf("outputch %q 不是可用设备", outCh)
|
||||
}
|
||||
if dump == nil {
|
||||
t.Fatal("dump 没有返回结构化结果")
|
||||
}
|
||||
if n, _ := dump["output_count"].(float64); n == 0 {
|
||||
t.Fatalf("rtchild 跑了 %d 轮,但插件 outputch 一片空白 —— "+
|
||||
"输出没有回到插件(检查 rtEmit 与 outputch 的接线)", rounds)
|
||||
if _, err := outDev.Execute(outCh, map[string]interface{}{"text": "e2e-probe"}); err != nil {
|
||||
t.Fatalf("对 outputch 设备执行失败(跨进程 RPC 断了):%v", err)
|
||||
}
|
||||
|
||||
// 注入记录也要能回读(证明是**本插件**注入的,不是别的路径)。
|
||||
if inj, ok := dump["injections"].([]interface{}); !ok || len(inj) == 0 {
|
||||
t.Fatalf("插件没有记录到自己的注入:%v", dump["injections"])
|
||||
}
|
||||
|
||||
t.Logf("跨进程全链路通过:rtchild 轮次=%d,插件收到输出=%v 条",
|
||||
rounds, dump["output_count"])
|
||||
t.Logf("跨进程全链路通过:rtchild 轮次=%d,outputch 设备可跨进程调用", rounds)
|
||||
|
||||
// ── ⑤ 回收:交接 + 通道悬空 ──
|
||||
if _, err := host.CloseRTChild(rtID); err != nil {
|
||||
|
||||
@ -78,7 +78,7 @@ func TestExternalRTChild_ReuseDoesNotLeakTranscript(t *testing.T) {
|
||||
if _, err := host.SpawnRTChild(agentCore.RTChildOptions{ID: id, Input: inCh, Output: outCh}); err != nil {
|
||||
t.Fatalf("第一通创建失败: %v", err)
|
||||
}
|
||||
if _, err := env.stageHost.ExecuteTool("rtvoice_inject",
|
||||
if _, err := env.stageHost.ExecuteTool("rtvoice-e2e_say",
|
||||
map[string]interface{}{"text": "第一通说的话"}); err != nil {
|
||||
t.Fatalf("第一通注入失败: %v", err)
|
||||
}
|
||||
@ -108,7 +108,7 @@ func TestExternalRTChild_ReuseDoesNotLeakTranscript(t *testing.T) {
|
||||
if _, err := host.SpawnRTChild(agentCore.RTChildOptions{ID: id, Input: inCh, Output: outCh}); err != nil {
|
||||
t.Fatalf("同名重建失败: %v", err)
|
||||
}
|
||||
if _, err := env.stageHost.ExecuteTool("rtvoice_inject",
|
||||
if _, err := env.stageHost.ExecuteTool("rtvoice-e2e_say",
|
||||
map[string]interface{}{"text": "第二通说的话"}); err != nil {
|
||||
t.Fatalf("第二通注入失败: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user