package hooks import ( "context" "testing" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/execenv" ) // ============================================================ // HookHandlerFunc 测试 // ============================================================ func TestHookHandlerFunc(t *testing.T) { called := false fn := HookHandlerFunc(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { called = true if hookType != HookPreToolUse { t.Errorf("hookType = %s, want pre_tool_use", hookType) } if env["TOOL_NAME"] != "Bash" { t.Errorf("TOOL_NAME = %q, want Bash", env["TOOL_NAME"]) } return &HookResult{ExitCode: 0, Stdout: "ok"} }) result := fn.ExecuteHook(context.Background(), HookPreToolUse, map[string]string{"TOOL_NAME": "Bash"}) if !called { t.Error("handler function should have been called") } if result.ExitCode != 0 { t.Errorf("ExitCode = %d, want 0", result.ExitCode) } } // ============================================================ // CallbackHandler 测试 // ============================================================ func TestCallbackHandler_Basic(t *testing.T) { h := NewCallbackHandler(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { return &HookResult{ ExitCode: 0, Stdout: `{"decision":"allow"}`, JSONOutput: map[string]any{"decision": "allow"}, } }) result := h.ExecuteHook(context.Background(), HookPermission, nil) if result.ExitCode != 0 { t.Errorf("ExitCode = %d, want 0", result.ExitCode) } if result.Duration <= 0 { t.Error("Duration should be positive") } } func TestCallbackHandler_Block(t *testing.T) { h := NewCallbackHandler(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { return &HookResult{ExitCode: 2, Stderr: "dangerous operation"} }) result := h.ExecuteHook(context.Background(), HookPreToolUse, nil) if result.ExitCode != 2 { t.Errorf("ExitCode = %d, want 2", result.ExitCode) } } func TestCallbackHandler_Timeout(t *testing.T) { h := NewCallbackHandlerWithTimeout(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { <-ctx.Done() // 等到超时 return &HookResult{ExitCode: 0} }, 100*time.Millisecond) result := h.ExecuteHook(context.Background(), HookPreToolUse, nil) if result.Error == nil { t.Error("should have timeout error") } if result.ExitCode != -1 { t.Errorf("ExitCode = %d, want -1 (timeout)", result.ExitCode) } } func TestCallbackHandler_NilResult(t *testing.T) { h := NewCallbackHandler(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { return nil // 回调返回 nil }) result := h.ExecuteHook(context.Background(), HookPreToolUse, nil) if result == nil { t.Fatal("should return non-nil even if callback returns nil") } if result.ExitCode != 0 { t.Errorf("ExitCode = %d, want 0", result.ExitCode) } } // ============================================================ // ShellHandler 测试 // ============================================================ func TestShellHandler_Basic(t *testing.T) { h := NewShellHandler(HookDef{Command: "echo hello", Timeout: 5}, execenv.DefaultExecutor{}) result := h.ExecuteHook(context.Background(), HookPreToolUse, nil) if result.ExitCode != 0 { t.Errorf("ExitCode = %d, want 0", result.ExitCode) } if result.Stdout != "hello\n" { t.Errorf("Stdout = %q, want 'hello\\n'", result.Stdout) } } // ============================================================ // Manager 多后端测试 // ============================================================ func TestManager_CallbackHandler(t *testing.T) { m := NewManager(nil, execenv.DefaultExecutor{}) called := false m.Register(HookPreToolUse, HookDef{ Handler: NewCallbackHandler(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { called = true return &HookResult{ExitCode: 0} }), }) results, err := m.Execute(context.Background(), HookPreToolUse, nil) if err != nil { t.Fatalf("Execute error: %v", err) } if !called { t.Error("callback handler should have been called") } if len(results.Results) != 1 { t.Errorf("expected 1 result, got %d", len(results.Results)) } } func TestManager_HandlerPriorityOverCommand(t *testing.T) { m := NewManager(nil, execenv.DefaultExecutor{}) handlerCalled := false m.Register(HookPreToolUse, HookDef{ Command: "echo should_not_run", Handler: NewCallbackHandler(func(ctx context.Context, hookType HookType, env map[string]string) *HookResult { handlerCalled = true return &HookResult{ExitCode: 0, Stdout: "from handler"} }), }) results, _ := m.Execute(context.Background(), HookPreToolUse, nil) if !handlerCalled { t.Error("Handler should take priority over Command") } if results.Results[0].Stdout != "from handler" { t.Errorf("Stdout = %q, want 'from handler'", results.Results[0].Stdout) } } func TestManager_RegisterRequiresCommandOrHandler(t *testing.T) { m := NewManager(nil, execenv.DefaultExecutor{}) err := m.Register(HookPreToolUse, HookDef{}) if err == nil { t.Error("should require command or handler") } }