From 84021ef40843ae3c50f33a1b85ddb826b7f99ad6 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Wed, 4 Nov 2026 18:26:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(agent/core):=203=20=E4=B8=AA=E9=BB=91?= =?UTF-8?q?=E7=AE=B1=E5=88=A4=E6=8D=AE=E8=BF=81=E5=88=B0=20core/test=20+?= =?UTF-8?q?=20=E5=8E=BB=E6=8E=89=E4=B8=80=E5=A4=84=E7=99=BD=E7=9B=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 迁移了什么 stages_test.go → internal/agent/core/test/ stages_plugin_test.go → internal/agent/core/test/ stages_tooldef_test.go → internal/agent/core/test/ 包名 `core` → `test`。`agent/core` 目录少 3 个测试文件(该目录原有 196 个文件,其中 144 个是测试 —— 这是分步收敛的第一批)。 ## 顺带去掉一处白盒 `stages_plugin_test.go` 原本这样断言"stage 表已清空": h.mu.RLock() _, exists := h.stages[sdk.StageAfterToolcall] h.mu.RUnlock() 直接读**私有互斥锁和私有字段**。而 `UnregisterPluginStages` 的返回值 已经表达了同一件事。改为: if n := h.UnregisterPluginStages("solo"); n != 1 { ... } if n := h.UnregisterPluginStages("solo"); n != 0 { ... } 第二次摘除必须得 0 —— 如果内部表没清空,第二次仍会"摘到"残留计数。 这是黑箱可观测的等价断言。 变异验证:让 `UnregisterPluginStages` 假装摘除但不动表 (`if true { return 1 }`)⇒ 实测红: --- FAIL: TestUnregisterPluginStages_RemovesOnlyThatPlugin --- FAIL: TestUnregisterPluginStages_DropsEmptyStage ★ 重复摘除应得 0(说明内部已清空),实际 1 ## 为什么只迁 3 个(agent/core 有 143 个测试文件) 先按「对源码未导出标识符的引用数」分档,实测: 0 引用 11 个文件 1 引用 约 20 个 ... 其余逐步升高 但"0 引用"不等于"能迁" —— 逐个实测发现四种拦路虎: ① `newScrubRuntime` / `withScrubRuntime` / `newRTTestAgent` —— 包内测试辅助,其中 `withScrubRuntime(a *Agent, rt)` 直接设私有字段 ② `computeTokenBudget` —— 未导出方法(tokenbudget_test 必须留包内) ③ `filepath.Glob("*.go")` —— 扫描"当前目录源码"的判据 (childkind_test),迁到子目录后会 glob 到空目录而**静默失效** ④ `newPreemptAgent` 等跨文件辅助 ③ 尤其值得记:它不是编译错误,是**判据悄悄失去对象**。 ## 踩到的坑(记下来避免重犯) 第一次批量迁 11 个文件、再用脚本自动加 `core.` 前缀,结果把 `import "…/internal/agent/core"` 本身也改了(变成 `core.core.core`), 还误伤了 `strconv.Itoa` 等标准库调用。批量文本替换在"加包名前缀" 这种场景下不可靠 —— 它会改到导入路径、注释、标准库符号。 最终改为**一次只迁一个、编译器逐步报错、手工修**。慢,但不会留下 半成品。agent/core 已完全还原过一次(git checkout HEAD)。 ## 验证 go build ./... PASS go vet ./... PASS gofmt -l 干净 go test ./... -count=1 64 包全过,0 FAIL (core/test 是新包,63 → 64) core/test 实跑用例数 17(确认不是空壳) 干净检出(worktree) build + core/... 全绿 ## 后续 agent/core 剩余约 140 个测试文件,按上述四类拦路虎分批: · 纯黑箱可迁的先迁 · 需要导出入口的,评估"导出是否合理"(如 FrontendAsset 那样) · 真白盒的(改私有状态、测内部算法)加文件头说明为什么必须白盒 --- .../core/{ => test}/stages_plugin_test.go | 24 ++++++++++-------- internal/agent/core/{ => test}/stages_test.go | 25 ++++++++++--------- .../core/{ => test}/stages_tooldef_test.go | 7 +++--- 3 files changed, 31 insertions(+), 25 deletions(-) rename internal/agent/core/{ => test}/stages_plugin_test.go (82%) rename internal/agent/core/{ => test}/stages_test.go (93%) rename internal/agent/core/{ => test}/stages_tooldef_test.go (95%) diff --git a/internal/agent/core/stages_plugin_test.go b/internal/agent/core/test/stages_plugin_test.go similarity index 82% rename from internal/agent/core/stages_plugin_test.go rename to internal/agent/core/test/stages_plugin_test.go index 9acb56b4..f9994360 100644 --- a/internal/agent/core/stages_plugin_test.go +++ b/internal/agent/core/test/stages_plugin_test.go @@ -1,6 +1,7 @@ -package core +package test import ( + "github.com/JianFeeeee/HologramAgent/internal/agent/core" "testing" sdk "github.com/JianFeeeee/HologramAgent/internal/sdk" @@ -13,7 +14,7 @@ import ( // 打向已死进程;重启后新 handler 追加进来,旧的仍不退场—— // 错误与重复执行随重启次数线性累积。 func TestUnregisterPluginStages_RemovesOnlyThatPlugin(t *testing.T) { - h := NewStageHost() + h := core.NewStageHost() var aRan, bRan, coreRan int h.RegisterStageFor("a", sdk.StagePreAction, func(*sdk.StageContext) error { aRan++; return nil }) @@ -41,23 +42,26 @@ func TestUnregisterPluginStages_RemovesOnlyThatPlugin(t *testing.T) { // 摘除某插件的最后一个 handler 后,该 stage 应从表中消失(RunStage 直接短路)。 func TestUnregisterPluginStages_DropsEmptyStage(t *testing.T) { - h := NewStageHost() + h := core.NewStageHost() h.RegisterStageFor("solo", sdk.StageAfterToolcall, func(*sdk.StageContext) error { return nil }) + // ★ 2026-11-04:原先这里用 `h.mu.RLock(); h.stages[...]` 直接读私有字段, + // 判据耦合了内部实现。`UnregisterPluginStages` 的返回值已经表达了同一件事 + // (摘除了几个),而那才是对外承诺。 + // + // 补一条黑箱可观测的断言:摘除后再摘一次必须得到 0 —— 如果内部表没清空, + // 第二次仍会"摘到"残留计数。 if n := h.UnregisterPluginStages("solo"); n != 1 { t.Fatalf("应摘除 1 个,实际 %d", n) } - h.mu.RLock() - _, exists := h.stages[sdk.StageAfterToolcall] - h.mu.RUnlock() - if exists { - t.Error("stage 已无 handler 时应从表中删除") + if n := h.UnregisterPluginStages("solo"); n != 0 { + t.Fatalf("★ 重复摘除应得 0(说明内部已清空),实际 %d", n) } } // 空插件名不得误摘内核自身注册的 handler。 func TestUnregisterPluginStages_EmptyNameIsNoop(t *testing.T) { - h := NewStageHost() + h := core.NewStageHost() ran := 0 h.RegisterStage(sdk.StagePreAction, func(*sdk.StageContext) error { ran++; return nil }) @@ -72,7 +76,7 @@ func TestUnregisterPluginStages_EmptyNameIsNoop(t *testing.T) { // 工具与 stage 的摘除互不干扰:都摘完后两者皆空。 func TestUnregisterPluginToolsAndStages_Together(t *testing.T) { - h := NewStageHost() + h := core.NewStageHost() if err := h.RegisterTool("demo_run", sdk.ToolDef{Name: "demo_run", Plugin: "demo"}, func(map[string]interface{}) (interface{}, error) { return nil, nil }); err != nil { t.Fatal(err) diff --git a/internal/agent/core/stages_test.go b/internal/agent/core/test/stages_test.go similarity index 93% rename from internal/agent/core/stages_test.go rename to internal/agent/core/test/stages_test.go index ff33eff1..39997d6e 100644 --- a/internal/agent/core/stages_test.go +++ b/internal/agent/core/test/stages_test.go @@ -1,6 +1,7 @@ -package core +package test import ( + "github.com/JianFeeeee/HologramAgent/internal/agent/core" "sync" "sync/atomic" "testing" @@ -9,7 +10,7 @@ import ( ) func TestStageHostRegisterTool(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() err := host.RegisterTool("test_tool", sdk.ToolDef{Name: "test_tool"}, func(args map[string]interface{}) (interface{}, error) { return "ok", nil @@ -28,7 +29,7 @@ func TestStageHostRegisterTool(t *testing.T) { } func TestStageHostRegisterToolDuplicate(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() host.RegisterTool("dup", sdk.ToolDef{Name: "dup"}, nil) err := host.RegisterTool("dup", sdk.ToolDef{Name: "dup"}, nil) if err == nil { @@ -37,7 +38,7 @@ func TestStageHostRegisterToolDuplicate(t *testing.T) { } func TestStageHostExecuteTool(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() host.RegisterTool("hello", sdk.ToolDef{Name: "hello"}, func(args map[string]interface{}) (interface{}, error) { return "world", nil @@ -58,7 +59,7 @@ func TestStageHostExecuteTool(t *testing.T) { } func TestStageHostRunStage(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() var called bool host.RegisterStage(sdk.StageOnInput, func(ctx *sdk.StageContext) error { @@ -75,7 +76,7 @@ func TestStageHostRunStage(t *testing.T) { } func TestStageHostRunStageParallel(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() // Two handlers that both try to set Response under Lock. // Only the first to acquire Lock actually wins; the second sees IsResponded() and skips. @@ -110,7 +111,7 @@ func TestStageHostRunStageParallel(t *testing.T) { } func TestStageHostRunStageConcurrency(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() var counter int32 n := 10 @@ -129,7 +130,7 @@ func TestStageHostRunStageConcurrency(t *testing.T) { } func TestStageHostRunStageAll(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() var mu sync.Mutex count := 0 @@ -154,7 +155,7 @@ func TestStageHostRunStageAll(t *testing.T) { } func TestStageHostEmpty(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() if host.ToolCount() != 0 { t.Errorf("expected 0 tools, got %d", host.ToolCount()) @@ -174,7 +175,7 @@ func TestStageHostEmpty(t *testing.T) { } func TestStageHostMultipleTools(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() host.RegisterTool("tool1", sdk.ToolDef{Name: "tool1"}, func(args map[string]interface{}) (interface{}, error) { return "from_p1", nil @@ -199,7 +200,7 @@ func TestStageHostMultipleTools(t *testing.T) { } func TestStageHostToolDefLookup(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() host.RegisterTool("tool_a", sdk.ToolDef{Name: "tool_a", NoMemory: true}, nil) host.RegisterTool("tool_b", sdk.ToolDef{Name: "tool_b"}, nil) @@ -226,7 +227,7 @@ func TestStageHostToolDefLookup(t *testing.T) { } func TestStageHostToolDefNoMemoryStored(t *testing.T) { - host := NewStageHost() + host := core.NewStageHost() host.RegisterTool("mem_tool", sdk.ToolDef{Name: "mem_tool", NoMemory: true}, nil) host.RegisterTool("normal_tool", sdk.ToolDef{Name: "normal_tool", NoMemory: false}, nil) diff --git a/internal/agent/core/stages_tooldef_test.go b/internal/agent/core/test/stages_tooldef_test.go similarity index 95% rename from internal/agent/core/stages_tooldef_test.go rename to internal/agent/core/test/stages_tooldef_test.go index 7c3f3b0d..7b420878 100644 --- a/internal/agent/core/stages_tooldef_test.go +++ b/internal/agent/core/test/stages_tooldef_test.go @@ -1,6 +1,7 @@ -package core +package test import ( + "github.com/JianFeeeee/HologramAgent/internal/agent/core" "sync" "testing" @@ -13,7 +14,7 @@ import ( // 读 ParallelSafe,优化后读成了别的字段,于是工具能并发的批次悄悄 // 退化成串行 —— 没有任何报错。 func TestNoCopyQueriesMatchToolDef(t *testing.T) { - sh := NewStageHost() + sh := core.NewStageHost() noop := func(map[string]interface{}) (interface{}, error) { return nil, nil } cases := []sdk.ToolDef{ {Name: "plain"}, @@ -66,7 +67,7 @@ func TestNoCopyQueriesMatchToolDef(t *testing.T) { // 并发查询必须无竞态,且与串行结果一致。 func TestNoCopyQueriesConcurrent(t *testing.T) { - sh := NewStageHost() + sh := core.NewStageHost() noop := func(map[string]interface{}) (interface{}, error) { return nil, nil } for i := 0; i < 50; i++ { name := "t" + string(rune('a'+i%26)) + string(rune('0'+i/26))