Files
HomeAgent/internal/agent/core/prunepolicy_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

120 lines
4.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 core
import (
"testing"
agentIO "github.com/JianFeeeee/HologramAgent/internal/agent/io"
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
)
// 这一组测试锁死「默认不裁剪」这条语义。
//
// 改动前:每条非中断输入都无条件 Prune 一次,没有任何声明能关掉它。
// 这是破坏性行为(低相关事件被归档并从上下文移走),却无法从调用点看出
// 「谁触发的裁剪」。改成需声明后,必须逐条验证默认值确实是不裁剪。
func TestPruneDeclared_DefaultsToNoPrune(t *testing.T) {
m := agentIO.NewIOManager()
a := &Agent{io: m}
evt := &agentIO.InputEvent{Source: "unknown_source", Payload: map[string]interface{}{}}
if a.pruneDeclared(evt) {
t.Fatal("既没有通道声明也没有注入声明的输入,默认必须不裁剪")
}
// 通道注册了、但策略是 none / 空:仍然不裁剪。
m.RegisterInputChannel("quiet", pubsdk.ChannelDef{ContextPolicy: pubsdk.ContextPolicyNone})
if a.pruneDeclared(&agentIO.InputEvent{Source: "quiet", Payload: map[string]interface{}{}}) {
t.Fatal("ChannelDef.ContextPolicy=none 不应裁剪")
}
m.RegisterInputChannel("empty", pubsdk.ChannelDef{})
if a.pruneDeclared(&agentIO.InputEvent{Source: "empty", Payload: map[string]interface{}{}}) {
t.Fatal("ChannelDef 未设 ContextPolicy 不应裁剪")
}
}
// 通道显式声明 prune 才裁剪。
func TestPruneDeclared_ChannelOptIn(t *testing.T) {
m := agentIO.NewIOManager()
m.RegisterInputChannel("noisy", pubsdk.ChannelDef{ContextPolicy: pubsdk.ContextPolicyPrune})
a := &Agent{io: m}
if !a.pruneDeclared(&agentIO.InputEvent{Source: "noisy", Payload: map[string]interface{}{}}) {
t.Fatal("通道声明 prune 后应裁剪")
}
}
// 注入点声明的优先级高于通道定义:同一通道下的不同注入可以有不同意图。
func TestPruneDeclared_InjectionOverridesChannel(t *testing.T) {
m := agentIO.NewIOManager()
a := &Agent{io: m}
m.RegisterInputChannel("chan", pubsdk.ChannelDef{ContextPolicy: pubsdk.ContextPolicyPrune})
// 注入点说 none → 即使通道说 prune 也不裁。
evt := &agentIO.InputEvent{Source: "chan", Payload: map[string]interface{}{
"context_policy": pubsdk.ContextPolicyNone,
}}
if a.pruneDeclared(evt) {
t.Fatal("注入点声明 none 应覆盖通道的 prune")
}
// 通道没说,注入点说 prune → 裁。
m.RegisterInputChannel("plain", pubsdk.ChannelDef{})
evt = &agentIO.InputEvent{Source: "plain", Payload: map[string]interface{}{
"context_policy": pubsdk.ContextPolicyPrune,
}}
if !a.pruneDeclared(evt) {
t.Fatal("注入点声明 prune 应生效")
}
}
// 没有 context 时不能 panic,也不该裁剪。
func TestPruneOnInput_NilContextIsSafe(t *testing.T) {
m := agentIO.NewIOManager()
m.RegisterInputChannel("noisy", pubsdk.ChannelDef{ContextPolicy: pubsdk.ContextPolicyPrune})
a := &Agent{io: m}
if got := a.pruneOnInput(&agentIO.InputEvent{Source: "noisy", Payload: map[string]interface{}{}}, "x"); got != 0 {
t.Fatalf("nil context 应返回 0,实际 %d", got)
}
}
// cleanInputFor 的优先级:注入点声明的 cleaner > 按 source 查的 cleaner > 原文。
func TestCleanInputFor_Priority(t *testing.T) {
m := agentIO.NewIOManager()
m.RegisterInputChannel("src", pubsdk.ChannelDef{
Cleaner: func(s string) string { return "by-source:" + s },
})
m.RegisterInputChannel("explicit", pubsdk.ChannelDef{
Cleaner: func(s string) string { return "by-name:" + s },
})
a := &Agent{io: m}
// 无声明 → 用 source 的 cleaner
evt := &agentIO.InputEvent{Source: "src", Payload: map[string]interface{}{}}
if got := a.cleanInputFor(evt, "raw"); got != "by-source:raw" {
t.Fatalf("应回退到 source 的 cleaner,实际 %q", got)
}
// 注入点指定 cleaner_name → 覆盖 source 的
evt = &agentIO.InputEvent{Source: "src", Payload: map[string]interface{}{"cleaner_name": "explicit"}}
if got := a.cleanInputFor(evt, "raw"); got != "by-name:raw" {
t.Fatalf("注入点声明的 cleaner 应优先,实际 %q", got)
}
// 完全没有 cleaner → 原文
evt = &agentIO.InputEvent{Source: "nobody", Payload: map[string]interface{}{}}
if got := a.cleanInputFor(evt, "raw"); got != "raw" {
t.Fatalf("没有 cleaner 时应返回原文,实际 %q", got)
}
// 声明的名字查不到 → 回退到 source 的 cleaner(并记日志),不能 panic、不能丢内容
evt = &agentIO.InputEvent{Source: "src", Payload: map[string]interface{}{"cleaner_name": "missing"}}
if got := a.cleanInputFor(evt, "raw"); got != "by-source:raw" {
t.Fatalf("未知 cleaner_name 应回退,实际 %q", got)
}
// nil IOManager 不能 panic
if got := (&Agent{}).cleanInputFor(evt, "raw"); got != "raw" {
t.Fatalf("nil io 应返回原文,实际 %q", got)
}
}