Files
HomeAgent/internal/agent/core/toolname_rename_test.go
JianFeeeee b850feaef2 feat: 段页式记忆流转、coldcontext 重命名与安全治理
记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边

命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据

安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据

验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
2026-10-29 23:53:48 +08:00

119 lines
6.1 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
// 术语改名的判据(plan.md 裁定 #11:`coldcontext_*` / `stablemem_*`,不再用 doc/graph)。
//
// ## 为什么必须钉(2026-10-29 现场)
//
// 裁定 #11 只落实了一半:`coldcontext_*` 已改,而**图记忆的工具仍叫 `memory_*`**。
// 改名不是纯文字工作,它有三个会静默失效的面:
//
// ① 路由:toolroute 按前缀分派,改前缀不改路由 ⇒ 工具"存在但调不到"
// ② 提示词:BuildToolPrompt / skill / 工具描述里写旧名 ⇒ 模型照着调不存在的工具
// ③ 安全策略:生产配置用的是通配符 `memory_*`,命中不了 `stablemem_*`
// ⇒ 硬私有策略静默失效(用户能读私人记忆)
//
// ③ 最危险:它不报错、不告警,只是拦不住。所以本文件也钉一条策略匹配判据。
import (
"strings"
"testing"
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/toolroute"
)
// ① 面向模型的稳定记忆工具必须统一用 stablemem_ 前缀。
func TestStableMemoryToolsUseStablememPrefix(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
a.memory = nil // 只需要名字,不需要真库
// 旧名不得再出现于任何工具定义(会造成"提示词教旧名、定义是新名")。
//
// ★ 此处曾有例外 `def.Name != "memory_document_query"`(2026-10-29 移除)。
// 当时以为那个工具"仍在 memory_ 名下",实际它是改名前的死分支:
// 没有工具定义、Dispatch 也不分派它。例外一旦存在,就会掩盖真问题 ——
// 所以现在**零例外**。
for _, def := range a.ToolDefinitions() {
if strings.HasPrefix(def.Name, "memory_") {
t.Errorf("发现未改名的稳定记忆工具: %s(裁定 #11 要求 stablemem_*)", def.Name)
}
}
}
// ② 路由必须认 stablemem_ 前缀(否则工具存在但调不到)。
//
// ★ 这里直接调 Dispatch,而不是某个"IsBuiltinTool"前缀表 —— 后者曾有**两份**
// 手抄副本(toolroute 与 core 各一份),都零调用方,却在改名时制造了
// "改了 Dispatch 却漏了副本"的真实故障。判据钉**行为**(能不能分派),
// 不钉"某份表里有没有这个字符串"。
func TestStablememToolsAreRouted(t *testing.T) {
h := &routeProbeHandlers{}
for _, n := range []string{"stablemem_recall", "stablemem_purge"} {
if _, handled := toolroute.Dispatch(h, agentAPI.ToolCall{Name: n}, nil); !handled {
t.Errorf("%s 未被 Dispatch 分派 —— stablemem_ 前缀漏了,工具会变成\"存在但调不到\"", n)
}
}
// ★ 旧前缀 memory_ **不再被分派**(这是改名的语义,不是遗漏)。
//
// 我最初在这里断言"memory_ 应保留兼容分派",它变红了 —— 而红得对:
// Dispatch 里从来没有 memory_ 分支,改名后只有 stablemem_。
// 若为"兼容"而加一条 memory_ 分支,反而会让**旧名重新可用**,
// 与裁定 #11(不再用 graph/doc 时代的名)直接冲突。
//
// 真正需要兼容的是**配置与提示词**(那些已另行处理),不是工具路由:
// 存量插件注册的是自己的工具名,内核的稳定记忆工具由内核定义,
// 不存在"外部还在用旧名调内核工具"的情况。
if _, handled := toolroute.Dispatch(h, agentAPI.ToolCall{Name: "memory_recall"}, nil); handled {
t.Error("memory_ 不应再被分派 —— 旧名重新可用会与裁定 #11 冲突")
}
// 反向:不是内置工具的名字必须返回 handled=false(否则插件工具会被吞掉)。
if _, handled := toolroute.Dispatch(h, agentAPI.ToolCall{Name: "qq_get_message"}, nil); handled {
t.Error("非内置工具不应被 Dispatch 吞掉")
}
}
// routeProbeHandlers 记录 Dispatch 选中了哪个族,全部返回空串即可。
type routeProbeHandlers struct {
memoryCalls int
}
func (h *routeProbeHandlers) ExecutePersonaTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteMemoryTool(agentAPI.ToolCall, []string) string {
h.memoryCalls++
return ""
}
func (h *routeProbeHandlers) ExecuteSocialTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteKnowledgeTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteColdContextTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteSecretTool(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteOutputSend(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteOutputSendHelp(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteOutputListChannels() string { return "" }
func (h *routeProbeHandlers) ExecuteInputChannels(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteResidentAgents(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteNotifyParent(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecuteInputchNote(agentAPI.ToolCall) string { return "" }
func (h *routeProbeHandlers) ExecutePluginReload() string { return "" }
func (h *routeProbeHandlers) ExecuteGetPluginTools(string) string { return "" }
// ③ ★ 提示词里不得再教模型调旧工具名。
//
// BuildToolPrompt 是**硬编码字符串**(不是从定义生成),工具一改名它就过期。
// 这类不一致模型会当成事实执行,最终表现为"反复调用不存在的工具"。
func TestToolPromptDoesNotTeachRenamedTools(t *testing.T) {
// BuildToolPrompt 是 Indexer 上的**硬编码字符串**(不是从 toolDefs 生成),
// 工具一改名它就过期 —— 这正是本次改名后真实失败的那个测试。
idx := memory.NewIndexer(nil)
prompt := idx.BuildToolPrompt()
if prompt == "" {
t.Skip("BuildToolPrompt 返回空")
}
for _, stale := range []string{"memory_recall", "memory_commit", "memory_purge", "memory_introspect"} {
if strings.Contains(prompt, stale) {
t.Errorf("工具提示词仍在教旧工具名 %q —— 模型会照着调用不存在的工具", stale)
}
}
}