Files
HomeAgent/internal/agent/core/scene_test.go
JianFeeeee 8887e06274 feat(sdk+memory): 补 ScenePolicy 声明项,让通道能退出场面识别
缺口(R6):ChannelDef 的记忆声明已有三件套——NoMemory 管「进不进
记忆计算」、ContextPolicy 管「裁不裁上下文」、RecallPolicy 管「召不召回
记忆」,唯独没有「这条输入算不算一场戏的一部分」。现状是无条件参与:
situationFeaturesFor 里只要 evt.Source != "" 就产出一个 chan 特征,没有
可关的开关 ⇒ chan:system / chan:kernel / chan:timer 这类纯内部信噪通道
也在撑场面,每次触发都让不相干的场景长出来或变强,召回时又会把
「内核在跑定时器」当成「用户在这类场景下说过的话」取回。

穷举确认不是查漏:go.mod replace 指向 third_party/homeagent-sdk,
plugin.go 中 scene 出现 0 次,SDK 自身 git 历史 -S'Scene' -- sdk/ 为空。

SDK(纯追加,老插件行为逐字节不变):
- 常量 ScenePolicyAuto / ScenePolicyNone + ValidScenePolicy,形状与
  ContextPolicy / RecallPolicy 完全一致
- ChannelDef.ScenePolicy 与 InjectOptions.ScenePolicy,均带 omitempty
- 默认取 auto(参与)而非 none:场景只附加检索路、不改记忆本体,
  默认关会让存量通道突然失去召回;「关」是少数意图。与 ContextPolicy
  刻意相反(同为破坏性操作,那里是默认关)。

内核:
- applyInjectOpts 搬运 scene_policy(与另外三个标志位同面)
- sceneSuppressed 完全照 recallDeclared 的形状:注入点 payload >
  通道定义 > 默认。none 时连时段(part)特征都不产,也不派生场景键
  (只停指纹采集而留声明路,等于给这个口子开后门)
- situationFeaturesFor / sceneKeysFor 由包级函数改为 Agent 方法
  (需要 a.io 查通道定义),23 个调用点同步

判据:scenepolicy_test.go 7 例,改前编译期红(undefined:
pubsdk.ScenePolicyAuto),改后全绿。其中两例专门护住「未声明时行为
逐字节不变」,是纯追加承诺的护栏。

记忆 8 包 + agent/core 全绿,8 包齐全、无 FAIL/panic/race。

存量通道标注待定:kernel/timer/offload-*/child/* 是纯 0-refs 信噪,
可直接标 none;但 mc:system(12 refs) 与 system(3 refs) 带真实记忆,
性质不明,不擅自标。
2026-09-26 20:09:17 +08:00

125 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
import (
"strings"
"testing"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
)
// TestSceneKeysFor 钉住当前场景的推导优先级:
// 注入点显式声明 > 通道 > 工具;并列命中且去重。
func TestSceneKeysFor(t *testing.T) {
a := &Agent{io: agentIO.NewIOManager()}
// 通道 + 工具:两个都能独立成立的触发条件,都要带上
got := a.sceneKeysFor(&agentIO.InputEvent{Source: "qq"}, "qq_get_message")
want := []string{"chan:qq", "tool:qq_get_message"}
if len(got) != len(want) {
t.Fatalf("sceneKeysFor = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("sceneKeysFor[%d] = %q, want %q", i, got[i], want[i])
}
}
// 注入点显式声明排最前;归一化生效;重复声明去重
evt := &agentIO.InputEvent{
Source: "QQ",
Payload: map[string]interface{}{"scene": " chan:qq/peer:group_1 "},
}
got = a.sceneKeysFor(evt, "")
if len(got) != 2 || got[0] != "chan:qq/peer:group_1" || got[1] != "chan:qq" {
t.Errorf("显式声明应排最前且通道场景归一: %v", got)
}
// 数组形式声明
evt = &agentIO.InputEvent{
Source: "webui",
Payload: map[string]interface{}{"scene": []interface{}{"chan:qq", "task:reminder"}},
}
got = a.sceneKeysFor(evt, "")
if len(got) != 3 || got[0] != "chan:qq" || got[1] != "task:reminder" || got[2] != "chan:webui" {
t.Errorf("数组声明未生效: %v", got)
}
// nil 事件不 panic
if got := a.sceneKeysFor(nil, ""); len(got) != 0 {
t.Errorf("nil 事件应无场景: %v", got)
}
// 未声明的 payload 键不影响
evt = &agentIO.InputEvent{Source: "cli", Payload: map[string]interface{}{"recall_policy": "none"}}
if got := a.sceneKeysFor(evt, ""); len(got) != 1 || got[0] != "chan:cli" {
t.Errorf("无 scene 声明时应只有通道场景: %v", got)
}
}
// TestSituationFeaturesFor 钉住指纹来源:全部是运行时可观察量,
// 不需要模型配合也不需要人工标注。
func TestSituationFeaturesFor(t *testing.T) {
a := &Agent{io: agentIO.NewIOManager()}
evt := &agentIO.InputEvent{
Source: "QQ",
Payload: map[string]interface{}{"group_id": float64(1027993713)},
}
feats := a.situationFeaturesFor(evt, "帮我看看排班表", "qq_get_message")
kinds := map[string]int{}
for _, f := range feats {
kinds[f.Kind]++
}
if kinds["chan"] != 1 || kinds["peer_group"] != 1 || kinds["tool"] != 1 || kinds["part"] != 1 {
t.Fatalf("必备维度缺失: %+v", feats)
}
if kinds["topic"] == 0 {
t.Errorf("话题软特征缺失: %+v", feats)
}
if kinds["topic"] > 3 {
t.Errorf("话题最多 3 个,得到 %d", kinds["topic"])
}
sig := memory.NewSituation(feats...)
keys := sig.Keys()
// 归一化 + 数值 id 的转换
found := false
for _, k := range keys {
if k == "chan:qq" {
found = true
}
}
if !found {
t.Errorf("通道特征未归一化: %v", keys)
}
for _, k := range keys {
if strings.Contains(k, "peer_group:1027993713") {
found = true
}
}
if !found {
t.Errorf("数值型 group_id 未转成特征: %v", keys)
}
// 无事件时不 panic,且只有工具特征时也成立
if feats := a.situationFeaturesFor(nil, "", "memory_recall"); len(feats) != 1 {
t.Errorf("仅工具场景应有 1 个特征: %+v", feats)
}
}
// TestWritePathAttachesBothPaths 钉住写侧的「两条路都挂」:
// 显式声明优先;否则挂本轮声明的 + 涌现的场景集合。
func TestTurnSceneKeysBothPaths(t *testing.T) {
a := &Agent{io: agentIO.NewIOManager()}
// 声明的通道场景与工具场景都在,涌现键(若有)追加在后
evt := &agentIO.InputEvent{Source: "qq", Payload: map[string]interface{}{"scene": "chan:qq/peer:group_1"}}
got := a.sceneKeysFor(evt, "qq_get_message")
want := []string{"chan:qq/peer:group_1", "chan:qq", "tool:qq_get_message"}
if len(got) != len(want) {
t.Fatalf("声明侧场景数不对: %v want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("声明侧[%d] = %q want %q", i, got[i], want[i])
}
}
}