Files
HomeAgent/internal/agent/core/childpolicy_test.go
JianFeeeee 2b2707cb89 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
2026-11-04 08:56:47 +08:00

118 lines
4.0 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 core
// 判据:**销毁路径真的撤销了子策略**(行为判据)。
//
// ## 为什么需要它(而不是只靠源码扫描)
//
// R2/R3 把"撤销策略"从两个停止函数搬进了骨架 `child.Registry.Cleanup`,
// 由各类型注入的 `StopDeps.RevokePolicy` 触发。这带来一个新的、**源码扫描
// 看不见**的风险:
//
// StopDeps.RevokePolicy 漏填 ⇒ Cleanup 里那个 `if deps.RevokePolicy != nil`
// 直接跳过,静默不撤销
//
// 字符串 `revokeChildPolicy` 仍然出现在 `security_profile.go` 的定义体里,
// 所以源码扫描照样绿 —— 那正是结构判据的局限(engineering-lessons §1.1)。
//
// ## 判据语义
//
// 声明一条子策略 ⇒ 主体表里存在(前置)
// 走**真实销毁路径** ⇒ 主体表里消失(被测行为)
//
// 变红条件:
// - rtStopDeps 漏填 RevokePolicy ⇒ 红
// - Cleanup 不调 deps.RevokePolicy ⇒ 红
// - teardownResidentWith 删掉撤销那一步 ⇒ 红
import (
"testing"
"github.com/JianFeeeee/HologramAgent/internal/security"
)
// policyRTHost 造一个"父 agent + security 运行时 + 成对 rt 通道"。
//
// 复用 newRTHost 的注册方式,但多给一个 security 运行时(撤销策略要有地方落)。
func policyRTHost(t *testing.T) (*Agent, *security.Runtime) {
t.Helper()
rt := security.NewRuntime(nil)
host, _ := newRTHost(t, &rtBlockingProvider{})
host.security = rt
return host, rt
}
// TestRTChild_StopRevokesPolicy 钉住 rt 的**单数**停止撤销策略。
func TestRTChild_StopRevokesPolicy(t *testing.T) {
host, rt := policyRTHost(t)
defer host.StopRTChildren()
id := rtSpawn(t, host, "rp-1")
if err := host.ProbeDeclareChildPolicy(id, nil, []string{"cmd_run"}); err != nil {
t.Fatalf("声明子策略: %v", err)
}
if !rt.HasPrincipalPolicy(id) {
t.Fatal("前置失败:声明后主体表里没有该子")
}
if err := host.StopRTChild(id); err != nil {
t.Fatalf("StopRTChild: %v", err)
}
if rt.HasPrincipalPolicy(id) {
t.Fatal("★ StopRTChild 之后主体策略仍在 —— StopDeps.RevokePolicy 可能漏填," +
"Cleanup 里的 nil 检查会静默跳过。同名重建将继承上一代父留的约束")
}
}
// TestRTChildren_StopAllRevokesPolicy 钉住**复数**停止与单数对称。
//
// 这是 engineering-lessons.md §2.3「单数改了复数漏了」的行为版护栏:
// 源码扫描只能看到"函数里有没有那个字符串",看不到"复数路径到底撤没撤"。
func TestRTChildren_StopAllRevokesPolicy(t *testing.T) {
host, rt := policyRTHost(t)
defer host.StopRTChildren()
ids := []string{"rb-1", "rb-2", "rb-3"}
for _, id := range ids {
got := rtSpawn(t, host, id)
if err := host.ProbeDeclareChildPolicy(got, nil, []string{"cmd_run"}); err != nil {
t.Fatalf("声明 %s: %v", got, err)
}
if !rt.HasPrincipalPolicy(got) {
t.Fatalf("前置失败:声明后主体表里没有 %s", got)
}
}
if n := host.StopRTChildren(); n != len(ids) {
t.Fatalf("StopRTChildren 停止 %d 个,期望 %d", n, len(ids))
}
for _, id := range ids {
if rt.HasPrincipalPolicy(id) {
t.Fatalf("★ StopRTChildren 之后主体策略仍在(%s)—— "+
"复数路径与单数不对称(engineering-lessons §2.3 的形态)", id)
}
}
}
// TestResident_DestroyRevokesPolicy 钉住驻留子销毁撤销策略。
func TestResident_DestroyRevokesPolicy(t *testing.T) {
parent, _, dir := newRootWith(t, &countingProvider{})
rt := security.NewRuntime(nil)
parent.security = rt
const id = "res-rp"
spawnTestResident(t, parent, dir, id)
if err := parent.ProbeDeclareChildPolicy(id, nil, []string{"cmd_run"}); err != nil {
t.Fatalf("声明子策略: %v", err)
}
if !rt.HasPrincipalPolicy(id) {
t.Fatal("前置失败:声明后主体表里没有该驻留子")
}
if err := parent.DestroyResident(id); err != nil {
t.Fatalf("DestroyResident: %v", err)
}
if rt.HasPrincipalPolicy(id) {
t.Fatal("★ 驻留子销毁后主体策略仍在 —— 同名重建会继承上一代约束")
}
}