Skip to content

Commit e85d962

Browse files
test: add coverage for new event types, commands, and processor stubs
discord-types: - types.rs: roundtrip tests for DiscordRole, VoiceState, ModalInput; verify optional fields are omitted from JSON - events.rs: roundtrip tests for all 15 new event types (TypingStart, VoiceStateUpdate, Guild/Channel/Role lifecycle, PresenceUpdate, ModalSubmit, Autocomplete, BotReady); verify optional guild_id and old_channel_id are omitted when None - commands.rs: roundtrip tests for all 18 new command types; verify optional fields omitted (reason, color, category_id, message_id, topic) discord-agent: - processor_tests.rs: 13 tests verifying lifecycle stub methods return Ok without side effects (typing, voice, guild/channel/role, presence, ready) - 2 NATS integration tests: autocomplete responds with empty choices, modal submit responds ephemeral with "Received." Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 58410d4 commit e85d962

4 files changed

Lines changed: 847 additions & 0 deletions

File tree

rsworkspace/crates/discord-agent/src/processor_tests.rs

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)