@@ -484,6 +484,270 @@ mod tests {
484484 ) ;
485485 }
486486
487+ // ── lifecycle event stubs (no NATS needed) ────────────────────────────
488+
489+ fn make_typing_event ( session_id : & str , user_id : u64 , channel_id : u64 ) -> discord_types:: events:: TypingStartEvent {
490+ use discord_types:: events:: { EventMetadata , TypingStartEvent } ;
491+ TypingStartEvent { metadata : EventMetadata :: new ( session_id, 1 ) , user_id, channel_id, guild_id : None }
492+ }
493+
494+ fn make_voice_event ( session_id : & str , user_id : u64 ) -> discord_types:: events:: VoiceStateUpdateEvent {
495+ use discord_types:: events:: { EventMetadata , VoiceStateUpdateEvent } ;
496+ use discord_types:: types:: VoiceState ;
497+ VoiceStateUpdateEvent {
498+ metadata : EventMetadata :: new ( session_id, 1 ) ,
499+ guild_id : Some ( 200 ) ,
500+ old_channel_id : None ,
501+ new_state : VoiceState { user_id, channel_id : Some ( 500 ) , guild_id : Some ( 200 ) , self_mute : false , self_deaf : false } ,
502+ }
503+ }
504+
505+ fn make_guild_create_event ( session_id : & str ) -> discord_types:: events:: GuildCreateEvent {
506+ use discord_types:: events:: { EventMetadata , GuildCreateEvent } ;
507+ use discord_types:: types:: DiscordGuild ;
508+ GuildCreateEvent { metadata : EventMetadata :: new ( session_id, 1 ) , guild : DiscordGuild { id : 200 , name : "Test" . to_string ( ) } , member_count : 10 }
509+ }
510+
511+ fn make_guild_update_event ( session_id : & str ) -> discord_types:: events:: GuildUpdateEvent {
512+ use discord_types:: events:: { EventMetadata , GuildUpdateEvent } ;
513+ use discord_types:: types:: DiscordGuild ;
514+ GuildUpdateEvent { metadata : EventMetadata :: new ( session_id, 1 ) , guild : DiscordGuild { id : 200 , name : "Updated" . to_string ( ) } }
515+ }
516+
517+ fn make_guild_delete_event ( session_id : & str ) -> discord_types:: events:: GuildDeleteEvent {
518+ use discord_types:: events:: { EventMetadata , GuildDeleteEvent } ;
519+ GuildDeleteEvent { metadata : EventMetadata :: new ( session_id, 1 ) , guild_id : 200 , unavailable : false }
520+ }
521+
522+ fn make_channel_create_event ( session_id : & str ) -> discord_types:: events:: ChannelCreateEvent {
523+ use discord_types:: events:: { EventMetadata , ChannelCreateEvent } ;
524+ use discord_types:: types:: { ChannelType , DiscordChannel } ;
525+ ChannelCreateEvent { metadata : EventMetadata :: new ( session_id, 1 ) , channel : DiscordChannel { id : 300 , channel_type : ChannelType :: GuildText , guild_id : Some ( 200 ) , name : Some ( "general" . to_string ( ) ) } }
526+ }
527+
528+ fn make_channel_update_event ( session_id : & str ) -> discord_types:: events:: ChannelUpdateEvent {
529+ use discord_types:: events:: { EventMetadata , ChannelUpdateEvent } ;
530+ use discord_types:: types:: { ChannelType , DiscordChannel } ;
531+ ChannelUpdateEvent { metadata : EventMetadata :: new ( session_id, 1 ) , channel : DiscordChannel { id : 300 , channel_type : ChannelType :: GuildText , guild_id : Some ( 200 ) , name : Some ( "renamed" . to_string ( ) ) } }
532+ }
533+
534+ fn make_channel_delete_event ( session_id : & str ) -> discord_types:: events:: ChannelDeleteEvent {
535+ use discord_types:: events:: { EventMetadata , ChannelDeleteEvent } ;
536+ ChannelDeleteEvent { metadata : EventMetadata :: new ( session_id, 1 ) , channel_id : 300 , guild_id : 200 }
537+ }
538+
539+ fn make_role_create_event ( session_id : & str ) -> discord_types:: events:: RoleCreateEvent {
540+ use discord_types:: events:: { EventMetadata , RoleCreateEvent } ;
541+ use discord_types:: types:: DiscordRole ;
542+ RoleCreateEvent { metadata : EventMetadata :: new ( session_id, 1 ) , guild_id : 200 , role : DiscordRole { id : 111 , name : "Mod" . to_string ( ) , color : 0 , hoist : false , position : 1 , permissions : "0" . to_string ( ) , mentionable : false } }
543+ }
544+
545+ fn make_role_update_event ( session_id : & str ) -> discord_types:: events:: RoleUpdateEvent {
546+ use discord_types:: events:: { EventMetadata , RoleUpdateEvent } ;
547+ use discord_types:: types:: DiscordRole ;
548+ RoleUpdateEvent { metadata : EventMetadata :: new ( session_id, 1 ) , guild_id : 200 , role : DiscordRole { id : 111 , name : "Admin" . to_string ( ) , color : 0xFF0000 , hoist : true , position : 1 , permissions : "8" . to_string ( ) , mentionable : true } }
549+ }
550+
551+ fn make_role_delete_event ( session_id : & str ) -> discord_types:: events:: RoleDeleteEvent {
552+ use discord_types:: events:: { EventMetadata , RoleDeleteEvent } ;
553+ RoleDeleteEvent { metadata : EventMetadata :: new ( session_id, 1 ) , guild_id : 200 , role_id : 111 }
554+ }
555+
556+ fn make_presence_event ( session_id : & str ) -> discord_types:: events:: PresenceUpdateEvent {
557+ use discord_types:: events:: { EventMetadata , PresenceUpdateEvent } ;
558+ PresenceUpdateEvent { metadata : EventMetadata :: new ( session_id, 1 ) , user_id : 42 , guild_id : 200 , status : "online" . to_string ( ) }
559+ }
560+
561+ fn make_bot_ready_event ( session_id : & str ) -> discord_types:: events:: BotReadyEvent {
562+ use discord_types:: events:: { EventMetadata , BotReadyEvent } ;
563+ use discord_types:: types:: DiscordUser ;
564+ BotReadyEvent { metadata : EventMetadata :: new ( session_id, 1 ) , bot_user : DiscordUser { id : 999 , username : "bot" . to_string ( ) , global_name : None , bot : true } , guild_count : 3 }
565+ }
566+
567+ fn make_autocomplete_event ( session_id : & str , interaction_id : u64 ) -> discord_types:: events:: AutocompleteEvent {
568+ use discord_types:: events:: { AutocompleteEvent , EventMetadata } ;
569+ use discord_types:: types:: DiscordUser ;
570+ AutocompleteEvent {
571+ metadata : EventMetadata :: new ( session_id, 1 ) ,
572+ interaction_id,
573+ interaction_token : "ac-tok" . to_string ( ) ,
574+ guild_id : None ,
575+ channel_id : 100 ,
576+ user : DiscordUser { id : 42 , username : "tester" . to_string ( ) , global_name : None , bot : false } ,
577+ command_name : "ask" . to_string ( ) ,
578+ focused_option : "question" . to_string ( ) ,
579+ current_value : "how" . to_string ( ) ,
580+ }
581+ }
582+
583+ fn make_modal_submit_event ( session_id : & str , interaction_id : u64 ) -> discord_types:: events:: ModalSubmitEvent {
584+ use discord_types:: events:: { EventMetadata , ModalSubmitEvent } ;
585+ use discord_types:: types:: DiscordUser ;
586+ ModalSubmitEvent {
587+ metadata : EventMetadata :: new ( session_id, 1 ) ,
588+ interaction_id,
589+ interaction_token : "modal-tok" . to_string ( ) ,
590+ guild_id : None ,
591+ channel_id : 100 ,
592+ user : DiscordUser { id : 42 , username : "tester" . to_string ( ) , global_name : None , bot : false } ,
593+ custom_id : "my_modal" . to_string ( ) ,
594+ inputs : vec ! [ ] ,
595+ }
596+ }
597+
598+ #[ tokio:: test]
599+ async fn test_typing_start_is_noop ( ) {
600+ let processor = MessageProcessor :: default ( ) ;
601+ let result = processor. process_typing_start ( & make_typing_event ( "dc-dm-100" , 42 , 100 ) ) . await ;
602+ assert ! ( result. is_ok( ) ) ;
603+ }
604+
605+ #[ tokio:: test]
606+ async fn test_voice_state_update_is_noop ( ) {
607+ let processor = MessageProcessor :: default ( ) ;
608+ let result = processor. process_voice_state_update ( & make_voice_event ( "dc-guild-200-500" , 42 ) ) . await ;
609+ assert ! ( result. is_ok( ) ) ;
610+ }
611+
612+ #[ tokio:: test]
613+ async fn test_guild_create_is_noop ( ) {
614+ let processor = MessageProcessor :: default ( ) ;
615+ let result = processor. process_guild_create ( & make_guild_create_event ( "dc-guild-200-100" ) ) . await ;
616+ assert ! ( result. is_ok( ) ) ;
617+ }
618+
619+ #[ tokio:: test]
620+ async fn test_guild_update_is_noop ( ) {
621+ let processor = MessageProcessor :: default ( ) ;
622+ let result = processor. process_guild_update ( & make_guild_update_event ( "dc-guild-200-100" ) ) . await ;
623+ assert ! ( result. is_ok( ) ) ;
624+ }
625+
626+ #[ tokio:: test]
627+ async fn test_guild_delete_is_noop ( ) {
628+ let processor = MessageProcessor :: default ( ) ;
629+ let result = processor. process_guild_delete ( & make_guild_delete_event ( "dc-guild-200-100" ) ) . await ;
630+ assert ! ( result. is_ok( ) ) ;
631+ }
632+
633+ #[ tokio:: test]
634+ async fn test_channel_create_is_noop ( ) {
635+ let processor = MessageProcessor :: default ( ) ;
636+ let result = processor. process_channel_create ( & make_channel_create_event ( "dc-guild-200-300" ) ) . await ;
637+ assert ! ( result. is_ok( ) ) ;
638+ }
639+
640+ #[ tokio:: test]
641+ async fn test_channel_update_is_noop ( ) {
642+ let processor = MessageProcessor :: default ( ) ;
643+ let result = processor. process_channel_update ( & make_channel_update_event ( "dc-guild-200-300" ) ) . await ;
644+ assert ! ( result. is_ok( ) ) ;
645+ }
646+
647+ #[ tokio:: test]
648+ async fn test_channel_delete_is_noop ( ) {
649+ let processor = MessageProcessor :: default ( ) ;
650+ let result = processor. process_channel_delete ( & make_channel_delete_event ( "dc-guild-200-300" ) ) . await ;
651+ assert ! ( result. is_ok( ) ) ;
652+ }
653+
654+ #[ tokio:: test]
655+ async fn test_role_create_is_noop ( ) {
656+ let processor = MessageProcessor :: default ( ) ;
657+ let result = processor. process_role_create ( & make_role_create_event ( "dc-guild-200-100" ) ) . await ;
658+ assert ! ( result. is_ok( ) ) ;
659+ }
660+
661+ #[ tokio:: test]
662+ async fn test_role_update_is_noop ( ) {
663+ let processor = MessageProcessor :: default ( ) ;
664+ let result = processor. process_role_update ( & make_role_update_event ( "dc-guild-200-100" ) ) . await ;
665+ assert ! ( result. is_ok( ) ) ;
666+ }
667+
668+ #[ tokio:: test]
669+ async fn test_role_delete_is_noop ( ) {
670+ let processor = MessageProcessor :: default ( ) ;
671+ let result = processor. process_role_delete ( & make_role_delete_event ( "dc-guild-200-100" ) ) . await ;
672+ assert ! ( result. is_ok( ) ) ;
673+ }
674+
675+ #[ tokio:: test]
676+ async fn test_presence_update_is_noop ( ) {
677+ let processor = MessageProcessor :: default ( ) ;
678+ let result = processor. process_presence_update ( & make_presence_event ( "dc-guild-200-100" ) ) . await ;
679+ assert ! ( result. is_ok( ) ) ;
680+ }
681+
682+ #[ tokio:: test]
683+ async fn test_bot_ready_is_noop ( ) {
684+ let processor = MessageProcessor :: default ( ) ;
685+ let result = processor. process_bot_ready ( & make_bot_ready_event ( "dc-guild-200-100" ) ) . await ;
686+ assert ! ( result. is_ok( ) ) ;
687+ }
688+
689+ // ── autocomplete & modal (publish to NATS) ─────────────────────────────
690+
691+ #[ tokio:: test]
692+ async fn test_autocomplete_responds_with_empty_choices ( ) {
693+ let Some ( client) = try_connect ( ) . await else {
694+ eprintln ! ( "SKIP: NATS not available at {}" , NATS_URL ) ;
695+ return ;
696+ } ;
697+
698+ let prefix = format ! ( "test-proc-ac-{}" , uuid:: Uuid :: new_v4( ) . simple( ) ) ;
699+ let publisher = discord_nats:: MessagePublisher :: new ( client. clone ( ) , prefix. clone ( ) ) ;
700+ let subscriber = discord_nats:: MessageSubscriber :: new ( client. clone ( ) , prefix. clone ( ) ) ;
701+
702+ let subject = discord_nats:: subjects:: agent:: interaction_autocomplete_respond ( & prefix) ;
703+ let mut stream = subscriber
704+ . subscribe :: < discord_types:: AutocompleteRespondCommand > ( & subject)
705+ . await
706+ . unwrap ( ) ;
707+
708+ let processor = MessageProcessor :: default ( ) ;
709+ let event = make_autocomplete_event ( "dc-dm-100" , 6666 ) ;
710+ processor. process_autocomplete ( & event, & publisher) . await . unwrap ( ) ;
711+
712+ let cmd = stream. next ( ) . await . unwrap ( ) . unwrap ( ) ;
713+ assert_eq ! ( cmd. interaction_id, 6666 ) ;
714+ assert_eq ! ( cmd. interaction_token, "ac-tok" ) ;
715+ assert ! ( cmd. choices. is_empty( ) , "default autocomplete must return empty choices" ) ;
716+ }
717+
718+ #[ tokio:: test]
719+ async fn test_modal_submit_responds_ephemeral ( ) {
720+ let Some ( client) = try_connect ( ) . await else {
721+ eprintln ! ( "SKIP: NATS not available at {}" , NATS_URL ) ;
722+ return ;
723+ } ;
724+
725+ let prefix = format ! ( "test-proc-modal-{}" , uuid:: Uuid :: new_v4( ) . simple( ) ) ;
726+ let publisher = discord_nats:: MessagePublisher :: new ( client. clone ( ) , prefix. clone ( ) ) ;
727+ let subscriber = discord_nats:: MessageSubscriber :: new ( client. clone ( ) , prefix. clone ( ) ) ;
728+
729+ let subject = discord_nats:: subjects:: agent:: interaction_respond ( & prefix) ;
730+ let mut stream = subscriber
731+ . subscribe :: < discord_types:: InteractionRespondCommand > ( & subject)
732+ . await
733+ . unwrap ( ) ;
734+
735+ let processor = MessageProcessor :: default ( ) ;
736+ let event = make_modal_submit_event ( "dc-dm-100" , 7777 ) ;
737+ processor. process_modal_submit ( & event, & publisher) . await . unwrap ( ) ;
738+
739+ let cmd = stream. next ( ) . await . unwrap ( ) . unwrap ( ) ;
740+ assert_eq ! ( cmd. interaction_id, 7777 ) ;
741+ assert_eq ! ( cmd. interaction_token, "modal-tok" ) ;
742+ assert ! ( cmd. ephemeral, "modal ack must be ephemeral" ) ;
743+ let content = cmd. content . unwrap_or_default ( ) ;
744+ assert ! (
745+ content. contains( "Received" ) ,
746+ "modal ack content must say Received, got: {}" ,
747+ content
748+ ) ;
749+ }
750+
487751 #[ tokio:: test]
488752 async fn test_unknown_slash_command_responds_with_help_hint ( ) {
489753 let Some ( client) = try_connect ( ) . await else {
0 commit comments