@@ -807,4 +807,202 @@ describe("issues command", () => {
807807
808808 mockExit . mockRestore ( ) ;
809809 } ) ;
810+
811+ describe ( "--false-positives flag" , ( ) => {
812+ it ( "should pass onlyPotentialFalsePositives: true in the body" , async ( ) => {
813+ vi . mocked ( AnalysisService . searchRepositoryIssues ) . mockResolvedValue ( {
814+ data : [ ] ,
815+ } as any ) ;
816+
817+ const program = createProgram ( ) ;
818+ await program . parseAsync ( [
819+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
820+ "--false-positives" ,
821+ ] ) ;
822+
823+ expect ( AnalysisService . searchRepositoryIssues ) . toHaveBeenCalledWith (
824+ "gh" , "test-org" , "test-repo" , undefined , 100 ,
825+ { onlyPotentialFalsePositives : true } ,
826+ ) ;
827+ } ) ;
828+
829+ it ( "should combine onlyPotentialFalsePositives with other filters (--patterns)" , async ( ) => {
830+ vi . mocked ( AnalysisService . searchRepositoryIssues ) . mockResolvedValue ( {
831+ data : [ ] ,
832+ } as any ) ;
833+
834+ const program = createProgram ( ) ;
835+ await program . parseAsync ( [
836+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
837+ "--false-positives" ,
838+ "--patterns" , "no-undef,sql-injection" ,
839+ "--branch" , "main" ,
840+ ] ) ;
841+
842+ expect ( AnalysisService . searchRepositoryIssues ) . toHaveBeenCalledWith (
843+ "gh" , "test-org" , "test-repo" , undefined , 100 ,
844+ {
845+ onlyPotentialFalsePositives : true ,
846+ patternIds : [ "no-undef" , "sql-injection" ] ,
847+ branchName : "main" ,
848+ } ,
849+ ) ;
850+ } ) ;
851+
852+ it ( "should display false positive issues in list format" , async ( ) => {
853+ const fpIssue = {
854+ ...mockIssues [ 0 ] ,
855+ falsePositiveProbability : 0.9 ,
856+ falsePositiveThreshold : 0.5 ,
857+ falsePositiveReason : "Common safe pattern" ,
858+ } ;
859+ vi . mocked ( AnalysisService . searchRepositoryIssues ) . mockResolvedValue ( {
860+ data : [ fpIssue ] ,
861+ } as any ) ;
862+
863+ const program = createProgram ( ) ;
864+ await program . parseAsync ( [
865+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
866+ "--false-positives" ,
867+ ] ) ;
868+
869+ const output = getAllOutput ( ) ;
870+ expect ( output ) . toContain ( "Potential SQL injection vulnerability" ) ;
871+ expect ( output ) . toContain ( "Potential false positive" ) ;
872+ } ) ;
873+ } ) ;
874+
875+ describe ( "--bulk-ignore flag" , ( ) => {
876+ it ( "should fetch all FP issues with onlyPotentialFalsePositives: true and call bulkIgnoreIssues" , async ( ) => {
877+ vi . mocked ( AnalysisService . searchRepositoryIssues ) . mockResolvedValue ( {
878+ data : mockIssues ,
879+ } as any ) ;
880+ vi . mocked ( AnalysisService . bulkIgnoreIssues ) . mockResolvedValue ( undefined as any ) ;
881+
882+ const program = createProgram ( ) ;
883+ await program . parseAsync ( [
884+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
885+ "--bulk-ignore" ,
886+ ] ) ;
887+
888+ expect ( AnalysisService . searchRepositoryIssues ) . toHaveBeenCalledWith (
889+ "gh" , "test-org" , "test-repo" , undefined , 100 ,
890+ { onlyPotentialFalsePositives : true } ,
891+ ) ;
892+ expect ( AnalysisService . bulkIgnoreIssues ) . toHaveBeenCalledWith (
893+ "gh" , "test-org" , "test-repo" ,
894+ {
895+ issueIds : [ mockIssues [ 0 ] . issueId , mockIssues [ 1 ] . issueId ] ,
896+ reason : "FalsePositive" ,
897+ comment : undefined ,
898+ } ,
899+ ) ;
900+ } ) ;
901+
902+ it ( "should show 'No false positive issues found' when API returns empty list" , async ( ) => {
903+ vi . mocked ( AnalysisService . searchRepositoryIssues ) . mockResolvedValue ( {
904+ data : [ ] ,
905+ } as any ) ;
906+
907+ const program = createProgram ( ) ;
908+ await program . parseAsync ( [
909+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
910+ "--bulk-ignore" ,
911+ ] ) ;
912+
913+ expect ( AnalysisService . bulkIgnoreIssues ) . not . toHaveBeenCalled ( ) ;
914+ const output = getAllOutput ( ) ;
915+ expect ( output ) . toContain ( "No false positive issues found" ) ;
916+ } ) ;
917+
918+ it ( "should batch bulkIgnoreIssues calls when there are more than 100 issues" , async ( ) => {
919+ // 150 issues across two pages
920+ const page1 = Array . from ( { length : 100 } , ( _ , i ) => ( {
921+ ...mockIssues [ 0 ] ,
922+ issueId : `fp-${ i } ` ,
923+ resultDataId : i ,
924+ } ) ) ;
925+ const page2 = Array . from ( { length : 50 } , ( _ , i ) => ( {
926+ ...mockIssues [ 0 ] ,
927+ issueId : `fp-${ 100 + i } ` ,
928+ resultDataId : 100 + i ,
929+ } ) ) ;
930+
931+ vi . mocked ( AnalysisService . searchRepositoryIssues )
932+ . mockResolvedValueOnce ( {
933+ data : page1 ,
934+ pagination : { cursor : "cursor-2" , limit : 100 , total : 150 } ,
935+ } as any )
936+ . mockResolvedValueOnce ( {
937+ data : page2 ,
938+ pagination : { cursor : undefined , limit : 100 , total : 150 } ,
939+ } as any ) ;
940+ vi . mocked ( AnalysisService . bulkIgnoreIssues ) . mockResolvedValue ( undefined as any ) ;
941+
942+ const program = createProgram ( ) ;
943+ await program . parseAsync ( [
944+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
945+ "--bulk-ignore" ,
946+ ] ) ;
947+
948+ // Should have made 2 search calls (paginated)
949+ expect ( AnalysisService . searchRepositoryIssues ) . toHaveBeenCalledTimes ( 2 ) ;
950+ // Should have made 2 bulk-ignore calls: one with 100 IDs, one with 50 IDs
951+ expect ( AnalysisService . bulkIgnoreIssues ) . toHaveBeenCalledTimes ( 2 ) ;
952+ expect ( AnalysisService . bulkIgnoreIssues ) . toHaveBeenNthCalledWith (
953+ 1 , "gh" , "test-org" , "test-repo" ,
954+ expect . objectContaining ( { issueIds : expect . arrayContaining ( [ expect . stringMatching ( / ^ f p - / ) ] ) } ) ,
955+ ) ;
956+ const firstCallIds : string [ ] = ( AnalysisService . bulkIgnoreIssues as ReturnType < typeof vi . fn > ) . mock . calls [ 0 ] [ 3 ] . issueIds ;
957+ expect ( firstCallIds ) . toHaveLength ( 100 ) ;
958+ const secondCallIds : string [ ] = ( AnalysisService . bulkIgnoreIssues as ReturnType < typeof vi . fn > ) . mock . calls [ 1 ] [ 3 ] . issueIds ;
959+ expect ( secondCallIds ) . toHaveLength ( 50 ) ;
960+ } ) ;
961+
962+ it ( "should forward --comment to bulkIgnoreIssues" , async ( ) => {
963+ vi . mocked ( AnalysisService . searchRepositoryIssues ) . mockResolvedValue ( {
964+ data : [ mockIssues [ 0 ] ] ,
965+ } as any ) ;
966+ vi . mocked ( AnalysisService . bulkIgnoreIssues ) . mockResolvedValue ( undefined as any ) ;
967+
968+ const program = createProgram ( ) ;
969+ await program . parseAsync ( [
970+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
971+ "--bulk-ignore" ,
972+ "--comment" , "Verified by security team" ,
973+ ] ) ;
974+
975+ expect ( AnalysisService . bulkIgnoreIssues ) . toHaveBeenCalledWith (
976+ "gh" , "test-org" , "test-repo" ,
977+ {
978+ issueIds : [ mockIssues [ 0 ] . issueId ] ,
979+ reason : "FalsePositive" ,
980+ comment : "Verified by security team" ,
981+ } ,
982+ ) ;
983+ } ) ;
984+
985+ it ( "should combine --bulk-ignore with other filters (--branch, --patterns)" , async ( ) => {
986+ vi . mocked ( AnalysisService . searchRepositoryIssues ) . mockResolvedValue ( {
987+ data : [ ] ,
988+ } as any ) ;
989+
990+ const program = createProgram ( ) ;
991+ await program . parseAsync ( [
992+ "node" , "test" , "issues" , "gh" , "test-org" , "test-repo" ,
993+ "--bulk-ignore" ,
994+ "--branch" , "develop" ,
995+ "--patterns" , "sql-injection" ,
996+ ] ) ;
997+
998+ expect ( AnalysisService . searchRepositoryIssues ) . toHaveBeenCalledWith (
999+ "gh" , "test-org" , "test-repo" , undefined , 100 ,
1000+ {
1001+ onlyPotentialFalsePositives : true ,
1002+ branchName : "develop" ,
1003+ patternIds : [ "sql-injection" ] ,
1004+ } ,
1005+ ) ;
1006+ } ) ;
1007+ } ) ;
8101008} ) ;
0 commit comments