@@ -634,6 +634,149 @@ describe("preflightDiscordMessage", () => {
634634 ) . toBe ( "default" ) ;
635635 } ) ;
636636
637+ it ( "passes bot-loop protection facts for accepted bot-authored Discord messages (#58789)" , async ( ) => {
638+ const channelId = "channel-bot-loop" ;
639+ const guildId = "guild-bot-loop" ;
640+ const senderBotId = "relay-bot-1" ;
641+ const messageTimestamp = "2026-05-13T05:00:00.000Z" ;
642+
643+ const message = createDiscordMessage ( {
644+ id : "m-loop-1" ,
645+ channelId,
646+ content : "chatter <@openclaw-bot>" ,
647+ mentionedUsers : [ { id : "openclaw-bot" } ] ,
648+ author : { id : senderBotId , bot : true , username : "Relay" } ,
649+ timestamp : messageTimestamp ,
650+ } ) ;
651+ const result = await preflightDiscordMessage (
652+ createPreflightArgs ( {
653+ cfg : DEFAULT_PREFLIGHT_CFG ,
654+ discordConfig : {
655+ allowBots : true ,
656+ botLoopProtection : {
657+ enabled : true ,
658+ maxEventsPerWindow : 3 ,
659+ cooldownSeconds : 60 ,
660+ } ,
661+ } as DiscordConfig ,
662+ data : createGuildEvent ( {
663+ channelId,
664+ guildId,
665+ author : message . author ,
666+ message,
667+ } ) ,
668+ client : createGuildTextClient ( channelId ) ,
669+ } ) ,
670+ ) ;
671+
672+ expect ( expectPreflightResult ( result ) . botLoopProtection ) . toEqual ( {
673+ scopeId : "default" ,
674+ conversationId : channelId ,
675+ senderId : senderBotId ,
676+ receiverId : "openclaw-bot" ,
677+ config : {
678+ enabled : true ,
679+ maxEventsPerWindow : 3 ,
680+ cooldownSeconds : 60 ,
681+ } ,
682+ defaultsConfig : undefined ,
683+ defaultEnabled : true ,
684+ nowMs : Date . parse ( messageTimestamp ) ,
685+ } ) ;
686+ } ) ;
687+
688+ it ( "passes generic channel defaults for Discord bot loop budgets" , async ( ) => {
689+ const channelId = "channel-bot-loop-defaults" ;
690+ const guildId = "guild-bot-loop-defaults" ;
691+ const discordConfig = { allowBots : true } as DiscordConfig ;
692+ const message = createDiscordMessage ( {
693+ id : "m-loop-default-1" ,
694+ channelId,
695+ content : "relay <@openclaw-bot>" ,
696+ mentionedUsers : [ { id : "openclaw-bot" } ] ,
697+ author : { id : "relay-bot-defaults" , bot : true , username : "Relay" } ,
698+ } ) ;
699+ const result = await runGuildPreflight ( {
700+ channelId,
701+ guildId,
702+ message,
703+ discordConfig,
704+ cfg : {
705+ ...DEFAULT_PREFLIGHT_CFG ,
706+ channels : {
707+ defaults : {
708+ botLoopProtection : {
709+ maxEventsPerWindow : 1 ,
710+ cooldownSeconds : 60 ,
711+ } ,
712+ } ,
713+ } ,
714+ } ,
715+ } ) ;
716+
717+ expect ( expectPreflightResult ( result ) . botLoopProtection ?. defaultsConfig ) . toEqual ( {
718+ maxEventsPerWindow : 1 ,
719+ cooldownSeconds : 60 ,
720+ } ) ;
721+ } ) ;
722+
723+ it ( "does not prepare loop-guard facts for bot messages that later preflight gates drop (#58789)" , async ( ) => {
724+ const channelId = "channel-bot-loop-dropped" ;
725+ const guildId = "guild-bot-loop-dropped" ;
726+ const senderBotId = "relay-bot-dropped" ;
727+ const discordConfig = {
728+ allowBots : true ,
729+ botLoopProtection : {
730+ enabled : true ,
731+ maxEventsPerWindow : 1 ,
732+ cooldownSeconds : 60 ,
733+ } ,
734+ } as DiscordConfig ;
735+ const guildEntries = {
736+ [ guildId ] : {
737+ requireMention : false ,
738+ ignoreOtherMentions : true ,
739+ } ,
740+ } ;
741+
742+ for ( const messageId of [ "m-dropped-1" , "m-dropped-2" ] ) {
743+ const message = createDiscordMessage ( {
744+ id : messageId ,
745+ channelId,
746+ content : `cc <@999> ${ messageId } ` ,
747+ mentionedUsers : [ { id : "999" } ] ,
748+ author : { id : senderBotId , bot : true , username : "Relay" } ,
749+ } ) ;
750+
751+ expect (
752+ await runGuildPreflight ( {
753+ channelId,
754+ guildId,
755+ message,
756+ discordConfig,
757+ guildEntries,
758+ } ) ,
759+ ) . toBeNull ( ) ;
760+ }
761+
762+ const validMessage = createDiscordMessage ( {
763+ id : "m-valid-after-dropped" ,
764+ channelId,
765+ content : "legitimate bot relay" ,
766+ author : { id : senderBotId , bot : true , username : "Relay" } ,
767+ } ) ;
768+
769+ expect (
770+ await runGuildPreflight ( {
771+ channelId,
772+ guildId,
773+ message : validMessage ,
774+ discordConfig,
775+ guildEntries,
776+ } ) ,
777+ ) . not . toBeNull ( ) ;
778+ } ) ;
779+
637780 it ( "keeps bound-thread regular bot messages flowing when allowBots=true" , async ( ) => {
638781 const threadBinding = createThreadBinding ( {
639782 targetKind : "session" ,
0 commit comments