Skip to main content

a3s_code_core/hooks/
events.rs

1//! Hook Event Types
2//!
3//! Defines all event types that can trigger hooks.
4
5use serde::{Deserialize, Serialize};
6
7/// Hook event types
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum HookEventType {
11    /// Before tool execution
12    PreToolUse,
13    /// After tool execution
14    PostToolUse,
15    /// Before LLM generation
16    GenerateStart,
17    /// After LLM generation
18    GenerateEnd,
19    /// When session is created
20    SessionStart,
21    /// When session is destroyed
22    SessionEnd,
23    /// When a skill is loaded
24    SkillLoad,
25    /// When a skill is unloaded
26    SkillUnload,
27    /// Before prompt augmentation (can modify prompt)
28    PrePrompt,
29    /// After LLM response is processed, before returning to user
30    PostResponse,
31    /// When an error occurs (tool failure, LLM error, etc.)
32    OnError,
33}
34
35impl std::fmt::Display for HookEventType {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            HookEventType::PreToolUse => write!(f, "pre_tool_use"),
39            HookEventType::PostToolUse => write!(f, "post_tool_use"),
40            HookEventType::GenerateStart => write!(f, "generate_start"),
41            HookEventType::GenerateEnd => write!(f, "generate_end"),
42            HookEventType::SessionStart => write!(f, "session_start"),
43            HookEventType::SessionEnd => write!(f, "session_end"),
44            HookEventType::SkillLoad => write!(f, "skill_load"),
45            HookEventType::SkillUnload => write!(f, "skill_unload"),
46            HookEventType::PrePrompt => write!(f, "pre_prompt"),
47            HookEventType::PostResponse => write!(f, "post_response"),
48            HookEventType::OnError => write!(f, "on_error"),
49        }
50    }
51}
52
53/// Tool execution result data
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ToolResultData {
56    /// Whether execution succeeded
57    pub success: bool,
58    /// Tool output
59    pub output: String,
60    /// Exit code (for shell commands)
61    pub exit_code: Option<i32>,
62    /// Execution duration in milliseconds
63    pub duration_ms: u64,
64}
65
66/// Pre-tool-use event payload
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct PreToolUseEvent {
69    /// Session ID
70    pub session_id: String,
71    /// Tool name
72    pub tool: String,
73    /// Tool arguments
74    pub args: serde_json::Value,
75    /// Working directory
76    pub working_directory: String,
77    /// Recent tools executed (for context)
78    pub recent_tools: Vec<String>,
79}
80
81/// Post-tool-use event payload
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct PostToolUseEvent {
84    /// Session ID
85    pub session_id: String,
86    /// Tool name
87    pub tool: String,
88    /// Tool arguments
89    pub args: serde_json::Value,
90    /// Execution result
91    pub result: ToolResultData,
92}
93
94/// Generate start event payload
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct GenerateStartEvent {
97    /// Session ID
98    pub session_id: String,
99    /// User prompt
100    pub prompt: String,
101    /// System prompt (if any)
102    pub system_prompt: Option<String>,
103    /// Model provider
104    pub model_provider: String,
105    /// Model name
106    pub model_name: String,
107    /// Available tools
108    pub available_tools: Vec<String>,
109}
110
111/// Generate end event payload
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct GenerateEndEvent {
114    /// Session ID
115    pub session_id: String,
116    /// User prompt
117    pub prompt: String,
118    /// Response text
119    pub response_text: String,
120    /// Tool calls made
121    pub tool_calls: Vec<ToolCallInfo>,
122    /// Token usage
123    pub usage: TokenUsageInfo,
124    /// Duration in milliseconds
125    pub duration_ms: u64,
126}
127
128/// Tool call information
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ToolCallInfo {
131    /// Tool name
132    pub name: String,
133    /// Tool arguments
134    pub args: serde_json::Value,
135}
136
137/// Token usage information
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct TokenUsageInfo {
140    /// Prompt tokens
141    pub prompt_tokens: i32,
142    /// Completion tokens
143    pub completion_tokens: i32,
144    /// Total tokens
145    pub total_tokens: i32,
146}
147
148/// Session start event payload
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SessionStartEvent {
151    /// Session ID
152    pub session_id: String,
153    /// System prompt (if any)
154    pub system_prompt: Option<String>,
155    /// Model configuration
156    pub model_provider: String,
157    pub model_name: String,
158}
159
160/// Session end event payload
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct SessionEndEvent {
163    /// Session ID
164    pub session_id: String,
165    /// Total token usage
166    pub total_tokens: i32,
167    /// Total tool calls
168    pub total_tool_calls: i32,
169    /// Session duration in milliseconds
170    pub duration_ms: u64,
171}
172
173/// Skill load event payload
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct SkillLoadEvent {
176    /// Skill name
177    pub skill_name: String,
178    /// Tool names loaded from the skill
179    pub tool_names: Vec<String>,
180    /// Skill version (if available)
181    pub version: Option<String>,
182    /// Skill description (if available)
183    pub description: Option<String>,
184    /// Timestamp when skill was loaded (Unix milliseconds)
185    pub loaded_at: i64,
186}
187
188/// Skill unload event payload
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct SkillUnloadEvent {
191    /// Skill name
192    pub skill_name: String,
193    /// Tool names that were unloaded
194    pub tool_names: Vec<String>,
195    /// How long the skill was loaded (milliseconds)
196    pub duration_ms: u64,
197}
198
199/// Pre-prompt event payload (fired before prompt augmentation)
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PrePromptEvent {
202    /// Session ID
203    pub session_id: String,
204    /// User prompt text
205    pub prompt: String,
206    /// Current system prompt (if any)
207    pub system_prompt: Option<String>,
208    /// Number of messages in conversation history
209    pub message_count: usize,
210}
211
212/// Post-response event payload (fired after LLM response is processed)
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct PostResponseEvent {
215    /// Session ID
216    pub session_id: String,
217    /// Final response text
218    pub response_text: String,
219    /// Number of tool calls made during this turn
220    pub tool_calls_count: usize,
221    /// Token usage
222    pub usage: TokenUsageInfo,
223    /// Total duration in milliseconds
224    pub duration_ms: u64,
225}
226
227/// Error type classification for OnError events
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum ErrorType {
231    /// Tool execution failed
232    ToolFailure,
233    /// LLM API call failed
234    LlmFailure,
235    /// Permission denied
236    PermissionDenied,
237    /// Timeout
238    Timeout,
239    /// Other error
240    Other,
241}
242
243impl std::fmt::Display for ErrorType {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            ErrorType::ToolFailure => write!(f, "tool_failure"),
247            ErrorType::LlmFailure => write!(f, "llm_failure"),
248            ErrorType::PermissionDenied => write!(f, "permission_denied"),
249            ErrorType::Timeout => write!(f, "timeout"),
250            ErrorType::Other => write!(f, "other"),
251        }
252    }
253}
254
255/// On-error event payload
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct OnErrorEvent {
258    /// Session ID
259    pub session_id: String,
260    /// Error classification
261    pub error_type: ErrorType,
262    /// Error message
263    pub error_message: String,
264    /// Additional context (e.g., tool name, model name)
265    pub context: serde_json::Value,
266}
267
268/// Unified hook event enum
269#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(tag = "event_type", content = "payload")]
271pub enum HookEvent {
272    #[serde(rename = "pre_tool_use")]
273    PreToolUse(PreToolUseEvent),
274    #[serde(rename = "post_tool_use")]
275    PostToolUse(PostToolUseEvent),
276    #[serde(rename = "generate_start")]
277    GenerateStart(GenerateStartEvent),
278    #[serde(rename = "generate_end")]
279    GenerateEnd(GenerateEndEvent),
280    #[serde(rename = "session_start")]
281    SessionStart(SessionStartEvent),
282    #[serde(rename = "session_end")]
283    SessionEnd(SessionEndEvent),
284    #[serde(rename = "skill_load")]
285    SkillLoad(SkillLoadEvent),
286    #[serde(rename = "skill_unload")]
287    SkillUnload(SkillUnloadEvent),
288    #[serde(rename = "pre_prompt")]
289    PrePrompt(PrePromptEvent),
290    #[serde(rename = "post_response")]
291    PostResponse(PostResponseEvent),
292    #[serde(rename = "on_error")]
293    OnError(OnErrorEvent),
294}
295
296impl HookEvent {
297    /// Get the event type
298    pub fn event_type(&self) -> HookEventType {
299        match self {
300            HookEvent::PreToolUse(_) => HookEventType::PreToolUse,
301            HookEvent::PostToolUse(_) => HookEventType::PostToolUse,
302            HookEvent::GenerateStart(_) => HookEventType::GenerateStart,
303            HookEvent::GenerateEnd(_) => HookEventType::GenerateEnd,
304            HookEvent::SessionStart(_) => HookEventType::SessionStart,
305            HookEvent::SessionEnd(_) => HookEventType::SessionEnd,
306            HookEvent::SkillLoad(_) => HookEventType::SkillLoad,
307            HookEvent::SkillUnload(_) => HookEventType::SkillUnload,
308            HookEvent::PrePrompt(_) => HookEventType::PrePrompt,
309            HookEvent::PostResponse(_) => HookEventType::PostResponse,
310            HookEvent::OnError(_) => HookEventType::OnError,
311        }
312    }
313
314    /// Get the session ID (returns empty string for skill events which are global)
315    pub fn session_id(&self) -> &str {
316        match self {
317            HookEvent::PreToolUse(e) => &e.session_id,
318            HookEvent::PostToolUse(e) => &e.session_id,
319            HookEvent::GenerateStart(e) => &e.session_id,
320            HookEvent::GenerateEnd(e) => &e.session_id,
321            HookEvent::SessionStart(e) => &e.session_id,
322            HookEvent::SessionEnd(e) => &e.session_id,
323            HookEvent::PrePrompt(e) => &e.session_id,
324            HookEvent::PostResponse(e) => &e.session_id,
325            HookEvent::OnError(e) => &e.session_id,
326            // Skill events are global (not session-specific)
327            HookEvent::SkillLoad(_) => "",
328            HookEvent::SkillUnload(_) => "",
329        }
330    }
331
332    /// Get the tool name (for tool events)
333    pub fn tool_name(&self) -> Option<&str> {
334        match self {
335            HookEvent::PreToolUse(e) => Some(&e.tool),
336            HookEvent::PostToolUse(e) => Some(&e.tool),
337            _ => None,
338        }
339    }
340
341    /// Get the tool args (for tool events)
342    pub fn tool_args(&self) -> Option<&serde_json::Value> {
343        match self {
344            HookEvent::PreToolUse(e) => Some(&e.args),
345            HookEvent::PostToolUse(e) => Some(&e.args),
346            _ => None,
347        }
348    }
349
350    /// Get the skill name (for skill events)
351    pub fn skill_name(&self) -> Option<&str> {
352        match self {
353            HookEvent::SkillLoad(e) => Some(&e.skill_name),
354            HookEvent::SkillUnload(e) => Some(&e.skill_name),
355            _ => None,
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_hook_event_type_display() {
366        assert_eq!(HookEventType::PreToolUse.to_string(), "pre_tool_use");
367        assert_eq!(HookEventType::PostToolUse.to_string(), "post_tool_use");
368        assert_eq!(HookEventType::GenerateStart.to_string(), "generate_start");
369        assert_eq!(HookEventType::GenerateEnd.to_string(), "generate_end");
370        assert_eq!(HookEventType::SessionStart.to_string(), "session_start");
371        assert_eq!(HookEventType::SessionEnd.to_string(), "session_end");
372        assert_eq!(HookEventType::SkillLoad.to_string(), "skill_load");
373        assert_eq!(HookEventType::SkillUnload.to_string(), "skill_unload");
374    }
375
376    #[test]
377    fn test_pre_tool_use_event() {
378        let event = PreToolUseEvent {
379            session_id: "session-1".to_string(),
380            tool: "Bash".to_string(),
381            args: serde_json::json!({"command": "echo hello"}),
382            working_directory: "/workspace".to_string(),
383            recent_tools: vec!["Read".to_string()],
384        };
385
386        assert_eq!(event.session_id, "session-1");
387        assert_eq!(event.tool, "Bash");
388    }
389
390    #[test]
391    fn test_post_tool_use_event() {
392        let event = PostToolUseEvent {
393            session_id: "session-1".to_string(),
394            tool: "Bash".to_string(),
395            args: serde_json::json!({"command": "echo hello"}),
396            result: ToolResultData {
397                success: true,
398                output: "hello\n".to_string(),
399                exit_code: Some(0),
400                duration_ms: 50,
401            },
402        };
403
404        assert!(event.result.success);
405        assert_eq!(event.result.exit_code, Some(0));
406    }
407
408    #[test]
409    fn test_hook_event_type() {
410        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
411            session_id: "s1".to_string(),
412            tool: "Bash".to_string(),
413            args: serde_json::json!({}),
414            working_directory: "/".to_string(),
415            recent_tools: vec![],
416        });
417
418        assert_eq!(pre_tool.event_type(), HookEventType::PreToolUse);
419        assert_eq!(pre_tool.session_id(), "s1");
420        assert_eq!(pre_tool.tool_name(), Some("Bash"));
421    }
422
423    #[test]
424    fn test_hook_event_serialization() {
425        let event = HookEvent::PreToolUse(PreToolUseEvent {
426            session_id: "s1".to_string(),
427            tool: "Bash".to_string(),
428            args: serde_json::json!({"command": "ls"}),
429            working_directory: "/workspace".to_string(),
430            recent_tools: vec![],
431        });
432
433        let json = serde_json::to_string(&event).unwrap();
434        assert!(json.contains("pre_tool_use"));
435        assert!(json.contains("Bash"));
436
437        // Deserialize back
438        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
439        assert_eq!(parsed.event_type(), HookEventType::PreToolUse);
440    }
441
442    #[test]
443    fn test_generate_events() {
444        let start = GenerateStartEvent {
445            session_id: "s1".to_string(),
446            prompt: "Hello".to_string(),
447            system_prompt: Some("You are helpful".to_string()),
448            model_provider: "anthropic".to_string(),
449            model_name: "claude-3".to_string(),
450            available_tools: vec!["Bash".to_string(), "Read".to_string()],
451        };
452
453        let end = GenerateEndEvent {
454            session_id: "s1".to_string(),
455            prompt: "Hello".to_string(),
456            response_text: "Hi there!".to_string(),
457            tool_calls: vec![],
458            usage: TokenUsageInfo {
459                prompt_tokens: 10,
460                completion_tokens: 5,
461                total_tokens: 15,
462            },
463            duration_ms: 500,
464        };
465
466        assert_eq!(start.prompt, "Hello");
467        assert_eq!(end.response_text, "Hi there!");
468        assert_eq!(end.usage.total_tokens, 15);
469    }
470
471    #[test]
472    fn test_session_events() {
473        let start = SessionStartEvent {
474            session_id: "s1".to_string(),
475            system_prompt: Some("System".to_string()),
476            model_provider: "anthropic".to_string(),
477            model_name: "claude-3".to_string(),
478        };
479
480        let end = SessionEndEvent {
481            session_id: "s1".to_string(),
482            total_tokens: 1000,
483            total_tool_calls: 5,
484            duration_ms: 60000,
485        };
486
487        let start_event = HookEvent::SessionStart(start);
488        let end_event = HookEvent::SessionEnd(end);
489
490        assert_eq!(start_event.event_type(), HookEventType::SessionStart);
491        assert_eq!(end_event.event_type(), HookEventType::SessionEnd);
492        assert!(start_event.tool_name().is_none());
493    }
494
495    #[test]
496    fn test_skill_load_event() {
497        let event = SkillLoadEvent {
498            skill_name: "test-skill".to_string(),
499            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
500            version: Some("1.0.0".to_string()),
501            description: Some("A test skill".to_string()),
502            loaded_at: 1234567890,
503        };
504
505        assert_eq!(event.skill_name, "test-skill");
506        assert_eq!(event.tool_names.len(), 2);
507        assert_eq!(event.version, Some("1.0.0".to_string()));
508        assert_eq!(event.loaded_at, 1234567890);
509    }
510
511    #[test]
512    fn test_skill_unload_event() {
513        let event = SkillUnloadEvent {
514            skill_name: "test-skill".to_string(),
515            tool_names: vec!["tool1".to_string(), "tool2".to_string()],
516            duration_ms: 60000,
517        };
518
519        assert_eq!(event.skill_name, "test-skill");
520        assert_eq!(event.tool_names.len(), 2);
521        assert_eq!(event.duration_ms, 60000);
522    }
523
524    #[test]
525    fn test_hook_event_skill_name() {
526        let load_event = HookEvent::SkillLoad(SkillLoadEvent {
527            skill_name: "my-skill".to_string(),
528            tool_names: vec!["tool1".to_string()],
529            version: None,
530            description: None,
531            loaded_at: 0,
532        });
533
534        let unload_event = HookEvent::SkillUnload(SkillUnloadEvent {
535            skill_name: "my-skill".to_string(),
536            tool_names: vec!["tool1".to_string()],
537            duration_ms: 1000,
538        });
539
540        assert_eq!(load_event.event_type(), HookEventType::SkillLoad);
541        assert_eq!(load_event.skill_name(), Some("my-skill"));
542        assert_eq!(load_event.session_id(), ""); // Skills are global
543
544        assert_eq!(unload_event.event_type(), HookEventType::SkillUnload);
545        assert_eq!(unload_event.skill_name(), Some("my-skill"));
546        assert_eq!(unload_event.session_id(), ""); // Skills are global
547
548        // Non-skill events return None for skill_name
549        let pre_tool = HookEvent::PreToolUse(PreToolUseEvent {
550            session_id: "s1".to_string(),
551            tool: "Bash".to_string(),
552            args: serde_json::json!({}),
553            working_directory: "/".to_string(),
554            recent_tools: vec![],
555        });
556        assert!(pre_tool.skill_name().is_none());
557    }
558
559    #[test]
560    fn test_skill_event_serialization() {
561        let event = HookEvent::SkillLoad(SkillLoadEvent {
562            skill_name: "test-skill".to_string(),
563            tool_names: vec!["tool1".to_string()],
564            version: Some("1.0.0".to_string()),
565            description: None,
566            loaded_at: 1234567890,
567        });
568
569        let json = serde_json::to_string(&event).unwrap();
570        assert!(json.contains("skill_load"));
571        assert!(json.contains("test-skill"));
572        assert!(json.contains("1.0.0"));
573
574        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
575        assert_eq!(parsed.event_type(), HookEventType::SkillLoad);
576        assert_eq!(parsed.skill_name(), Some("test-skill"));
577    }
578
579    #[test]
580    fn test_hook_event_type_display_new_variants() {
581        assert_eq!(HookEventType::PrePrompt.to_string(), "pre_prompt");
582        assert_eq!(HookEventType::PostResponse.to_string(), "post_response");
583        assert_eq!(HookEventType::OnError.to_string(), "on_error");
584    }
585
586    #[test]
587    fn test_pre_prompt_event() {
588        let event = PrePromptEvent {
589            session_id: "s1".to_string(),
590            prompt: "Fix the bug".to_string(),
591            system_prompt: Some("You are helpful".to_string()),
592            message_count: 5,
593        };
594
595        assert_eq!(event.session_id, "s1");
596        assert_eq!(event.prompt, "Fix the bug");
597        assert_eq!(event.message_count, 5);
598
599        let hook_event = HookEvent::PrePrompt(event);
600        assert_eq!(hook_event.event_type(), HookEventType::PrePrompt);
601        assert_eq!(hook_event.session_id(), "s1");
602        assert!(hook_event.tool_name().is_none());
603        assert!(hook_event.skill_name().is_none());
604    }
605
606    #[test]
607    fn test_post_response_event() {
608        let event = PostResponseEvent {
609            session_id: "s1".to_string(),
610            response_text: "Done!".to_string(),
611            tool_calls_count: 3,
612            usage: TokenUsageInfo {
613                prompt_tokens: 100,
614                completion_tokens: 50,
615                total_tokens: 150,
616            },
617            duration_ms: 2000,
618        };
619
620        assert_eq!(event.response_text, "Done!");
621        assert_eq!(event.tool_calls_count, 3);
622        assert_eq!(event.usage.total_tokens, 150);
623
624        let hook_event = HookEvent::PostResponse(event);
625        assert_eq!(hook_event.event_type(), HookEventType::PostResponse);
626        assert_eq!(hook_event.session_id(), "s1");
627    }
628
629    #[test]
630    fn test_on_error_event() {
631        let event = OnErrorEvent {
632            session_id: "s1".to_string(),
633            error_type: ErrorType::ToolFailure,
634            error_message: "Command failed with exit code 1".to_string(),
635            context: serde_json::json!({"tool": "Bash", "command": "false"}),
636        };
637
638        assert_eq!(event.error_type.to_string(), "tool_failure");
639        assert_eq!(event.error_message, "Command failed with exit code 1");
640
641        let hook_event = HookEvent::OnError(event);
642        assert_eq!(hook_event.event_type(), HookEventType::OnError);
643        assert_eq!(hook_event.session_id(), "s1");
644    }
645
646    #[test]
647    fn test_error_type_display() {
648        assert_eq!(ErrorType::ToolFailure.to_string(), "tool_failure");
649        assert_eq!(ErrorType::LlmFailure.to_string(), "llm_failure");
650        assert_eq!(ErrorType::PermissionDenied.to_string(), "permission_denied");
651        assert_eq!(ErrorType::Timeout.to_string(), "timeout");
652        assert_eq!(ErrorType::Other.to_string(), "other");
653    }
654
655    #[test]
656    fn test_new_event_serialization() {
657        // PrePrompt
658        let event = HookEvent::PrePrompt(PrePromptEvent {
659            session_id: "s1".to_string(),
660            prompt: "Hello".to_string(),
661            system_prompt: None,
662            message_count: 0,
663        });
664        let json = serde_json::to_string(&event).unwrap();
665        assert!(json.contains("pre_prompt"));
666        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
667        assert_eq!(parsed.event_type(), HookEventType::PrePrompt);
668
669        // PostResponse
670        let event = HookEvent::PostResponse(PostResponseEvent {
671            session_id: "s1".to_string(),
672            response_text: "Hi".to_string(),
673            tool_calls_count: 0,
674            usage: TokenUsageInfo {
675                prompt_tokens: 10,
676                completion_tokens: 5,
677                total_tokens: 15,
678            },
679            duration_ms: 100,
680        });
681        let json = serde_json::to_string(&event).unwrap();
682        assert!(json.contains("post_response"));
683        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
684        assert_eq!(parsed.event_type(), HookEventType::PostResponse);
685
686        // OnError
687        let event = HookEvent::OnError(OnErrorEvent {
688            session_id: "s1".to_string(),
689            error_type: ErrorType::LlmFailure,
690            error_message: "API timeout".to_string(),
691            context: serde_json::json!({}),
692        });
693        let json = serde_json::to_string(&event).unwrap();
694        assert!(json.contains("on_error"));
695        let parsed: HookEvent = serde_json::from_str(&json).unwrap();
696        assert_eq!(parsed.event_type(), HookEventType::OnError);
697    }
698}