@@ -594,6 +594,311 @@ describe("add_comment", () => {
594594 // Should NOT be threaded since item_number was explicitly provided
595595 expect ( capturedReplyToId ) . toBeUndefined ( ) ;
596596 } ) ;
597+
598+ it ( "should use reply_to_id from message when not triggered by discussion_comment event" , async ( ) => {
599+ const addCommentScript = fs . readFileSync ( path . join ( __dirname , "add_comment.cjs" ) , "utf8" ) ;
600+
601+ // workflow_dispatch trigger (not discussion_comment); item_number refers to a discussion
602+ mockContext . eventName = "workflow_dispatch" ;
603+ mockContext . payload = { } ;
604+
605+ // Make REST API return 404 so the code falls back to the discussion path
606+ mockGithub . rest . issues . createComment = async ( ) => {
607+ const err = new Error ( "Not Found" ) ;
608+ // @ts -expect-error - Simulating GitHub REST API error
609+ err . status = 404 ;
610+ throw err ;
611+ } ;
612+
613+ let capturedReplyToId = undefined ;
614+ mockGithub . graphql = async ( query , variables ) => {
615+ if ( query . includes ( "addDiscussionComment" ) ) {
616+ capturedReplyToId = variables ?. replyToId ;
617+ return {
618+ addDiscussionComment : {
619+ comment : {
620+ id : "DC_kwDOTest456" ,
621+ body : variables ?. body ,
622+ createdAt : "2024-01-01" ,
623+ url : "https://github.com/owner/repo/discussions/10#discussioncomment-456" ,
624+ } ,
625+ } ,
626+ } ;
627+ }
628+ // Mock replyTo resolution: the provided reply_to_id is already top-level (no parent)
629+ if ( query . includes ( "replyTo" ) ) {
630+ return { node : { replyTo : null } } ;
631+ }
632+ return {
633+ repository : {
634+ discussion : {
635+ id : "D_kwDOTest123" ,
636+ url : "https://github.com/owner/repo/discussions/10" ,
637+ } ,
638+ } ,
639+ } ;
640+ } ;
641+
642+ const handler = await eval ( `(async () => { ${ addCommentScript } ; return await main({}); })()` ) ;
643+
644+ const message = {
645+ type : "add_comment" ,
646+ item_number : 10 ,
647+ body : "🔄 Updated finding..." ,
648+ reply_to_id : "DC_kwDOParentComment456" ,
649+ } ;
650+
651+ const result = await handler ( message , { } ) ;
652+
653+ expect ( result . success ) . toBe ( true ) ;
654+ expect ( result . isDiscussion ) . toBe ( true ) ;
655+ // Should use the reply_to_id from the message
656+ expect ( capturedReplyToId ) . toBe ( "DC_kwDOParentComment456" ) ;
657+ } ) ;
658+
659+ it ( "should resolve top-level parent when reply_to_id points to a nested reply" , async ( ) => {
660+ const addCommentScript = fs . readFileSync ( path . join ( __dirname , "add_comment.cjs" ) , "utf8" ) ;
661+
662+ // workflow_dispatch trigger (not discussion_comment); item_number refers to a discussion
663+ mockContext . eventName = "workflow_dispatch" ;
664+ mockContext . payload = { } ;
665+
666+ // Make REST API return 404 so the code falls back to the discussion path
667+ mockGithub . rest . issues . createComment = async ( ) => {
668+ const err = new Error ( "Not Found" ) ;
669+ // @ts -expect-error - Simulating GitHub REST API error
670+ err . status = 404 ;
671+ throw err ;
672+ } ;
673+
674+ let capturedReplyToId = undefined ;
675+ mockGithub . graphql = async ( query , variables ) => {
676+ if ( query . includes ( "addDiscussionComment" ) ) {
677+ capturedReplyToId = variables ?. replyToId ;
678+ return {
679+ addDiscussionComment : {
680+ comment : {
681+ id : "DC_kwDOTest456" ,
682+ body : variables ?. body ,
683+ createdAt : "2024-01-01" ,
684+ url : "https://github.com/owner/repo/discussions/10#discussioncomment-456" ,
685+ } ,
686+ } ,
687+ } ;
688+ }
689+ // Mock replyTo resolution: the provided reply_to_id is itself a reply, return its parent
690+ if ( query . includes ( "replyTo" ) ) {
691+ return { node : { replyTo : { id : "DC_kwDOTopLevelComment123" } } } ;
692+ }
693+ return {
694+ repository : {
695+ discussion : {
696+ id : "D_kwDOTest123" ,
697+ url : "https://github.com/owner/repo/discussions/10" ,
698+ } ,
699+ } ,
700+ } ;
701+ } ;
702+
703+ const handler = await eval ( `(async () => { ${ addCommentScript } ; return await main({}); })()` ) ;
704+
705+ const message = {
706+ type : "add_comment" ,
707+ item_number : 10 ,
708+ body : "🔄 Updated finding..." ,
709+ reply_to_id : "DC_kwDONestedReply789" ,
710+ } ;
711+
712+ const result = await handler ( message , { } ) ;
713+
714+ expect ( result . success ) . toBe ( true ) ;
715+ expect ( result . isDiscussion ) . toBe ( true ) ;
716+ // Should resolve to the top-level parent, not the nested reply
717+ expect ( capturedReplyToId ) . toBe ( "DC_kwDOTopLevelComment123" ) ;
718+ } ) ;
719+
720+ it ( "should ignore reply_to_id when triggered by discussion_comment event without explicit item_number" , async ( ) => {
721+ const addCommentScript = fs . readFileSync ( path . join ( __dirname , "add_comment.cjs" ) , "utf8" ) ;
722+
723+ // discussion_comment trigger takes precedence over reply_to_id
724+ mockContext . eventName = "discussion_comment" ;
725+ mockContext . payload = {
726+ discussion : {
727+ number : 10 ,
728+ } ,
729+ comment : {
730+ node_id : "DC_kwDOTriggeringComment123" ,
731+ } ,
732+ } ;
733+
734+ let capturedReplyToId = undefined ;
735+ mockGithub . graphql = async ( query , variables ) => {
736+ if ( query . includes ( "addDiscussionComment" ) ) {
737+ capturedReplyToId = variables ?. replyToId ;
738+ return {
739+ addDiscussionComment : {
740+ comment : {
741+ id : "DC_kwDOTest456" ,
742+ body : variables ?. body ,
743+ createdAt : "2024-01-01" ,
744+ url : "https://github.com/owner/repo/discussions/10#discussioncomment-456" ,
745+ } ,
746+ } ,
747+ } ;
748+ }
749+ // Mock replyTo resolution: triggering comment is top-level (no parent)
750+ if ( query . includes ( "replyTo" ) ) {
751+ return { node : { replyTo : null } } ;
752+ }
753+ return {
754+ repository : {
755+ discussion : {
756+ id : "D_kwDOTest123" ,
757+ url : "https://github.com/owner/repo/discussions/10" ,
758+ } ,
759+ } ,
760+ } ;
761+ } ;
762+
763+ const handler = await eval ( `(async () => { ${ addCommentScript } ; return await main({}); })()` ) ;
764+
765+ const message = {
766+ type : "add_comment" ,
767+ body : "Reply that provides reply_to_id but should use triggering comment instead" ,
768+ reply_to_id : "DC_kwDOShouldBeIgnored999" ,
769+ } ;
770+
771+ const result = await handler ( message , { } ) ;
772+
773+ expect ( result . success ) . toBe ( true ) ;
774+ expect ( result . isDiscussion ) . toBe ( true ) ;
775+ // Should use the triggering comment node_id, not the reply_to_id from the message
776+ expect ( capturedReplyToId ) . toBe ( "DC_kwDOTriggeringComment123" ) ;
777+ } ) ;
778+
779+ it ( "should ignore and warn when reply_to_id is a whitespace-only string" , async ( ) => {
780+ const addCommentScript = fs . readFileSync ( path . join ( __dirname , "add_comment.cjs" ) , "utf8" ) ;
781+
782+ mockContext . eventName = "workflow_dispatch" ;
783+ mockContext . payload = { } ;
784+
785+ // Capture warning calls
786+ const warningCalls = [ ] ;
787+ mockCore . warning = msg => {
788+ warningCalls . push ( msg ) ;
789+ } ;
790+
791+ // Make REST API return 404 so the code falls back to the discussion path
792+ mockGithub . rest . issues . createComment = async ( ) => {
793+ const err = new Error ( "Not Found" ) ;
794+ // @ts -expect-error - Simulating GitHub REST API error
795+ err . status = 404 ;
796+ throw err ;
797+ } ;
798+
799+ let capturedReplyToId = undefined ;
800+ mockGithub . graphql = async ( query , variables ) => {
801+ if ( query . includes ( "addDiscussionComment" ) ) {
802+ capturedReplyToId = variables ?. replyToId ;
803+ return {
804+ addDiscussionComment : {
805+ comment : {
806+ id : "DC_kwDOTest456" ,
807+ body : variables ?. body ,
808+ createdAt : "2024-01-01" ,
809+ url : "https://github.com/owner/repo/discussions/10#discussioncomment-456" ,
810+ } ,
811+ } ,
812+ } ;
813+ }
814+ return {
815+ repository : {
816+ discussion : {
817+ id : "D_kwDOTest123" ,
818+ url : "https://github.com/owner/repo/discussions/10" ,
819+ } ,
820+ } ,
821+ } ;
822+ } ;
823+
824+ const handler = await eval ( `(async () => { ${ addCommentScript } ; return await main({}); })()` ) ;
825+
826+ const message = {
827+ type : "add_comment" ,
828+ item_number : 10 ,
829+ body : "Comment with blank reply_to_id" ,
830+ reply_to_id : " " ,
831+ } ;
832+
833+ const result = await handler ( message , { } ) ;
834+
835+ expect ( result . success ) . toBe ( true ) ;
836+ expect ( result . isDiscussion ) . toBe ( true ) ;
837+ // Whitespace-only reply_to_id should be ignored (post top-level)
838+ expect ( capturedReplyToId ) . toBeUndefined ( ) ;
839+ expect ( warningCalls ) . toContain ( "Ignoring empty discussion reply_to_id after normalization" ) ;
840+ } ) ;
841+
842+ it ( "should coerce numeric reply_to_id to string before use" , async ( ) => {
843+ const addCommentScript = fs . readFileSync ( path . join ( __dirname , "add_comment.cjs" ) , "utf8" ) ;
844+
845+ mockContext . eventName = "workflow_dispatch" ;
846+ mockContext . payload = { } ;
847+
848+ // Make REST API return 404 so the code falls back to the discussion path
849+ mockGithub . rest . issues . createComment = async ( ) => {
850+ const err = new Error ( "Not Found" ) ;
851+ // @ts -expect-error - Simulating GitHub REST API error
852+ err . status = 404 ;
853+ throw err ;
854+ } ;
855+
856+ let capturedReplyToId = undefined ;
857+ mockGithub . graphql = async ( query , variables ) => {
858+ if ( query . includes ( "addDiscussionComment" ) ) {
859+ capturedReplyToId = variables ?. replyToId ;
860+ return {
861+ addDiscussionComment : {
862+ comment : {
863+ id : "DC_kwDOTest456" ,
864+ body : variables ?. body ,
865+ createdAt : "2024-01-01" ,
866+ url : "https://github.com/owner/repo/discussions/10#discussioncomment-456" ,
867+ } ,
868+ } ,
869+ } ;
870+ }
871+ // Mock replyTo resolution: node ID is top-level (no parent)
872+ if ( query . includes ( "replyTo" ) ) {
873+ return { node : { replyTo : null } } ;
874+ }
875+ return {
876+ repository : {
877+ discussion : {
878+ id : "D_kwDOTest123" ,
879+ url : "https://github.com/owner/repo/discussions/10" ,
880+ } ,
881+ } ,
882+ } ;
883+ } ;
884+
885+ const handler = await eval ( `(async () => { ${ addCommentScript } ; return await main({}); })()` ) ;
886+
887+ const message = {
888+ type : "add_comment" ,
889+ item_number : 10 ,
890+ body : "Comment with numeric reply_to_id" ,
891+ // @ts -expect-error - intentionally passing a number to test coercion
892+ reply_to_id : 12345 ,
893+ } ;
894+
895+ const result = await handler ( message , { } ) ;
896+
897+ expect ( result . success ) . toBe ( true ) ;
898+ expect ( result . isDiscussion ) . toBe ( true ) ;
899+ // Numeric reply_to_id should be coerced to "12345"
900+ expect ( capturedReplyToId ) . toBe ( "12345" ) ;
901+ } ) ;
597902 } ) ;
598903
599904 describe ( "regression test for wrong PR bug" , ( ) => {
0 commit comments