@@ -273,8 +273,8 @@ describe('processWebhookRequest()', () => {
273273 expect ( opts . jobId ) . toBe ( 'org/my-repo#42@abc123' ) ;
274274 } ) ;
275275
276- it ( 'does not create a second check run when the job already exists ' , async ( ) => {
277- scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' } ) ;
276+ it ( 'does not create a second check run when the job is already active ' , async ( ) => {
277+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' , getState : vi . fn ( ) . mockResolvedValue ( 'waiting' ) } ) ;
278278
279279 const res = await processWebhookRequest ( webhookRequest ( prPayload ( 'opened' ) ) ) ;
280280
@@ -283,6 +283,30 @@ describe('processWebhookRequest()', () => {
283283 expect ( scanQueue . add ) . not . toHaveBeenCalled ( ) ;
284284 } ) ;
285285
286+ it ( 'removes the old job and re-enqueues when the existing job is already completed (re-run scenario)' , async ( ) => {
287+ const mockRemove = vi . fn ( ) . mockResolvedValue ( undefined ) ;
288+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' , getState : vi . fn ( ) . mockResolvedValue ( 'completed' ) , remove : mockRemove } ) ;
289+
290+ const res = await processWebhookRequest ( webhookRequest ( prPayload ( 'opened' ) ) ) ;
291+
292+ expect ( res ) . toEqual ( { status : 200 , body : 'Accepted' } ) ;
293+ expect ( mockRemove ) . toHaveBeenCalled ( ) ;
294+ expect ( createCheckRun ) . toHaveBeenCalled ( ) ;
295+ expect ( scanQueue . add ) . toHaveBeenCalled ( ) ;
296+ } ) ;
297+
298+ it ( 'removes the old job and re-enqueues when the existing job is in a failed state (re-run scenario)' , async ( ) => {
299+ const mockRemove = vi . fn ( ) . mockResolvedValue ( undefined ) ;
300+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' , getState : vi . fn ( ) . mockResolvedValue ( 'failed' ) , remove : mockRemove } ) ;
301+
302+ const res = await processWebhookRequest ( webhookRequest ( prPayload ( 'opened' ) ) ) ;
303+
304+ expect ( res ) . toEqual ( { status : 200 , body : 'Accepted' } ) ;
305+ expect ( mockRemove ) . toHaveBeenCalled ( ) ;
306+ expect ( createCheckRun ) . toHaveBeenCalled ( ) ;
307+ expect ( scanQueue . add ) . toHaveBeenCalled ( ) ;
308+ } ) ;
309+
286310 it ( 'does not create a second check run while another request is accepting the same webhook' , async ( ) => {
287311 redis . set . mockResolvedValueOnce ( null ) ;
288312
@@ -330,7 +354,7 @@ describe('processWebhookRequest()', () => {
330354
331355 it ( 'increments webhooksTotal with deduplicated=true when a duplicate webhook is received' , async ( ) => {
332356 loadScanConfig . mockResolvedValue ( PR_TRIGGER_CONFIG ) ;
333- scanQueue . getJob . mockResolvedValueOnce ( { id : 'existing-job' } ) ;
357+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'existing-job' , getState : vi . fn ( ) . mockResolvedValue ( 'waiting' ) } ) ;
334358 await processWebhookRequest ( webhookRequest ( prPayload ( 'opened' ) ) ) ;
335359 expect ( webhooksTotal . inc ) . toHaveBeenCalledWith ( { action : 'opened' , deduplicated : 'true' } ) ;
336360 } ) ;
@@ -488,8 +512,8 @@ describe('workflow_run trigger — workflow_run event', () => {
488512 expect ( scanQueue . add ) . not . toHaveBeenCalled ( ) ;
489513 } ) ;
490514
491- it ( 'deduplicates: does not enqueue when the job already exists ' , async ( ) => {
492- scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' } ) ;
515+ it ( 'deduplicates: does not enqueue when the job is already active ' , async ( ) => {
516+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' , getState : vi . fn ( ) . mockResolvedValue ( 'active' ) } ) ;
493517
494518 const res = await processWebhookRequest ( webhookRequest ( workflowRunPayload ( ) , { event : 'workflow_run' } ) ) ;
495519
@@ -498,6 +522,18 @@ describe('workflow_run trigger — workflow_run event', () => {
498522 expect ( scanQueue . add ) . not . toHaveBeenCalled ( ) ;
499523 } ) ;
500524
525+ it ( 'removes the old job and re-enqueues when the existing job has already completed (re-run scenario)' , async ( ) => {
526+ const mockRemove = vi . fn ( ) . mockResolvedValue ( undefined ) ;
527+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' , getState : vi . fn ( ) . mockResolvedValue ( 'completed' ) , remove : mockRemove } ) ;
528+
529+ const res = await processWebhookRequest ( webhookRequest ( workflowRunPayload ( ) , { event : 'workflow_run' } ) ) ;
530+
531+ expect ( res ) . toEqual ( { status : 200 , body : 'Accepted' } ) ;
532+ expect ( mockRemove ) . toHaveBeenCalled ( ) ;
533+ expect ( createCheckRun ) . toHaveBeenCalled ( ) ;
534+ expect ( scanQueue . add ) . toHaveBeenCalled ( ) ;
535+ } ) ;
536+
501537 it ( 'returns 200 with "PR not found" when cache is cold and GitHub API returns nothing' , async ( ) => {
502538 redis . get . mockResolvedValue ( null ) ;
503539 findPullRequestBySha . mockResolvedValue ( null ) ;
@@ -703,8 +739,8 @@ describe('workflow_job trigger — workflow_job event', () => {
703739 expect ( scanQueue . add ) . not . toHaveBeenCalled ( ) ;
704740 } ) ;
705741
706- it ( 'deduplicates: does not enqueue when the job already exists ' , async ( ) => {
707- scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' } ) ;
742+ it ( 'deduplicates: does not enqueue when the job is already active ' , async ( ) => {
743+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' , getState : vi . fn ( ) . mockResolvedValue ( 'active' ) } ) ;
708744
709745 const res = await processWebhookRequest ( webhookRequest ( workflowJobPayload ( ) , { event : 'workflow_job' } ) ) ;
710746
@@ -713,6 +749,18 @@ describe('workflow_job trigger — workflow_job event', () => {
713749 expect ( scanQueue . add ) . not . toHaveBeenCalled ( ) ;
714750 } ) ;
715751
752+ it ( 'removes the old job and re-enqueues when the existing job has already completed (re-run scenario)' , async ( ) => {
753+ const mockRemove = vi . fn ( ) . mockResolvedValue ( undefined ) ;
754+ scanQueue . getJob . mockResolvedValueOnce ( { id : 'org/my-repo#42@abc123' , getState : vi . fn ( ) . mockResolvedValue ( 'completed' ) , remove : mockRemove } ) ;
755+
756+ const res = await processWebhookRequest ( webhookRequest ( workflowJobPayload ( ) , { event : 'workflow_job' } ) ) ;
757+
758+ expect ( res ) . toEqual ( { status : 200 , body : 'Accepted' } ) ;
759+ expect ( mockRemove ) . toHaveBeenCalled ( ) ;
760+ expect ( createCheckRun ) . toHaveBeenCalled ( ) ;
761+ expect ( scanQueue . add ) . toHaveBeenCalled ( ) ;
762+ } ) ;
763+
716764 it ( 'returns 200 with "PR not found" when cache is cold and GitHub API returns nothing' , async ( ) => {
717765 redis . get . mockResolvedValue ( null ) ;
718766 findPullRequestBySha . mockResolvedValue ( null ) ;
0 commit comments