11/**
22 * @vitest -environment node
33 */
4- import { account , credential } from '@sim/db/schema'
5- import { queueTableRows , resetDbChainMock , resetEnvFlagsMock , setEnvFlags } from '@sim/testing'
6- import { eq } from 'drizzle-orm'
4+ import { account , credential , webhook , workflowDeploymentVersion } from '@sim/db/schema'
5+ import {
6+ dbChainMockFns ,
7+ queueTableRows ,
8+ resetDbChainMock ,
9+ resetEnvFlagsMock ,
10+ setEnvFlags ,
11+ } from '@sim/testing'
12+ import { eq , ne } from 'drizzle-orm'
713import { afterAll , beforeEach , describe , expect , it , type Mock , vi } from 'vitest'
814import type { SubBlockConfig } from '@/blocks/types'
915import type { BlockState } from '@/stores/workflows/workflow/types'
@@ -29,6 +35,14 @@ vi.mock('@/lib/webhooks/utils.server', () => ({
2935vi . mock ( '@/lib/webhooks/pending-verification' , ( ) => ( {
3036 PendingWebhookVerificationTracker : vi . fn ( ) ,
3137} ) )
38+ const { mockIsDeploymentVersionActive, mockIsDeploymentVersionProtected } = vi . hoisted ( ( ) => ( {
39+ mockIsDeploymentVersionActive : vi . fn ( ) ,
40+ mockIsDeploymentVersionProtected : vi . fn ( ) ,
41+ } ) )
42+ vi . mock ( '@/lib/workflows/persistence/deployment-operations' , ( ) => ( {
43+ isDeploymentVersionActive : mockIsDeploymentVersionActive ,
44+ isDeploymentVersionProtectedByCurrentOperation : mockIsDeploymentVersionProtected ,
45+ } ) )
3246
3347const {
3448 mockGetSlackBotCredential,
@@ -52,9 +66,11 @@ vi.mock('@/lib/webhooks/providers/slack', () => ({
5266
5367import {
5468 buildProviderConfig ,
69+ cleanupInactiveDeploymentWebhooks ,
5570 resolveTriggerCredentialId ,
5671 resolveWebhookConfigForBlock ,
5772} from '@/lib/webhooks/deploy'
73+ import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
5874import { getBlock } from '@/blocks'
5975import { getTrigger } from '@/triggers'
6076
@@ -639,3 +655,107 @@ describe('resolveWebhookConfigForBlock — TikTok routing', () => {
639655 expect ( result ?. error . message ) . toContain ( 'Reconnect' )
640656 } )
641657} )
658+
659+ describe ( 'cleanupInactiveDeploymentWebhooks' , ( ) => {
660+ const workflow = { id : 'workflow-1' , userId : 'user-1' , workspaceId : 'workspace-1' }
661+ const input = {
662+ workflowId : 'workflow-1' ,
663+ workflow,
664+ requestId : 'request-1' ,
665+ protectedDeploymentVersionId : null ,
666+ limit : 5 ,
667+ }
668+
669+ function staleWebhookRow ( id : string ) {
670+ return {
671+ id,
672+ workflowId : 'workflow-1' ,
673+ deploymentVersionId : 'version-1' ,
674+ provider : 'github' ,
675+ providerConfig : { } ,
676+ archivedAt : null ,
677+ createdAt : new Date ( '2026-07-14T08:00:00.000Z' ) ,
678+ }
679+ }
680+
681+ beforeEach ( ( ) => {
682+ mockIsDeploymentVersionActive . mockResolvedValue ( false )
683+ mockIsDeploymentVersionProtected . mockResolvedValue ( false )
684+ } )
685+
686+ it ( 'retires one bounded batch of stale rows and reports the remainder' , async ( ) => {
687+ queueTableRows ( webhook , [
688+ staleWebhookRow ( 'wh-1' ) ,
689+ staleWebhookRow ( 'wh-2' ) ,
690+ staleWebhookRow ( 'wh-3' ) ,
691+ ] )
692+ queueTableRows ( workflowDeploymentVersion , [ { id : 'version-1' } ] )
693+ queueTableRows ( workflowDeploymentVersion , [ { id : 'version-1' } ] )
694+
695+ await expect ( cleanupInactiveDeploymentWebhooks ( { ...input , limit : 2 } ) ) . resolves . toEqual ( {
696+ hasMore : true ,
697+ } )
698+
699+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . toHaveBeenCalledTimes ( 2 )
700+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . toHaveBeenCalledWith (
701+ expect . objectContaining ( { id : 'wh-1' } ) ,
702+ workflow ,
703+ 'request-1' ,
704+ { throwOnError : true }
705+ )
706+ expect ( dbChainMockFns . delete ) . toHaveBeenCalledTimes ( 2 )
707+ } )
708+
709+ it ( 'reports completion once the batch drains every stale row' , async ( ) => {
710+ queueTableRows ( webhook , [ staleWebhookRow ( 'wh-1' ) ] )
711+ queueTableRows ( workflowDeploymentVersion , [ { id : 'version-1' } ] )
712+
713+ await expect ( cleanupInactiveDeploymentWebhooks ( input ) ) . resolves . toEqual ( { hasMore : false } )
714+
715+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . toHaveBeenCalledTimes ( 1 )
716+ expect ( dbChainMockFns . delete ) . toHaveBeenCalledTimes ( 1 )
717+ } )
718+
719+ it ( 'excludes the version the current operation is preparing from the batch' , async ( ) => {
720+ queueTableRows ( webhook , [ ] )
721+
722+ await expect (
723+ cleanupInactiveDeploymentWebhooks ( { ...input , protectedDeploymentVersionId : 'version-3' } )
724+ ) . resolves . toEqual ( { hasMore : false } )
725+
726+ expect ( ne ) . toHaveBeenCalledWith ( webhook . deploymentVersionId , 'version-3' )
727+ } )
728+
729+ it ( 'stops before any provider call once the fence reports a change' , async ( ) => {
730+ queueTableRows ( webhook , [ staleWebhookRow ( 'wh-1' ) ] )
731+
732+ await expect (
733+ cleanupInactiveDeploymentWebhooks ( { ...input , shouldContinue : async ( ) => false } )
734+ ) . resolves . toEqual ( { hasMore : true } )
735+
736+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . not . toHaveBeenCalled ( )
737+ expect ( dbChainMockFns . delete ) . not . toHaveBeenCalled ( )
738+ } )
739+
740+ it ( 'leaves a row alone when its version was re-activated after the batch was selected' , async ( ) => {
741+ queueTableRows ( webhook , [ staleWebhookRow ( 'wh-1' ) ] )
742+ mockIsDeploymentVersionActive . mockResolvedValue ( true )
743+
744+ await expect ( cleanupInactiveDeploymentWebhooks ( input ) ) . resolves . toEqual ( { hasMore : true } )
745+
746+ expect ( mockIsDeploymentVersionActive ) . toHaveBeenCalledWith ( 'workflow-1' , 'version-1' )
747+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . not . toHaveBeenCalled ( )
748+ expect ( dbChainMockFns . delete ) . not . toHaveBeenCalled ( )
749+ } )
750+
751+ it ( 'leaves a row alone when its version became the current candidate mid-batch' , async ( ) => {
752+ queueTableRows ( webhook , [ staleWebhookRow ( 'wh-1' ) ] )
753+ mockIsDeploymentVersionProtected . mockResolvedValue ( true )
754+
755+ await expect ( cleanupInactiveDeploymentWebhooks ( input ) ) . resolves . toEqual ( { hasMore : true } )
756+
757+ expect ( mockIsDeploymentVersionProtected ) . toHaveBeenCalledWith ( 'workflow-1' , 'version-1' )
758+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . not . toHaveBeenCalled ( )
759+ expect ( dbChainMockFns . delete ) . not . toHaveBeenCalled ( )
760+ } )
761+ } )
0 commit comments