// session_registry_test.go - 14.P1-C Session-scoped Hook Registry 测试. // // 覆盖场景: // - ResolveHooksMgr(nil, engine) 返回 engine(向后兼容) // - ResolveHooksMgr(session, nil) 返回 session // - ResolveHooksMgr(nil, nil) 返回 nil // - NewCompositeManager:session hooks 先触发,engine hooks 后触发 // - 合并后 HasHooks 正确检查 // - WithSessionHooksCtx / SessionHooksFromCtx 上下文传递 package hooks import ( "context" "testing" "git.flytoex.net/yuanwei/flyto-agent/pkg/execenv" ) // TestResolveHooksMgr_NilSession nil session 时返回 engine(向后兼容). func TestResolveHooksMgr_NilSession(t *testing.T) { engine := NewManager(nil, execenv.DefaultExecutor{}) result := ResolveHooksMgr(nil, engine) if result != engine { t.Error("nil session 时应直接返回 engine Manager(零开销向后兼容)") } } // TestResolveHooksMgr_NilEngine nil engine 时返回 session. func TestResolveHooksMgr_NilEngine(t *testing.T) { session := NewManager(nil, execenv.DefaultExecutor{}) result := ResolveHooksMgr(session, nil) if result != session { t.Error("nil engine 时应直接返回 session Manager") } } // TestResolveHooksMgr_BothNil 两者都 nil 时返回 nil. func TestResolveHooksMgr_BothNil(t *testing.T) { result := ResolveHooksMgr(nil, nil) if result != nil { t.Error("两者都 nil 时应返回 nil") } } // TestResolveHooksMgr_BothPresent 两者都存在时返回 CompositeManager. func TestResolveHooksMgr_BothPresent(t *testing.T) { session := NewManager(nil, execenv.DefaultExecutor{}) engine := NewManager(nil, execenv.DefaultExecutor{}) result := ResolveHooksMgr(session, engine) if result == nil { t.Fatal("两者都存在时应返回非 nil CompositeManager") } // 结果不应是原始的 session 或 engine(应是新的合并 Manager) if result == session || result == engine { t.Error("两者都存在时应返回新的合并 Manager,而非原始指针") } } // TestNewCompositeManager_SessionHookFirst session hooks 先于 engine hooks 触发. // // 精妙之处(CLEVER): 使用 CallbackHandler 记录触发顺序-- // 不需要真实的 shell 命令,直接用 Go 回调追踪执行序列. // 验证"session first, engine second"的合并语义. func TestNewCompositeManager_SessionHookFirst(t *testing.T) { var order []string session := NewManager(nil, execenv.DefaultExecutor{}) if err := session.Register(HookPreToolUse, HookDef{ Handler: HookHandlerFunc(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { order = append(order, "session") return &HookResult{ExitCode: 0} }), }); err != nil { t.Fatalf("session.Register: %v", err) } engine := NewManager(nil, execenv.DefaultExecutor{}) if err := engine.Register(HookPreToolUse, HookDef{ Handler: HookHandlerFunc(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { order = append(order, "engine") return &HookResult{ExitCode: 0} }), }); err != nil { t.Fatalf("engine.Register: %v", err) } composite := NewCompositeManager(session, engine) if composite == nil { t.Fatal("CompositeManager 不应为 nil") } _, err := composite.Execute(context.Background(), HookPreToolUse, nil) if err != nil { t.Fatalf("Execute: %v", err) } if len(order) != 2 { t.Fatalf("应触发 2 个 hooks,got %d: %v", len(order), order) } // 升华改进(ELEVATED): session 先于 engine-- // 租户审计 hook 先执行,然后才是全局 hook,确保合规拦截的优先权. if order[0] != "session" || order[1] != "engine" { t.Errorf("期望顺序 [session, engine],实际 %v", order) } } // TestNewCompositeManager_HasHooks_UnionSemantic HasHooks 取两者的并集. func TestNewCompositeManager_HasHooks_UnionSemantic(t *testing.T) { session := NewManager(nil, execenv.DefaultExecutor{}) // session 只有 PreToolUse if err := session.Register(HookPreToolUse, HookDef{ Handler: HookHandlerFunc(func(_ context.Context, _ HookType, _ map[string]string) *HookResult { return &HookResult{ExitCode: 0} }), }); err != nil { t.Fatalf("session.Register: %v", err) } engine := NewManager(nil, execenv.DefaultExecutor{}) // engine 只有 HookStop if err := engine.Register(HookStop, HookDef{ Handler: HookHandlerFunc(func(_ context.Context, _ HookType, _ map[string]string) *HookResult { return &HookResult{ExitCode: 0} }), }); err != nil { t.Fatalf("engine.Register: %v", err) } composite := NewCompositeManager(session, engine) // 合并后应能看到两个 hook 类型 if !composite.HasHooks(HookPreToolUse) { t.Error("composite 应包含 session 的 PreToolUse hook") } if !composite.HasHooks(HookStop) { t.Error("composite 应包含 engine 的 Stop hook") } // 两者都没有的类型应返回 false if composite.HasHooks(HookNotification) { t.Error("composite 不应有 Notification hook(两者都未注册)") } } // TestNewCompositeManager_EngineOnlyHooks 只有 engine hooks 时 session 不贡献. func TestNewCompositeManager_EngineOnlyHooks(t *testing.T) { var triggered []string session := NewManager(nil, execenv.DefaultExecutor{}) // session 没有 hook engine := NewManager(nil, execenv.DefaultExecutor{}) if err := engine.Register(HookPostToolUse, HookDef{ Handler: HookHandlerFunc(func(_ context.Context, _ HookType, _ map[string]string) *HookResult { triggered = append(triggered, "engine-post") return &HookResult{ExitCode: 0} }), }); err != nil { t.Fatalf("engine.Register: %v", err) } composite := NewCompositeManager(session, engine) composite.Execute(context.Background(), HookPostToolUse, nil) if len(triggered) != 1 || triggered[0] != "engine-post" { t.Errorf("应触发 engine-post,got %v", triggered) } } // TestNewCompositeManager_SessionOnlyHooks 只有 session hooks 时 engine 不贡献. func TestNewCompositeManager_SessionOnlyHooks(t *testing.T) { var triggered []string session := NewManager(nil, execenv.DefaultExecutor{}) if err := session.Register(HookPreSampling, HookDef{ Handler: HookHandlerFunc(func(_ context.Context, _ HookType, _ map[string]string) *HookResult { triggered = append(triggered, "session-pre-sampling") return &HookResult{ExitCode: 0} }), }); err != nil { t.Fatalf("session.Register: %v", err) } engine := NewManager(nil, execenv.DefaultExecutor{}) // engine 没有 hook composite := NewCompositeManager(session, engine) composite.Execute(context.Background(), HookPreSampling, nil) if len(triggered) != 1 || triggered[0] != "session-pre-sampling" { t.Errorf("应触发 session-pre-sampling,got %v", triggered) } } // TestCompositeManager_IsolatedFromOriginals 合并不修改原始 Manager. // // 升华改进(ELEVATED): 合并是副本语义-- // 原始 session 和 engine Manager 不受影响,确保多请求并发安全. // 一个请求的 CompositeManager 不会干扰另一个请求. func TestCompositeManager_IsolatedFromOriginals(t *testing.T) { session := NewManager(nil, execenv.DefaultExecutor{}) if err := session.Register(HookPreToolUse, HookDef{ Handler: HookHandlerFunc(func(_ context.Context, _ HookType, _ map[string]string) *HookResult { return &HookResult{ExitCode: 0} }), }); err != nil { t.Fatalf("session.Register: %v", err) } engine := NewManager(nil, execenv.DefaultExecutor{}) // 创建 composite _ = NewCompositeManager(session, engine) // 原始 engine 不应有 PreToolUse hook(session 的 hook 没有泄漏进去) if engine.HasHooks(HookPreToolUse) { t.Error("CompositeManager 创建不应修改原始 engine Manager") } } // ---- context 传播 ---- // TestSessionHooksCtx_RoundTrip context 存取往返测试. func TestSessionHooksCtx_RoundTrip(t *testing.T) { mgr := NewManager(nil, execenv.DefaultExecutor{}) ctx := WithSessionHooksCtx(context.Background(), mgr) got := SessionHooksFromCtx(ctx) if got != mgr { t.Error("SessionHooksFromCtx 应返回注入的 Manager") } } // TestSessionHooksCtx_NilManager nil Manager 时不注入(不 panic). func TestSessionHooksCtx_NilManager(t *testing.T) { ctx := WithSessionHooksCtx(context.Background(), nil) got := SessionHooksFromCtx(ctx) if got != nil { t.Error("nil Manager 不应被注入 context") } } // TestSessionHooksFromCtx_MissingKey context 中无 session hooks 时返回 nil. func TestSessionHooksFromCtx_MissingKey(t *testing.T) { ctx := context.Background() got := SessionHooksFromCtx(ctx) if got != nil { t.Error("context 中无 session hooks 时应返回 nil") } }