Files
HomeAgent/internal/toolroute/test/dispatch_contract_test.go
JianFeeeee 8814ccdef6 test: 按模块导出接口重写黑盒判据(批次 6:promptsec / toolroute / pluginview / meta)
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 21 条判据,只调导出接口,输入 → 可观察输出。

## internal/promptsec —— NewRegistry/Declare/Render/Snapshot/Names

    TestDeclareThenRender                声明两段,按 Order 输出且含正文
    TestDeclareRejectsMismatchedOwner    段名前缀与 Owner 不符必须报错
    TestDeclareIsIdempotent              同段名覆盖,不产生两份
    TestEmptyContentRemovesSection       空正文 = 撤销声明
    TestRemoveOwnerOnlyDropsThatOwner    撤销一个 owner 不影响其他
    TestSnapshotReturnsAllSections       快照反映当前声明

## internal/toolroute —— Dispatch

    TestDispatchRoutesEachPrefix         15 种工具前缀 → 对应族处理器
    TestDispatchGetPluginToolsExtractsArg  plugin_name 参数透传
    TestDispatchUnknownIsNotSwallowed    未知工具 handled=false(插件工具不能消失)
    TestDispatchNilHandlers              nil handlers 不 panic

用 recordingHandlers 记录哪个族被调用,不断言内部 switch 结构。

## internal/pluginview —— 事件 → Snapshot

    TestFreshViewIsEmpty                 新视图为空
    TestCmdRunEventAppearsInSnapshot     cmd_run → 命令历史
    TestTerminalCreateThenOutput         terminal_create + 输出 → 终端状态
    TestUnknownToolIsIgnored             未知工具不造记录

**判据写错过一次**:`OnTerminalOutput` 的 payload 键是 `terminal_id`
不是 `id`,第一版按 `id` 写 ⇒ 输出没进 Snapshot。按真实输入契约修正。

## internal/meta —— 版本元信息

    TestFullVersionContainsKernelAndVersion
    TestSourceURLLooksLikeHTTPS
    TestLicenseMetadataNotEmpty
    TestSDKCompatibleVersionIsSemverLike

## 变异验证(改回旧实现必须红)

    promptsec  Declare 直接 return nil  → ★ Render 缺正文
    toolroute  未知工具 handled=true     → ★ 未知工具被吞掉
    pluginview cmd_run 不记录           → ★ cmd_run 未进命令历史
    meta       FullVersion 丢版本       → ★ 不含 "1.4.0"

第一版 promptsec 变异改的是条件中的一段 token、没改变行为(空变异),
重做后确认判据有效。

## 验证

    go build ./...                            PASS
    go vet ./...                              PASS
    gofmt -l                                  干净
    python3 .github/scripts/check-ci-gates.py PASS
    go test ./... -count=1                    24 个测试包 / 100 包无测试 / 0 FAIL
    4 个新包 -race                            全绿
    源码目录 _test.go                         0
2026-11-09 13:22:35 +08:00

121 lines
4.3 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 test
// internal/toolroute 的黑盒判据:Dispatch(handlers, ToolCall) → (输出, handled)。
// 只调导出函数,不读源码、不碰内部 switch 以外的任何状态。
import (
"testing"
"github.com/JianFeeeee/HologramAgent/internal/agent/api"
"github.com/JianFeeeee/HologramAgent/internal/toolroute"
)
// recordingHandlers 记录每个族处理器被调用的次数。
type recordingHandlers struct {
calls map[string]int
}
func newHandlers() *recordingHandlers {
return &recordingHandlers{calls: map[string]int{}}
}
func (h *recordingHandlers) hit(name string) string {
h.calls[name]++
return "out:" + name
}
func (h *recordingHandlers) ExecutePersonaTool(tc api.ToolCall) string { return h.hit("persona") }
func (h *recordingHandlers) ExecuteMemoryTool(tc api.ToolCall, s []string) string {
return h.hit("memory")
}
func (h *recordingHandlers) ExecuteSocialTool(tc api.ToolCall) string { return h.hit("social") }
func (h *recordingHandlers) ExecuteKnowledgeTool(tc api.ToolCall) string {
return h.hit("knowledge")
}
func (h *recordingHandlers) ExecuteColdContextTool(tc api.ToolCall) string {
return h.hit("coldcontext")
}
func (h *recordingHandlers) ExecuteSecretTool(tc api.ToolCall) string { return h.hit("secret") }
func (h *recordingHandlers) ExecuteOutputSendHelp(tc api.ToolCall) string {
return h.hit("output_help")
}
func (h *recordingHandlers) ExecuteOutputSend(tc api.ToolCall) string { return h.hit("output_send") }
func (h *recordingHandlers) ExecuteOutputListChannels() string { return h.hit("output_list") }
func (h *recordingHandlers) ExecuteInputChannels(tc api.ToolCall) string {
return h.hit("input_channels")
}
func (h *recordingHandlers) ExecuteResidentAgents(tc api.ToolCall) string {
return h.hit("resident")
}
func (h *recordingHandlers) ExecuteNotifyParent(tc api.ToolCall) string {
return h.hit("notify_parent")
}
func (h *recordingHandlers) ExecuteInputchNote(tc api.ToolCall) string {
return h.hit("inputch_note")
}
func (h *recordingHandlers) ExecutePluginReload() string { return h.hit("plgreload") }
func (h *recordingHandlers) ExecuteGetPluginTools(name string) string {
return h.hit("get_plugin_tools:" + name)
}
// TestDispatchRoutesEachPrefix:每种工具前缀路由到对应族处理器。
func TestDispatchRoutesEachPrefix(t *testing.T) {
cases := []struct {
name, family string
}{
{"persona_set", "persona"},
{"stablemem_commit", "memory"},
{"social_note", "social"},
{"knowledge_search", "knowledge"},
{"coldcontext_query", "coldcontext"},
{"doc_query", "coldcontext"}, // 旧前缀兼容
{"secret_get", "secret"},
{"output_send__qq", "output_send"},
{"output_send__qq_help", "output_help"},
{"output_list_channels", "output_list"},
{"input_channels", "input_channels"},
{"resident_agents", "resident"},
{"notify_parent", "notify_parent"},
{"inputch_note", "inputch_note"},
{"plgreload", "plgreload"},
}
for _, c := range cases {
h := newHandlers()
out, handled := toolroute.Dispatch(h, api.ToolCall{Name: c.name}, nil)
if !handled {
t.Fatalf("★ %q 未被认作内置工具", c.name)
}
if out == "" || h.calls[c.family] != 1 {
t.Fatalf("★ %q 路由错:out=%q calls=%v", c.name, out, h.calls)
}
}
}
// TestDispatchGetPluginToolsExtractsArg:plugin_name 参数必须传给处理器。
func TestDispatchGetPluginToolsExtractsArg(t *testing.T) {
h := newHandlers()
out, handled := toolroute.Dispatch(h, api.ToolCall{
Name: "get_plugin_tools", Arguments: map[string]interface{}{"plugin_name": "qq"},
}, nil)
if !handled || out != "out:get_plugin_tools:qq" {
t.Fatalf("★ plugin_name 未透传:handled=%v out=%q", handled, out)
}
}
// TestDispatchUnknownIsNotSwallowed:未知工具 handled=false(插件工具不能消失)。
func TestDispatchUnknownIsNotSwallowed(t *testing.T) {
h := newHandlers()
out, handled := toolroute.Dispatch(h, api.ToolCall{Name: "my_plugin_custom_tool"}, nil)
if handled || out != "" {
t.Fatalf("★ 未知工具被吞掉:handled=%v out=%q", handled, out)
}
if len(h.calls) != 0 {
t.Fatalf("★ 未知工具却调了处理器:%v", h.calls)
}
}
// TestDispatchNilHandlers:nil handlers 不得 panic。
func TestDispatchNilHandlers(t *testing.T) {
if _, handled := toolroute.Dispatch(nil, api.ToolCall{Name: "stablemem_commit"}, nil); handled {
t.Fatal("★ nil handlers 却报告 handled")
}
}