From 29bfad63d679e8b038eba5025c4a849f476d101d Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 14 Nov 2026 10:27:55 +0800 Subject: [PATCH] =?UTF-8?q?test(agent/core):=20StageHost=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=A4=B1=E8=B4=A5=E8=AF=AD=E4=B9=89=E4=B8=8E=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=91=98=E9=99=A4=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 TestRunStageCollectsHandlerErrors handler 返回 error → 汇总进 ctx.Errors,其他 handler 的产物保留 新增 TestRunStagePanicDoesNotKillRun handler panic 被隔离,其余 handler 照常执行 新增 TestUnregisterPluginToolsOnlyThatPlugin 工具摘除只影响目标插件,其他插件工具仍可执行 变异验证: 去掉 error 收集 → errors=[] 断言红 去掉 recover 隔离 → panic 直接炸掉测试进程(rc=1) --- internal/agent/core/test/stages_test.go | 63 ++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/internal/agent/core/test/stages_test.go b/internal/agent/core/test/stages_test.go index 39997d6e..148a0d1b 100644 --- a/internal/agent/core/test/stages_test.go +++ b/internal/agent/core/test/stages_test.go @@ -1,11 +1,13 @@ package test import ( - "github.com/JianFeeeee/HologramAgent/internal/agent/core" + "fmt" + "strings" "sync" "sync/atomic" "testing" + "github.com/JianFeeeee/HologramAgent/internal/agent/core" sdk "github.com/JianFeeeee/HologramAgent/internal/sdk" ) @@ -249,3 +251,62 @@ func TestStageHostToolDefNoMemoryStored(t *testing.T) { t.Error("normal_tool.NoMemory should be false") } } + +// TestRunStageCollectsHandlerErrors:handler 返回的错误必须汇总到 +// ctx.Errors,且不阻断其他 handler(模块级失败语义)。 +func TestRunStageCollectsHandlerErrors(t *testing.T) { + h := core.NewStageHost() + h.RegisterStage(sdk.StagePreAction, func(*sdk.StageContext) error { + return fmt.Errorf("boom-from-handler") + }) + h.RegisterStage(sdk.StagePreAction, func(ctx *sdk.StageContext) error { + kept := "kept" + ctx.Lock() + ctx.Response = &kept + ctx.Unlock() + return nil + }) + ctx := &sdk.StageContext{} + h.RunStage(sdk.StagePreAction, ctx) + if len(ctx.Errors) != 1 || ctx.Errors[0] != "boom-from-handler" { + t.Fatalf("errors = %v", ctx.Errors) + } + ctx.RLock() + defer ctx.RUnlock() + if ctx.Response == nil || *ctx.Response != "kept" { + t.Fatalf("healthy handler output lost: %v", ctx.Response) + } +} + +// TestRunStagePanicDoesNotKillRun:handler panic 必须被隔离, +// 其余 handler 照常执行且 RunStage 正常返回。 +func TestRunStagePanicDoesNotKillRun(t *testing.T) { + h := core.NewStageHost() + h.RegisterStage(sdk.StageAfterOutput, func(*sdk.StageContext) error { panic("handler exploded") }) + var ran int + h.RegisterStage(sdk.StageAfterOutput, func(*sdk.StageContext) error { ran++; return nil }) + h.RunStage(sdk.StageAfterOutput, &sdk.StageContext{}) + if ran != 1 { + t.Fatalf("healthy handler skipped after a panic: ran=%d", ran) + } +} + +// TestUnregisterPluginToolsOnlyThatPlugin:工具摘除必须只影响目标插件。 +func TestUnregisterPluginToolsOnlyThatPlugin(t *testing.T) { + h := core.NewStageHost() + for _, name := range []string{"a_tool", "b_tool"} { + pluginName := strings.SplitN(name, "_", 2)[0] + h.RegisterTool(name, sdk.ToolDef{Name: name, Plugin: pluginName}, + func(map[string]interface{}) (interface{}, error) { return "ok", nil }) + } + h.UnregisterPluginTools("a") + if h.HasTool("a_tool") { + t.Fatal("unregistered plugin tool still executing") + } + if !h.HasTool("b_tool") { + t.Fatal("unrelated plugin tool was removed") + } + if _, err := h.ExecuteTool("b_tool", nil); err != nil { + t.Fatalf("surviving tool broke: %v", err) + } +}