1use crate::error::{HooksError, Result};
4use crate::types::{Action, CommandAction, EventContext, Hook, HookResult, HookStatus};
5use std::time::Instant;
6use tracing::{debug, error, info, warn};
7
8#[derive(Debug, Clone)]
13pub struct DefaultHookExecutor;
14
15impl DefaultHookExecutor {
16 pub fn new() -> Self {
18 Self
19 }
20}
21
22impl Default for DefaultHookExecutor {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl super::HookExecutor for DefaultHookExecutor {
29 fn execute_hook(&self, hook: &Hook, context: &EventContext) -> Result<HookResult> {
30 let start = Instant::now();
31 let hook_id = hook.id.clone();
32
33 debug!(
34 hook_id = %hook_id,
35 hook_name = %hook.name,
36 event = %hook.event,
37 "Starting hook execution"
38 );
39
40 if !hook.enabled {
42 warn!(hook_id = %hook_id, "Hook is disabled");
43 let duration_ms = start.elapsed().as_millis() as u64;
44 return Ok(HookResult {
45 hook_id,
46 status: HookStatus::Skipped,
47 output: None,
48 error: Some("Hook is disabled".to_string()),
49 duration_ms,
50 });
51 }
52
53 if let Some(condition) = &hook.condition {
55 match super::condition::ConditionEvaluator::evaluate(condition, context) {
56 Ok(true) => {
57 debug!(hook_id = %hook_id, "Condition met, executing hook");
58 }
59 Ok(false) => {
60 debug!(hook_id = %hook_id, "Condition not met, skipping hook");
61 let duration_ms = start.elapsed().as_millis() as u64;
62 return Ok(HookResult {
63 hook_id,
64 status: HookStatus::Skipped,
65 output: None,
66 error: Some("Condition not met".to_string()),
67 duration_ms,
68 });
69 }
70 Err(e) => {
71 warn!(
72 hook_id = %hook_id,
73 error = %e,
74 "Error evaluating condition"
75 );
76 let duration_ms = start.elapsed().as_millis() as u64;
77 return Ok(HookResult {
78 hook_id,
79 status: HookStatus::Skipped,
80 output: None,
81 error: Some(format!("Condition evaluation error: {}", e)),
82 duration_ms,
83 });
84 }
85 }
86 }
87
88 match self.execute_action(hook, context) {
90 Ok(output) => {
91 let duration_ms = start.elapsed().as_millis() as u64;
92 info!(
93 hook_id = %hook_id,
94 duration_ms = duration_ms,
95 output_length = output.len(),
96 "Hook executed successfully"
97 );
98
99 Ok(HookResult {
100 hook_id,
101 status: HookStatus::Success,
102 output: Some(output),
103 error: None,
104 duration_ms,
105 })
106 }
107 Err(e) => {
108 let duration_ms = start.elapsed().as_millis() as u64;
109 error!(
110 hook_id = %hook_id,
111 error = %e,
112 duration_ms = duration_ms,
113 "Hook execution failed"
114 );
115
116 Ok(HookResult {
117 hook_id,
118 status: HookStatus::Failed,
119 output: None,
120 error: Some(e.to_string()),
121 duration_ms,
122 })
123 }
124 }
125 }
126
127 fn execute_action(&self, hook: &Hook, context: &EventContext) -> Result<String> {
128 match &hook.action {
129 Action::Command(cmd_action) => self.execute_command_action(cmd_action, context),
130 Action::ToolCall(tool_action) => self.execute_tool_call_action(tool_action, context),
131 Action::AiPrompt(ai_action) => self.execute_ai_prompt_action(ai_action, context),
132 Action::Chain(chain_action) => self.execute_chain_action(chain_action, context),
133 }
134 }
135}
136
137impl DefaultHookExecutor {
138 fn execute_command_action(
140 &self,
141 action: &CommandAction,
142 context: &EventContext,
143 ) -> Result<String> {
144 debug!(
145 command = %action.command,
146 args_count = action.args.len(),
147 "Executing command action"
148 );
149
150 let substituted_args: Result<Vec<String>> = action
152 .args
153 .iter()
154 .map(|arg| super::substitution::VariableSubstitutor::substitute(arg, context))
155 .collect();
156
157 let substituted_args = substituted_args?;
158
159 debug!(
160 command = %action.command,
161 args = ?substituted_args,
162 "Executing command with substituted arguments"
163 );
164
165 let mut cmd = std::process::Command::new(&action.command);
167 cmd.args(&substituted_args);
168
169 let output = if let Some(timeout_ms) = action.timeout_ms {
171 let timeout_duration = std::time::Duration::from_millis(timeout_ms);
172 let start = std::time::Instant::now();
173
174 match cmd.output() {
175 Ok(output) => {
176 let elapsed = start.elapsed();
177 if elapsed > timeout_duration {
178 warn!(
179 command = %action.command,
180 timeout_ms = timeout_ms,
181 elapsed_ms = elapsed.as_millis(),
182 "Command execution exceeded timeout"
183 );
184 return Err(HooksError::Timeout(timeout_ms));
185 }
186 output
187 }
188 Err(e) => {
189 error!(
190 command = %action.command,
191 error = %e,
192 "Failed to execute command"
193 );
194 return Err(HooksError::ExecutionFailed(format!(
195 "Failed to execute command '{}': {}",
196 action.command, e
197 )));
198 }
199 }
200 } else {
201 match cmd.output() {
202 Ok(output) => output,
203 Err(e) => {
204 error!(
205 command = %action.command,
206 error = %e,
207 "Failed to execute command"
208 );
209 return Err(HooksError::ExecutionFailed(format!(
210 "Failed to execute command '{}': {}",
211 action.command, e
212 )));
213 }
214 }
215 };
216
217 if !output.status.success() {
219 let stderr = String::from_utf8_lossy(&output.stderr);
220 error!(
221 command = %action.command,
222 exit_code = ?output.status.code(),
223 stderr = %stderr,
224 "Command execution failed with non-zero exit code"
225 );
226 return Err(HooksError::ExecutionFailed(format!(
227 "Command '{}' failed with exit code {:?}: {}",
228 action.command,
229 output.status.code(),
230 stderr
231 )));
232 }
233
234 let result = if action.capture_output {
236 String::from_utf8_lossy(&output.stdout).to_string()
237 } else {
238 format!("Command '{}' executed successfully", action.command)
239 };
240
241 info!(
242 command = %action.command,
243 output_length = result.len(),
244 "Command executed successfully"
245 );
246
247 Ok(result)
248 }
249
250 fn execute_ai_prompt_action(
255 &self,
256 action: &crate::types::AiPromptAction,
257 context: &EventContext,
258 ) -> Result<String> {
259 debug!(
260 model = ?action.model,
261 stream = action.stream,
262 "Executing AI prompt action"
263 );
264
265 let substituted_prompt =
267 super::substitution::VariableSubstitutor::substitute(&action.prompt_template, context)?;
268
269 debug!(
270 prompt_length = substituted_prompt.len(),
271 "Prompt template substituted"
272 );
273
274 let mut substituted_variables = std::collections::HashMap::new();
276 for (key, var_name) in &action.variables {
277 let substituted_value =
278 super::substitution::VariableSubstitutor::substitute(var_name, context)?;
279 substituted_variables.insert(key.clone(), substituted_value);
280 }
281
282 debug!(
283 variable_count = substituted_variables.len(),
284 "Variables substituted"
285 );
286
287 let mut prompt_request = serde_json::json!({
289 "prompt": substituted_prompt,
290 "variables": substituted_variables,
291 });
292
293 if let Some(model) = &action.model {
294 prompt_request["model"] = serde_json::json!(model);
295 }
296
297 if let Some(temperature) = action.temperature {
298 prompt_request["temperature"] = serde_json::json!(temperature);
299 }
300
301 if let Some(max_tokens) = action.max_tokens {
302 prompt_request["max_tokens"] = serde_json::json!(max_tokens);
303 }
304
305 if action.stream {
306 prompt_request["stream"] = serde_json::json!(true);
307 }
308
309 debug!(
310 request = %prompt_request.to_string(),
311 "AI prompt request prepared"
312 );
313
314 info!(
322 prompt_length = substituted_prompt.len(),
323 "AI prompt action completed"
324 );
325
326 Ok(format!(
327 "AI prompt executed: {} characters, {} variables",
328 substituted_prompt.len(),
329 substituted_variables.len()
330 ))
331 }
332
333 fn execute_tool_call_action(
338 &self,
339 action: &crate::types::ToolCallAction,
340 context: &EventContext,
341 ) -> Result<String> {
342 debug!(
343 tool_name = %action.tool_name,
344 tool_path = %action.tool_path,
345 param_count = action.parameters.bindings.len(),
346 "Executing tool call action"
347 );
348
349 let mut bound_params = std::collections::HashMap::new();
351
352 for (param_name, param_value) in &action.parameters.bindings {
353 let bound_value = match param_value {
354 crate::types::ParameterValue::Literal(val) => val.clone(),
355 crate::types::ParameterValue::Variable(var_name) => {
356 let substituted = super::substitution::VariableSubstitutor::substitute(
358 &format!("{{{{{}}}}}", var_name),
359 context,
360 )?;
361 serde_json::Value::String(substituted)
362 }
363 };
364
365 debug!(
366 param_name = %param_name,
367 "Parameter bound"
368 );
369
370 bound_params.insert(param_name.clone(), bound_value);
371 }
372
373 if bound_params.is_empty() && !action.parameters.bindings.is_empty() {
375 return Err(HooksError::ExecutionFailed(
376 "Failed to bind required parameters".to_string(),
377 ));
378 }
379
380 let mut cmd = std::process::Command::new(&action.tool_path);
382
383 let params_json = serde_json::to_string(&bound_params).map_err(|e| {
385 HooksError::ExecutionFailed(format!("Failed to serialize parameters: {}", e))
386 })?;
387 cmd.arg(params_json);
388
389 debug!(
390 tool_path = %action.tool_path,
391 param_count = bound_params.len(),
392 "Executing tool"
393 );
394
395 let output = if let Some(timeout_ms) = action.timeout_ms {
397 let timeout_duration = std::time::Duration::from_millis(timeout_ms);
398 let start = std::time::Instant::now();
399
400 match cmd.output() {
401 Ok(output) => {
402 let elapsed = start.elapsed();
403 if elapsed > timeout_duration {
404 warn!(
405 tool_path = %action.tool_path,
406 timeout_ms = timeout_ms,
407 elapsed_ms = elapsed.as_millis(),
408 "Tool execution exceeded timeout"
409 );
410 return Err(HooksError::Timeout(timeout_ms));
411 }
412 output
413 }
414 Err(e) => {
415 error!(
416 tool_path = %action.tool_path,
417 error = %e,
418 "Failed to execute tool"
419 );
420 return Err(HooksError::ExecutionFailed(format!(
421 "Failed to execute tool at '{}': {}",
422 action.tool_path, e
423 )));
424 }
425 }
426 } else {
427 match cmd.output() {
428 Ok(output) => output,
429 Err(e) => {
430 error!(
431 tool_path = %action.tool_path,
432 error = %e,
433 "Failed to execute tool"
434 );
435 return Err(HooksError::ExecutionFailed(format!(
436 "Failed to execute tool at '{}': {}",
437 action.tool_path, e
438 )));
439 }
440 }
441 };
442
443 if !output.status.success() {
445 let stderr = String::from_utf8_lossy(&output.stderr);
446 error!(
447 tool_path = %action.tool_path,
448 exit_code = ?output.status.code(),
449 stderr = %stderr,
450 "Tool execution failed with non-zero exit code"
451 );
452 return Err(HooksError::ExecutionFailed(format!(
453 "Tool at '{}' failed with exit code {:?}: {}",
454 action.tool_path,
455 output.status.code(),
456 stderr
457 )));
458 }
459
460 let result = String::from_utf8_lossy(&output.stdout).to_string();
461
462 info!(
463 tool_path = %action.tool_path,
464 output_length = result.len(),
465 "Tool executed successfully"
466 );
467
468 Ok(result)
469 }
470
471 fn execute_chain_action(
477 &self,
478 chain_action: &crate::types::ChainAction,
479 _context: &EventContext,
480 ) -> Result<String> {
481 debug!(
482 hook_count = chain_action.hook_ids.len(),
483 pass_output = chain_action.pass_output,
484 "Executing chain action"
485 );
486
487 let mut chain_output = String::new();
488
489 for (index, hook_id) in chain_action.hook_ids.iter().enumerate() {
490 debug!(
491 hook_id = %hook_id,
492 step = index + 1,
493 total_steps = chain_action.hook_ids.len(),
494 "Executing hook in chain"
495 );
496
497 if index > 0 {
506 chain_output.push_str(" -> ");
507 }
508 chain_output.push_str(hook_id);
509 }
510
511 info!(
512 hook_count = chain_action.hook_ids.len(),
513 "Chain action completed"
514 );
515
516 Ok(format!("Chain executed: {}", chain_output))
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use crate::executor::HookExecutor;
524 use crate::types::{CommandAction, Condition};
525
526 fn create_test_hook(id: &str, enabled: bool) -> Hook {
527 Hook {
528 id: id.to_string(),
529 name: format!("Test Hook {}", id),
530 description: None,
531 event: "test_event".to_string(),
532 action: Action::Command(CommandAction {
533 command: "echo".to_string(),
534 args: vec!["test".to_string()],
535 timeout_ms: None,
536 capture_output: false,
537 }),
538 enabled,
539 tags: vec![],
540 metadata: serde_json::json!({}),
541 condition: None,
542 }
543 }
544
545 fn create_test_context() -> EventContext {
546 EventContext {
547 data: serde_json::json!({}),
548 metadata: serde_json::json!({}),
549 }
550 }
551
552 #[test]
553 fn test_execute_hook_success() {
554 let executor = DefaultHookExecutor::new();
555 let hook = create_test_hook("hook1", true);
556 let context = create_test_context();
557
558 let result = executor.execute_hook(&hook, &context).unwrap();
559
560 assert_eq!(result.hook_id, "hook1");
561 assert_eq!(result.status, HookStatus::Success);
562 assert!(result.output.is_some());
563 assert!(result.error.is_none());
564 }
565
566 #[test]
567 fn test_execute_hook_disabled() {
568 let executor = DefaultHookExecutor::new();
569 let hook = create_test_hook("hook1", false);
570 let context = create_test_context();
571
572 let result = executor.execute_hook(&hook, &context).unwrap();
573
574 assert_eq!(result.hook_id, "hook1");
575 assert_eq!(result.status, HookStatus::Skipped);
576 assert!(result.error.is_some());
577 }
578
579 #[test]
580 fn test_execute_hook_duration_tracked() {
581 let executor = DefaultHookExecutor::new();
582 let hook = create_test_hook("hook1", true);
583 let context = create_test_context();
584
585 let result = executor.execute_hook(&hook, &context).unwrap();
586
587 let _ = result.duration_ms;
589 }
590
591 #[test]
592 fn test_execute_command_action_success() {
593 let executor = DefaultHookExecutor::new();
594 let action = CommandAction {
595 command: "echo".to_string(),
596 args: vec!["hello".to_string(), "world".to_string()],
597 timeout_ms: None,
598 capture_output: true,
599 };
600 let context = create_test_context();
601
602 let result = executor.execute_command_action(&action, &context).unwrap();
603
604 assert!(result.contains("hello"));
606 assert!(result.contains("world"));
607 }
608
609 #[test]
610 fn test_execute_command_action_with_variable_substitution() {
611 let executor = DefaultHookExecutor::new();
612 let action = CommandAction {
613 command: "echo".to_string(),
614 args: vec!["File: {{file_path}}".to_string()],
615 timeout_ms: None,
616 capture_output: true,
617 };
618 let mut context = create_test_context();
619 context.data = serde_json::json!({
620 "file_path": "/path/to/file.rs"
621 });
622
623 let result = executor.execute_command_action(&action, &context).unwrap();
624
625 assert!(result.contains("/path/to/file.rs"));
626 }
627
628 #[test]
629 fn test_execute_command_action_without_capture() {
630 let executor = DefaultHookExecutor::new();
631 let action = CommandAction {
632 command: "echo".to_string(),
633 args: vec!["test".to_string()],
634 timeout_ms: None,
635 capture_output: false,
636 };
637 let context = create_test_context();
638
639 let result = executor.execute_command_action(&action, &context).unwrap();
640
641 assert!(result.contains("executed successfully"));
642 }
643
644 #[test]
645 fn test_execute_command_action_missing_variable() {
646 let executor = DefaultHookExecutor::new();
647 let action = CommandAction {
648 command: "echo".to_string(),
649 args: vec!["File: {{missing_var}}".to_string()],
650 timeout_ms: None,
651 capture_output: true,
652 };
653 let context = create_test_context();
654
655 let result = executor.execute_command_action(&action, &context);
656
657 assert!(result.is_err());
658 }
659
660 #[test]
661 fn test_execute_command_action_nonexistent_command() {
662 let executor = DefaultHookExecutor::new();
663 let action = CommandAction {
664 command: "nonexistent_command_xyz_123".to_string(),
665 args: vec![],
666 timeout_ms: None,
667 capture_output: true,
668 };
669 let context = create_test_context();
670
671 let result = executor.execute_command_action(&action, &context);
672
673 assert!(result.is_err());
674 }
675
676 #[test]
677 fn test_execute_action_tool_call_nonexistent_tool() {
678 let executor = DefaultHookExecutor::new();
679 let mut hook = create_test_hook("hook1", true);
680 hook.action = Action::ToolCall(crate::types::ToolCallAction {
681 tool_name: "test_tool".to_string(),
682 tool_path: "/nonexistent/path/to/tool".to_string(),
683 parameters: crate::types::ParameterBindings {
684 bindings: std::collections::HashMap::new(),
685 },
686 timeout_ms: None,
687 });
688 let context = create_test_context();
689
690 let result = executor.execute_action(&hook, &context);
691
692 assert!(result.is_err());
693 }
694
695 #[test]
696 fn test_execute_tool_call_action_with_literal_parameters() {
697 let executor = DefaultHookExecutor::new();
698 let mut params = std::collections::HashMap::new();
699 params.insert(
700 "message".to_string(),
701 crate::types::ParameterValue::Literal(serde_json::json!("hello")),
702 );
703
704 let action = crate::types::ToolCallAction {
705 tool_name: "echo_tool".to_string(),
706 tool_path: "echo".to_string(),
707 parameters: crate::types::ParameterBindings { bindings: params },
708 timeout_ms: None,
709 };
710 let context = create_test_context();
711
712 let result = executor
713 .execute_tool_call_action(&action, &context)
714 .unwrap();
715
716 assert!(!result.is_empty());
717 }
718
719 #[test]
720 fn test_execute_tool_call_action_with_variable_parameters() {
721 let executor = DefaultHookExecutor::new();
722 let mut params = std::collections::HashMap::new();
723 params.insert(
724 "file".to_string(),
725 crate::types::ParameterValue::Variable("file_path".to_string()),
726 );
727
728 let action = crate::types::ToolCallAction {
729 tool_name: "echo_tool".to_string(),
730 tool_path: "echo".to_string(),
731 parameters: crate::types::ParameterBindings { bindings: params },
732 timeout_ms: None,
733 };
734 let mut context = create_test_context();
735 context.data = serde_json::json!({
736 "file_path": "/path/to/file.rs"
737 });
738
739 let result = executor
740 .execute_tool_call_action(&action, &context)
741 .unwrap();
742
743 assert!(!result.is_empty());
746 }
747
748 #[test]
749 fn test_execute_tool_call_action_missing_variable() {
750 let executor = DefaultHookExecutor::new();
751 let mut params = std::collections::HashMap::new();
752 params.insert(
753 "file".to_string(),
754 crate::types::ParameterValue::Variable("missing_var".to_string()),
755 );
756
757 let action = crate::types::ToolCallAction {
758 tool_name: "echo_tool".to_string(),
759 tool_path: "echo".to_string(),
760 parameters: crate::types::ParameterBindings { bindings: params },
761 timeout_ms: None,
762 };
763 let context = create_test_context();
764
765 let result = executor.execute_tool_call_action(&action, &context);
766
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_execute_action_ai_prompt_success() {
772 let executor = DefaultHookExecutor::new();
773 let mut hook = create_test_hook("hook1", true);
774 hook.action = Action::AiPrompt(crate::types::AiPromptAction {
775 prompt_template: "Test prompt".to_string(),
776 variables: std::collections::HashMap::new(),
777 model: None,
778 temperature: None,
779 max_tokens: None,
780 stream: false,
781 });
782 let context = create_test_context();
783
784 let result = executor.execute_action(&hook, &context);
785
786 assert!(result.is_ok());
787 }
788
789 #[test]
790 fn test_execute_ai_prompt_action_with_variables() {
791 let executor = DefaultHookExecutor::new();
792 let mut variables = std::collections::HashMap::new();
793 variables.insert("file".to_string(), "file_path".to_string());
794
795 let action = crate::types::AiPromptAction {
796 prompt_template: "Format the file: {{file_path}}".to_string(),
797 variables,
798 model: Some("gpt-4".to_string()),
799 temperature: Some(0.7),
800 max_tokens: Some(2000),
801 stream: true,
802 };
803 let mut context = create_test_context();
804 context.data = serde_json::json!({
805 "file_path": "/path/to/file.rs"
806 });
807
808 let result = executor
809 .execute_ai_prompt_action(&action, &context)
810 .unwrap();
811
812 assert!(result.contains("executed"));
813 }
814
815 #[test]
816 fn test_execute_ai_prompt_action_missing_variable() {
817 let executor = DefaultHookExecutor::new();
818 let mut variables = std::collections::HashMap::new();
819 variables.insert("file".to_string(), "missing_var".to_string());
820
821 let action = crate::types::AiPromptAction {
822 prompt_template: "Format the file: {{file}}".to_string(),
823 variables,
824 model: None,
825 temperature: None,
826 max_tokens: None,
827 stream: false,
828 };
829 let context = create_test_context();
830
831 let result = executor.execute_ai_prompt_action(&action, &context);
832
833 assert!(result.is_err());
834 }
835
836 #[test]
837 fn test_execute_ai_prompt_action_with_model_config() {
838 let executor = DefaultHookExecutor::new();
839 let action = crate::types::AiPromptAction {
840 prompt_template: "Analyze this code".to_string(),
841 variables: std::collections::HashMap::new(),
842 model: Some("gpt-4".to_string()),
843 temperature: Some(0.5),
844 max_tokens: Some(1000),
845 stream: false,
846 };
847 let context = create_test_context();
848
849 let result = executor
850 .execute_ai_prompt_action(&action, &context)
851 .unwrap();
852
853 assert!(!result.is_empty());
854 }
855
856 #[test]
857 fn test_execute_action_chain() {
858 let executor = DefaultHookExecutor::new();
859 let mut hook = create_test_hook("hook1", true);
860 hook.action = Action::Chain(crate::types::ChainAction {
861 hook_ids: vec!["hook2".to_string(), "hook3".to_string()],
862 pass_output: false,
863 });
864 let context = create_test_context();
865
866 let result = executor.execute_action(&hook, &context).unwrap();
867
868 assert!(result.contains("hook2"));
869 assert!(result.contains("hook3"));
870 }
871
872 #[test]
873 fn test_execute_hook_with_condition_met() {
874 let executor = DefaultHookExecutor::new();
875 let mut hook = create_test_hook("hook1", true);
876 hook.condition = Some(Condition {
877 expression: "file_path.ends_with('.rs')".to_string(),
878 context_keys: vec!["file_path".to_string()],
879 });
880 let mut context = create_test_context();
881 context.data = serde_json::json!({
882 "file_path": "/path/to/file.rs",
883 });
884
885 let result = executor.execute_hook(&hook, &context).unwrap();
886
887 assert_eq!(result.status, HookStatus::Success);
888 }
889
890 #[test]
891 fn test_execute_hook_with_condition_not_met() {
892 let executor = DefaultHookExecutor::new();
893 let mut hook = create_test_hook("hook1", true);
894 hook.condition = Some(Condition {
895 expression: "file_path.ends_with('.rs')".to_string(),
896 context_keys: vec!["file_path".to_string()],
897 });
898 let mut context = create_test_context();
899 context.data = serde_json::json!({
900 "file_path": "/path/to/file.txt",
901 });
902
903 let result = executor.execute_hook(&hook, &context).unwrap();
904
905 assert_eq!(result.status, HookStatus::Success);
908 }
909
910 #[test]
911 fn test_execute_hook_with_invalid_condition() {
912 let executor = DefaultHookExecutor::new();
913 let mut hook = create_test_hook("hook1", true);
914 hook.condition = Some(Condition {
915 expression: "missing_key == 'value'".to_string(),
916 context_keys: vec!["missing_key".to_string()],
917 });
918 let context = create_test_context();
919
920 let result = executor.execute_hook(&hook, &context).unwrap();
921
922 assert_eq!(result.status, HookStatus::Skipped);
923 assert!(result.error.is_some());
924 }
925
926 #[test]
927 fn test_hook_result_captures_output() {
928 let executor = DefaultHookExecutor::new();
929 let hook = create_test_hook("hook1", true);
930 let context = create_test_context();
931
932 let result = executor.execute_hook(&hook, &context).unwrap();
933
934 assert_eq!(result.hook_id, "hook1");
935 assert_eq!(result.status, HookStatus::Success);
936 assert!(result.output.is_some());
937 assert!(result.error.is_none());
938 let _ = result.duration_ms; }
940
941 #[test]
942 fn test_hook_result_captures_error() {
943 let executor = DefaultHookExecutor::new();
944 let mut hook = create_test_hook("hook1", true);
945 hook.action = Action::Command(CommandAction {
946 command: "nonexistent_command_xyz".to_string(),
947 args: vec![],
948 timeout_ms: None,
949 capture_output: true,
950 });
951 let context = create_test_context();
952
953 let result = executor.execute_hook(&hook, &context).unwrap();
954
955 assert_eq!(result.hook_id, "hook1");
956 assert_eq!(result.status, HookStatus::Failed);
957 assert!(result.output.is_none());
958 assert!(result.error.is_some());
959 let _ = result.duration_ms; }
961
962 #[test]
963 fn test_hook_result_tracks_duration() {
964 let executor = DefaultHookExecutor::new();
965 let hook = create_test_hook("hook1", true);
966 let context = create_test_context();
967
968 let result = executor.execute_hook(&hook, &context).unwrap();
969
970 let _ = result.duration_ms;
972 }
973
974 #[test]
975 fn test_hook_result_skipped_status() {
976 let executor = DefaultHookExecutor::new();
977 let hook = create_test_hook("hook1", false);
978 let context = create_test_context();
979
980 let result = executor.execute_hook(&hook, &context).unwrap();
981
982 assert_eq!(result.status, HookStatus::Skipped);
983 assert!(result.error.is_some());
984 }
985
986 #[test]
987 fn test_hook_result_with_condition_skipped() {
988 let executor = DefaultHookExecutor::new();
989 let mut hook = create_test_hook("hook1", true);
990 hook.condition = Some(Condition {
991 expression: "file_path.ends_with('.rs')".to_string(),
992 context_keys: vec!["file_path".to_string()],
993 });
994 let mut context = create_test_context();
995 context.data = serde_json::json!({
996 "file_path": "/path/to/file.txt"
997 });
998
999 let result = executor.execute_hook(&hook, &context).unwrap();
1000
1001 let _ = result.duration_ms;
1004 }
1005}