@@ -297,6 +297,337 @@ describe('GitHubProvider', () => {
297297
298298 expect ( result ) . toBe ( false )
299299 } )
300+
301+ it ( 'handles deletion exception gracefully' , async ( ) => {
302+ mockGitDeleteRemoteBranch . mockRejectedValue ( new Error ( 'Branch not found' ) )
303+
304+ const provider = new GitHubProvider ( )
305+ const result = await provider . deleteBranch ( 'nonexistent-branch' )
306+
307+ expect ( result ) . toBe ( false )
308+ } )
309+ } )
310+
311+ describe ( 'updatePr' , ( ) => {
312+ it ( 'updates PR by merging base into head' , async ( ) => {
313+ mockOctokit . repos . merge . mockResolvedValue ( { } )
314+ mockOctokit . pulls . get . mockResolvedValue ( {
315+ data : { mergeable_state : 'clean' } ,
316+ } )
317+
318+ const provider = new GitHubProvider ( )
319+ await provider . updatePr ( {
320+ owner : 'owner' ,
321+ repo : 'repo' ,
322+ prNumber : 123 ,
323+ head : 'feature-branch' ,
324+ base : 'main' ,
325+ } )
326+
327+ expect ( mockOctokit . repos . merge ) . toHaveBeenCalledWith ( {
328+ owner : 'owner' ,
329+ repo : 'repo' ,
330+ head : 'main' ,
331+ base : 'feature-branch' ,
332+ } )
333+ } )
334+
335+ it ( 'adds conflict comment when PR becomes dirty' , async ( ) => {
336+ mockOctokit . repos . merge . mockResolvedValue ( { } )
337+ mockOctokit . pulls . get . mockResolvedValue ( {
338+ data : { mergeable_state : 'dirty' } ,
339+ } )
340+ mockOctokit . issues . createComment . mockResolvedValue ( { } )
341+
342+ const provider = new GitHubProvider ( )
343+ await provider . updatePr ( {
344+ owner : 'owner' ,
345+ repo : 'repo' ,
346+ prNumber : 456 ,
347+ head : 'feature-branch' ,
348+ base : 'main' ,
349+ } )
350+
351+ expect ( mockOctokit . issues . createComment ) . toHaveBeenCalledWith (
352+ expect . objectContaining ( {
353+ issue_number : 456 ,
354+ body : expect . stringContaining ( 'merge conflicts' ) ,
355+ } ) ,
356+ )
357+ } )
358+
359+ it ( 'throws when merge fails' , async ( ) => {
360+ mockWithGitHubRetry . mockResolvedValueOnce ( {
361+ ok : false ,
362+ message : 'Merge failed' ,
363+ cause : 'Conflict' ,
364+ } )
365+
366+ const provider = new GitHubProvider ( )
367+ await expect (
368+ provider . updatePr ( {
369+ owner : 'owner' ,
370+ repo : 'repo' ,
371+ prNumber : 789 ,
372+ head : 'feature' ,
373+ base : 'main' ,
374+ } ) ,
375+ ) . rejects . toThrow ( )
376+ } )
377+
378+ it ( 'throws when PR details fetch fails' , async ( ) => {
379+ mockWithGitHubRetry
380+ . mockResolvedValueOnce ( { ok : true , data : { } } )
381+ . mockResolvedValueOnce ( {
382+ ok : false ,
383+ message : 'PR not found' ,
384+ } )
385+
386+ const provider = new GitHubProvider ( )
387+ await expect (
388+ provider . updatePr ( {
389+ owner : 'owner' ,
390+ repo : 'repo' ,
391+ prNumber : 999 ,
392+ head : 'feature' ,
393+ base : 'main' ,
394+ } ) ,
395+ ) . rejects . toThrow ( )
396+ } )
397+ } )
398+
399+ describe ( 'createPr error handling' , ( ) => {
400+ it ( 'throws when API call fails' , async ( ) => {
401+ mockWithGitHubRetry . mockResolvedValueOnce ( {
402+ ok : false ,
403+ message : 'Failed to create PR' ,
404+ cause : 'Repository not found' ,
405+ } )
406+
407+ const provider = new GitHubProvider ( )
408+ await expect (
409+ provider . createPr ( {
410+ owner : 'owner' ,
411+ repo : 'nonexistent' ,
412+ title : 'Test' ,
413+ head : 'feature' ,
414+ base : 'main' ,
415+ body : 'Body' ,
416+ } ) ,
417+ ) . rejects . toThrow ( 'Repository not found' )
418+ } )
419+
420+ it ( 'handles closed PR state' , async ( ) => {
421+ mockOctokit . pulls . create . mockResolvedValue ( {
422+ data : {
423+ number : 789 ,
424+ html_url : 'https://github.com/owner/repo/pull/789' ,
425+ state : 'closed' ,
426+ merged_at : null ,
427+ } ,
428+ } )
429+
430+ const provider = new GitHubProvider ( )
431+ const result = await provider . createPr ( {
432+ owner : 'owner' ,
433+ repo : 'repo' ,
434+ title : 'Test' ,
435+ head : 'feature' ,
436+ base : 'main' ,
437+ body : 'Body' ,
438+ } )
439+
440+ expect ( result . state ) . toBe ( 'closed' )
441+ } )
442+ } )
443+
444+ describe ( 'addComment error handling' , ( ) => {
445+ it ( 'throws when comment fails' , async ( ) => {
446+ mockWithGitHubRetry . mockResolvedValueOnce ( {
447+ ok : false ,
448+ message : 'Comment failed' ,
449+ } )
450+
451+ const provider = new GitHubProvider ( )
452+ await expect (
453+ provider . addComment ( {
454+ owner : 'owner' ,
455+ repo : 'repo' ,
456+ prNumber : 123 ,
457+ body : 'Test' ,
458+ } ) ,
459+ ) . rejects . toThrow ( 'Comment failed' )
460+ } )
461+ } )
462+
463+ describe ( 'listPrs advanced scenarios' , ( ) => {
464+ it ( 'filters PRs by author' , async ( ) => {
465+ const mockResponse = {
466+ repository : {
467+ pullRequests : {
468+ pageInfo : { hasNextPage : false , endCursor : null } ,
469+ nodes : [
470+ {
471+ number : 1 ,
472+ title : 'PR by user1' ,
473+ author : { login : 'user1' } ,
474+ headRefName : 'f1' ,
475+ baseRefName : 'main' ,
476+ state : 'OPEN' ,
477+ mergeStateStatus : 'CLEAN' ,
478+ } ,
479+ {
480+ number : 2 ,
481+ title : 'PR by user2' ,
482+ author : { login : 'user2' } ,
483+ headRefName : 'f2' ,
484+ baseRefName : 'main' ,
485+ state : 'OPEN' ,
486+ mergeStateStatus : 'CLEAN' ,
487+ } ,
488+ ] ,
489+ } ,
490+ } ,
491+ }
492+
493+ mockCacheFetch . mockResolvedValue ( mockResponse )
494+
495+ const provider = new GitHubProvider ( )
496+ const results = await provider . listPrs ( {
497+ owner : 'owner' ,
498+ repo : 'repo' ,
499+ author : 'user1' ,
500+ } )
501+
502+ expect ( results ) . toHaveLength ( 1 )
503+ expect ( results [ 0 ] ! . author ) . toBe ( 'user1' )
504+ } )
505+
506+ it ( 'handles PRs without author' , async ( ) => {
507+ const mockResponse = {
508+ repository : {
509+ pullRequests : {
510+ pageInfo : { hasNextPage : false , endCursor : null } ,
511+ nodes : [
512+ {
513+ number : 1 ,
514+ title : 'PR without author' ,
515+ // No author field.
516+ headRefName : 'f1' ,
517+ baseRefName : 'main' ,
518+ state : 'OPEN' ,
519+ mergeStateStatus : 'CLEAN' ,
520+ } ,
521+ ] ,
522+ } ,
523+ } ,
524+ }
525+
526+ mockCacheFetch . mockResolvedValue ( mockResponse )
527+
528+ const provider = new GitHubProvider ( )
529+ const results = await provider . listPrs ( {
530+ owner : 'owner' ,
531+ repo : 'repo' ,
532+ } )
533+
534+ expect ( results ) . toHaveLength ( 1 )
535+ expect ( results [ 0 ] ! . author ) . toBe ( '<unknown>' )
536+ } )
537+
538+ it ( 'handles specific states filter' , async ( ) => {
539+ const mockResponse = {
540+ repository : {
541+ pullRequests : {
542+ pageInfo : { hasNextPage : false , endCursor : null } ,
543+ nodes : [
544+ {
545+ number : 1 ,
546+ title : 'Open PR' ,
547+ author : { login : 'user' } ,
548+ headRefName : 'f1' ,
549+ baseRefName : 'main' ,
550+ state : 'OPEN' ,
551+ mergeStateStatus : 'CLEAN' ,
552+ } ,
553+ ] ,
554+ } ,
555+ } ,
556+ }
557+
558+ mockCacheFetch . mockResolvedValue ( mockResponse )
559+
560+ const provider = new GitHubProvider ( )
561+ const results = await provider . listPrs ( {
562+ owner : 'owner' ,
563+ repo : 'repo' ,
564+ states : 'open' ,
565+ } )
566+
567+ expect ( results ) . toHaveLength ( 1 )
568+ } )
569+
570+ it ( 'exits early when ghsaId provided and matches found' , async ( ) => {
571+ const mockResponse = {
572+ repository : {
573+ pullRequests : {
574+ pageInfo : { hasNextPage : true , endCursor : 'cursor' } ,
575+ nodes : [
576+ {
577+ number : 1 ,
578+ title : 'Fix GHSA-1234' ,
579+ author : { login : 'user' } ,
580+ headRefName : 'socket/fix/GHSA-1234' ,
581+ baseRefName : 'main' ,
582+ state : 'OPEN' ,
583+ mergeStateStatus : 'CLEAN' ,
584+ } ,
585+ ] ,
586+ } ,
587+ } ,
588+ }
589+
590+ mockCacheFetch . mockResolvedValue ( mockResponse )
591+
592+ const provider = new GitHubProvider ( )
593+ const results = await provider . listPrs ( {
594+ owner : 'owner' ,
595+ repo : 'repo' ,
596+ ghsaId : 'GHSA-1234' ,
597+ } )
598+
599+ // Should have exited early after first page due to ghsaId optimization.
600+ expect ( mockCacheFetch ) . toHaveBeenCalledTimes ( 1 )
601+ expect ( results ) . toHaveLength ( 1 )
602+ } )
603+
604+ it ( 'handles empty repository response' , async ( ) => {
605+ mockCacheFetch . mockResolvedValue ( {
606+ repository : {
607+ pullRequests : undefined ,
608+ } ,
609+ } )
610+
611+ const provider = new GitHubProvider ( )
612+ const results = await provider . listPrs ( {
613+ owner : 'owner' ,
614+ repo : 'repo' ,
615+ } )
616+
617+ expect ( results ) . toHaveLength ( 0 )
618+ } )
619+
620+ it ( 'handles GraphQL error gracefully' , async ( ) => {
621+ mockCacheFetch . mockRejectedValue ( new Error ( 'GraphQL error' ) )
622+
623+ const provider = new GitHubProvider ( )
624+ const results = await provider . listPrs ( {
625+ owner : 'owner' ,
626+ repo : 'repo' ,
627+ } )
628+
629+ expect ( results ) . toHaveLength ( 0 )
630+ } )
300631 } )
301632
302633 describe ( 'metadata' , ( ) => {
0 commit comments