@@ -281,3 +281,204 @@ impl HookExecutor {
281281 cache. insert ( hook. name . clone ( ) , hook_output) ;
282282 }
283283}
284+
285+ #[ cfg( test) ]
286+ mod tests {
287+ use std:: str:: from_utf8;
288+ use std:: time:: Duration ;
289+
290+ use tokio:: time:: sleep;
291+
292+ use super :: * ;
293+
294+ #[ test]
295+ fn test_hook_creation ( ) {
296+ let command = "echo 'hello'" ;
297+ let hook = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , command. to_string ( ) ) ;
298+
299+ assert_eq ! ( hook. r#type, HookType :: Inline ) ;
300+ assert ! ( !hook. disabled) ;
301+ assert_eq ! ( hook. timeout_ms, DEFAULT_TIMEOUT_MS ) ;
302+ assert_eq ! ( hook. max_output_size, DEFAULT_MAX_OUTPUT_SIZE ) ;
303+ assert_eq ! ( hook. cache_ttl_seconds, DEFAULT_CACHE_TTL_SECONDS ) ;
304+ assert_eq ! ( hook. command, Some ( command. to_string( ) ) ) ;
305+ assert_eq ! ( hook. trigger, HookTrigger :: PerPrompt ) ;
306+ assert ! ( !hook. is_global) ;
307+ }
308+
309+ #[ tokio:: test]
310+ async fn test_hook_executor_cached_conversation_start ( ) {
311+ let mut executor = HookExecutor :: new ( ) ;
312+ let mut hook1 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo 'test1'" . to_string ( ) ) ;
313+ hook1. is_global = true ;
314+
315+ let mut hook2 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo 'test2'" . to_string ( ) ) ;
316+ hook2. is_global = false ;
317+
318+ // First execution should run the command
319+ let mut output = Vec :: new ( ) ;
320+ let results = executor. run_hooks ( vec ! [ & hook1, & hook2] , & mut output) . await ;
321+
322+ assert_eq ! ( results. len( ) , 2 ) ;
323+ assert ! ( results[ 0 ] . 1 . contains( "test1" ) ) ;
324+ assert ! ( results[ 1 ] . 1 . contains( "test2" ) ) ;
325+ assert ! ( from_utf8( & output) . unwrap( ) . contains( "Running 2 hooks" ) ) ;
326+
327+ // Second execution should use cache
328+ let mut output = Vec :: new ( ) ;
329+ let results = executor. run_hooks ( vec ! [ & hook1, & hook2] , & mut output) . await ;
330+
331+ assert_eq ! ( results. len( ) , 2 ) ;
332+ assert ! ( results[ 0 ] . 1 . contains( "test1" ) ) ;
333+ assert ! ( results[ 1 ] . 1 . contains( "test2" ) ) ;
334+ assert ! ( output. is_empty( ) ) ; // Should not have run the hook, so no output.
335+ }
336+
337+ #[ tokio:: test]
338+ async fn test_hook_executor_cached_per_prompt ( ) {
339+ let mut executor = HookExecutor :: new ( ) ;
340+ let mut hook1 = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "echo 'test1'" . to_string ( ) ) ;
341+ hook1. is_global = true ;
342+ hook1. cache_ttl_seconds = 60 ;
343+
344+ let mut hook2 = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "echo 'test2'" . to_string ( ) ) ;
345+ hook2. is_global = false ;
346+ hook2. cache_ttl_seconds = 60 ;
347+
348+ // First execution should run the command
349+ let mut output = Vec :: new ( ) ;
350+ let results = executor. run_hooks ( vec ! [ & hook1, & hook2] , & mut output) . await ;
351+
352+ assert_eq ! ( results. len( ) , 2 ) ;
353+ assert ! ( results[ 0 ] . 1 . contains( "test1" ) ) ;
354+ assert ! ( results[ 1 ] . 1 . contains( "test2" ) ) ;
355+ assert ! ( from_utf8( & output) . unwrap( ) . contains( "Running 2 hooks" ) ) ;
356+
357+ // Second execution should use cache
358+ let mut output = Vec :: new ( ) ;
359+ let results = executor. run_hooks ( vec ! [ & hook1, & hook2] , & mut output) . await ;
360+
361+ assert_eq ! ( results. len( ) , 2 ) ;
362+ assert ! ( results[ 0 ] . 1 . contains( "test1" ) ) ;
363+ assert ! ( results[ 1 ] . 1 . contains( "test2" ) ) ;
364+ assert ! ( output. is_empty( ) ) ; // Should not have run the hook, so no output.
365+ }
366+
367+ #[ tokio:: test]
368+ async fn test_hook_executor_not_cached_per_prompt ( ) {
369+ let mut executor = HookExecutor :: new ( ) ;
370+ let mut hook1 = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "echo 'test1'" . to_string ( ) ) ;
371+ hook1. is_global = true ;
372+
373+ let mut hook2 = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "echo 'test2'" . to_string ( ) ) ;
374+ hook2. is_global = false ;
375+
376+ // First execution should run the command
377+ let mut output = Vec :: new ( ) ;
378+ let results = executor. run_hooks ( vec ! [ & hook1, & hook2] , & mut output) . await ;
379+
380+ assert_eq ! ( results. len( ) , 2 ) ;
381+ assert ! ( results[ 0 ] . 1 . contains( "test1" ) ) ;
382+ assert ! ( results[ 1 ] . 1 . contains( "test2" ) ) ;
383+ assert ! ( from_utf8( & output) . unwrap( ) . contains( "Running 2 hooks" ) ) ;
384+
385+ // Second execution should use cache
386+ let mut output = Vec :: new ( ) ;
387+ let results = executor. run_hooks ( vec ! [ & hook1, & hook2] , & mut output) . await ;
388+
389+ assert_eq ! ( results. len( ) , 2 ) ;
390+ assert ! ( results[ 0 ] . 1 . contains( "test1" ) ) ;
391+ assert ! ( results[ 1 ] . 1 . contains( "test2" ) ) ;
392+ assert ! ( from_utf8( & output) . unwrap( ) . contains( "Running 2 hooks" ) ) ;
393+ }
394+
395+ #[ tokio:: test]
396+ async fn test_hook_timeout ( ) {
397+ let mut executor = HookExecutor :: new ( ) ;
398+ let mut hook = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "sleep 2" . to_string ( ) ) ;
399+ hook. timeout_ms = 100 ; // Set very short timeout
400+
401+ let mut output = Vec :: new ( ) ;
402+ let results = executor. run_hooks ( vec ! [ & hook] , & mut output) . await ;
403+
404+ assert_eq ! ( results. len( ) , 0 ) ; // Should fail due to timeout
405+ }
406+
407+ #[ tokio:: test]
408+ async fn test_disabled_hook ( ) {
409+ let mut executor = HookExecutor :: new ( ) ;
410+ let mut hook = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "echo 'test'" . to_string ( ) ) ;
411+ hook. disabled = true ;
412+
413+ let mut output = Vec :: new ( ) ;
414+ let results = executor. run_hooks ( vec ! [ & hook] , & mut output) . await ;
415+
416+ assert_eq ! ( results. len( ) , 0 ) ; // Disabled hook should not run
417+ }
418+
419+ #[ tokio:: test]
420+ async fn test_cache_expiration ( ) {
421+ let mut executor = HookExecutor :: new ( ) ;
422+ let mut hook = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "echo 'test'" . to_string ( ) ) ;
423+ hook. cache_ttl_seconds = 1 ;
424+
425+ let mut output = Vec :: new ( ) ;
426+
427+ // First execution
428+ let results1 = executor. run_hooks ( vec ! [ & hook] , & mut output) . await ;
429+ assert_eq ! ( results1. len( ) , 1 ) ;
430+
431+ // Wait for cache to expire
432+ sleep ( Duration :: from_millis ( 1001 ) ) . await ;
433+
434+ // Second execution should run command again
435+ let results2 = executor. run_hooks ( vec ! [ & hook] , & mut output) . await ;
436+ assert_eq ! ( results2. len( ) , 1 ) ;
437+ }
438+
439+ #[ test]
440+ fn test_hook_cache_storage ( ) {
441+ let mut executor: HookExecutor = HookExecutor :: new ( ) ;
442+ let hook = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "" . to_string ( ) ) ;
443+
444+ let cached_hook = CachedHook {
445+ output : "test output" . to_string ( ) ,
446+ expiry : None ,
447+ } ;
448+
449+ executor. insert_cache ( & hook, cached_hook. clone ( ) ) ;
450+
451+ assert_eq ! ( executor. get_cache( & hook) , Some ( "test output" . to_string( ) ) ) ;
452+ }
453+
454+ #[ test]
455+ fn test_hook_cache_storage_expired ( ) {
456+ let mut executor: HookExecutor = HookExecutor :: new ( ) ;
457+ let hook = Hook :: new_inline_hook ( HookTrigger :: PerPrompt , "" . to_string ( ) ) ;
458+
459+ let cached_hook = CachedHook {
460+ output : "test output" . to_string ( ) ,
461+ expiry : Some ( Instant :: now ( ) ) ,
462+ } ;
463+
464+ executor. insert_cache ( & hook, cached_hook. clone ( ) ) ;
465+
466+ // Item should not return since it is expired
467+ assert_eq ! ( executor. get_cache( & hook) , None ) ;
468+ }
469+
470+ #[ tokio:: test]
471+ async fn test_max_output_size ( ) {
472+ let mut executor = HookExecutor :: new ( ) ;
473+ let mut hook = Hook :: new_inline_hook (
474+ HookTrigger :: PerPrompt ,
475+ "for i in {1..1000}; do echo $i; done" . to_string ( ) ,
476+ ) ;
477+ hook. max_output_size = 100 ;
478+
479+ let mut output = Vec :: new ( ) ;
480+ let results = executor. run_hooks ( vec ! [ & hook] , & mut output) . await ;
481+
482+ assert ! ( results[ 0 ] . 1 . len( ) <= hook. max_output_size + " ... truncated" . len( ) ) ;
483+ }
484+ }
0 commit comments