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,12 @@ vi.mock('@/lib/webhooks/utils.server', () => ({
2935vi . mock ( '@/lib/webhooks/pending-verification' , ( ) => ( {
3036 PendingWebhookVerificationTracker : vi . fn ( ) ,
3137} ) )
38+ const { mockIsDeploymentVersionProtected } = vi . hoisted ( ( ) => ( {
39+ mockIsDeploymentVersionProtected : vi . fn ( ) ,
40+ } ) )
41+ vi . mock ( '@/lib/workflows/persistence/deployment-operations' , ( ) => ( {
42+ isDeploymentVersionProtectedByCurrentOperation : mockIsDeploymentVersionProtected ,
43+ } ) )
3244
3345const {
3446 mockGetSlackBotCredential,
@@ -52,9 +64,11 @@ vi.mock('@/lib/webhooks/providers/slack', () => ({
5264
5365import {
5466 buildProviderConfig ,
67+ cleanupInactiveDeploymentWebhooks ,
5568 resolveTriggerCredentialId ,
5669 resolveWebhookConfigForBlock ,
5770} from '@/lib/webhooks/deploy'
71+ import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
5872import { getBlock } from '@/blocks'
5973import { getTrigger } from '@/triggers'
6074
@@ -639,3 +653,95 @@ describe('resolveWebhookConfigForBlock — TikTok routing', () => {
639653 expect ( result ?. error . message ) . toContain ( 'Reconnect' )
640654 } )
641655} )
656+
657+ describe ( 'cleanupInactiveDeploymentWebhooks' , ( ) => {
658+ const workflow = { id : 'workflow-1' , userId : 'user-1' , workspaceId : 'workspace-1' }
659+ const input = {
660+ workflowId : 'workflow-1' ,
661+ workflow,
662+ requestId : 'request-1' ,
663+ protectedDeploymentVersionId : null ,
664+ limit : 5 ,
665+ }
666+
667+ function staleWebhookRow ( id : string ) {
668+ return {
669+ id,
670+ workflowId : 'workflow-1' ,
671+ deploymentVersionId : 'version-1' ,
672+ provider : 'github' ,
673+ providerConfig : { } ,
674+ archivedAt : null ,
675+ createdAt : new Date ( '2026-07-14T08:00:00.000Z' ) ,
676+ }
677+ }
678+
679+ beforeEach ( ( ) => {
680+ mockIsDeploymentVersionProtected . mockResolvedValue ( false )
681+ } )
682+
683+ it ( 'retires one bounded batch of stale rows and reports the remainder' , async ( ) => {
684+ queueTableRows ( webhook , [
685+ staleWebhookRow ( 'wh-1' ) ,
686+ staleWebhookRow ( 'wh-2' ) ,
687+ staleWebhookRow ( 'wh-3' ) ,
688+ ] )
689+ queueTableRows ( workflowDeploymentVersion , [ { id : 'version-1' } ] )
690+ queueTableRows ( workflowDeploymentVersion , [ { id : 'version-1' } ] )
691+
692+ await expect ( cleanupInactiveDeploymentWebhooks ( { ...input , limit : 2 } ) ) . resolves . toEqual ( {
693+ hasMore : true ,
694+ } )
695+
696+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . toHaveBeenCalledTimes ( 2 )
697+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . toHaveBeenCalledWith (
698+ expect . objectContaining ( { id : 'wh-1' } ) ,
699+ workflow ,
700+ 'request-1' ,
701+ { throwOnError : true }
702+ )
703+ expect ( dbChainMockFns . delete ) . toHaveBeenCalledTimes ( 2 )
704+ } )
705+
706+ it ( 'reports completion once the batch drains every stale row' , async ( ) => {
707+ queueTableRows ( webhook , [ staleWebhookRow ( 'wh-1' ) ] )
708+ queueTableRows ( workflowDeploymentVersion , [ { id : 'version-1' } ] )
709+
710+ await expect ( cleanupInactiveDeploymentWebhooks ( input ) ) . resolves . toEqual ( { hasMore : false } )
711+
712+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . toHaveBeenCalledTimes ( 1 )
713+ expect ( dbChainMockFns . delete ) . toHaveBeenCalledTimes ( 1 )
714+ } )
715+
716+ it ( 'excludes the version the current operation is preparing from the batch' , async ( ) => {
717+ queueTableRows ( webhook , [ ] )
718+
719+ await expect (
720+ cleanupInactiveDeploymentWebhooks ( { ...input , protectedDeploymentVersionId : 'version-3' } )
721+ ) . resolves . toEqual ( { hasMore : false } )
722+
723+ expect ( ne ) . toHaveBeenCalledWith ( webhook . deploymentVersionId , 'version-3' )
724+ } )
725+
726+ it ( 'stops before any provider call once the fence reports a change' , async ( ) => {
727+ queueTableRows ( webhook , [ staleWebhookRow ( 'wh-1' ) ] )
728+
729+ await expect (
730+ cleanupInactiveDeploymentWebhooks ( { ...input , shouldContinue : async ( ) => false } )
731+ ) . resolves . toEqual ( { hasMore : true } )
732+
733+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . not . toHaveBeenCalled ( )
734+ expect ( dbChainMockFns . delete ) . not . toHaveBeenCalled ( )
735+ } )
736+
737+ it ( 'leaves a row alone when its version became the current candidate mid-batch' , async ( ) => {
738+ queueTableRows ( webhook , [ staleWebhookRow ( 'wh-1' ) ] )
739+ mockIsDeploymentVersionProtected . mockResolvedValue ( true )
740+
741+ await expect ( cleanupInactiveDeploymentWebhooks ( input ) ) . resolves . toEqual ( { hasMore : true } )
742+
743+ expect ( mockIsDeploymentVersionProtected ) . toHaveBeenCalledWith ( 'workflow-1' , 'version-1' )
744+ expect ( vi . mocked ( cleanupExternalWebhook ) ) . not . toHaveBeenCalled ( )
745+ expect ( dbChainMockFns . delete ) . not . toHaveBeenCalled ( )
746+ } )
747+ } )
0 commit comments