@@ -6,11 +6,14 @@ import { NextRequest } from 'next/server'
66import { beforeEach , describe , expect , it , vi } from 'vitest'
77
88const {
9+ mockActivateWorkflowVersionById,
910 mockCleanupWebhooksForWorkflow,
11+ mockCleanupDeploymentVersion,
1012 mockRecordAudit,
1113 mockDbLimit,
1214 mockDbOrderBy,
1315 mockDbFrom,
16+ mockDbInnerJoin,
1417 mockDbSelect,
1518 mockDbSet,
1619 mockDbUpdate,
@@ -19,18 +22,24 @@ const {
1922 mockDeployWorkflow,
2023 mockLoadWorkflowFromNormalizedTables,
2124 mockRemoveMcpToolsForWorkflow,
25+ mockReactivateWorkflowVersionForRollback,
26+ mockRestorePreviousVersionWebhooks,
2227 mockSaveTriggerWebhooksForDeploy,
2328 mockSyncMcpToolsForWorkflow,
29+ mockDeleteDeploymentVersionById,
2430 mockUndeployWorkflow,
2531 mockValidatePublicApiAllowed,
2632 mockValidateWorkflowAccess,
2733 mockValidateWorkflowPermissions,
2834} = vi . hoisted ( ( ) => ( {
35+ mockActivateWorkflowVersionById : vi . fn ( ) ,
2936 mockCleanupWebhooksForWorkflow : vi . fn ( ) ,
37+ mockCleanupDeploymentVersion : vi . fn ( ) ,
3038 mockRecordAudit : vi . fn ( ) ,
3139 mockDbLimit : vi . fn ( ) ,
3240 mockDbOrderBy : vi . fn ( ) ,
3341 mockDbFrom : vi . fn ( ) ,
42+ mockDbInnerJoin : vi . fn ( ) ,
3443 mockDbSelect : vi . fn ( ) ,
3544 mockDbSet : vi . fn ( ) ,
3645 mockDbUpdate : vi . fn ( ) ,
@@ -39,8 +48,11 @@ const {
3948 mockDeployWorkflow : vi . fn ( ) ,
4049 mockLoadWorkflowFromNormalizedTables : vi . fn ( ) ,
4150 mockRemoveMcpToolsForWorkflow : vi . fn ( ) ,
51+ mockReactivateWorkflowVersionForRollback : vi . fn ( ) ,
52+ mockRestorePreviousVersionWebhooks : vi . fn ( ) ,
4253 mockSaveTriggerWebhooksForDeploy : vi . fn ( ) ,
4354 mockSyncMcpToolsForWorkflow : vi . fn ( ) ,
55+ mockDeleteDeploymentVersionById : vi . fn ( ) ,
4456 mockUndeployWorkflow : vi . fn ( ) ,
4557 mockValidatePublicApiAllowed : vi . fn ( ) ,
4658 mockValidateWorkflowAccess : vi . fn ( ) ,
@@ -65,7 +77,7 @@ vi.mock('@/lib/core/utils/request', () => ({
6577
6678vi . mock ( '@sim/db' , ( ) => ( {
6779 db : { select : mockDbSelect , update : mockDbUpdate } ,
68- workflow : { variables : 'variables' , id : 'id' } ,
80+ workflow : { variables : 'variables' , id : 'id' , deployedAt : 'deployedAt' } ,
6981 workflowDeploymentVersion : {
7082 state : 'state' ,
7183 workflowId : 'workflowId' ,
@@ -88,23 +100,28 @@ vi.mock('drizzle-orm', async (importOriginal) => {
88100vi . mock ( '@/lib/workflows/persistence/utils' , ( ) => ( {
89101 loadWorkflowFromNormalizedTables : ( ...args : unknown [ ] ) =>
90102 mockLoadWorkflowFromNormalizedTables ( ...args ) ,
103+ deleteDeploymentVersionById : ( ...args : unknown [ ] ) => mockDeleteDeploymentVersionById ( ...args ) ,
91104 deployWorkflow : ( ...args : unknown [ ] ) => mockDeployWorkflow ( ...args ) ,
105+ reactivateWorkflowVersionForRollback : ( ...args : unknown [ ] ) =>
106+ mockReactivateWorkflowVersionForRollback ( ...args ) ,
92107 undeployWorkflow : ( ...args : unknown [ ] ) => mockUndeployWorkflow ( ...args ) ,
108+ activateWorkflowVersionById : ( ...args : unknown [ ] ) => mockActivateWorkflowVersionById ( ...args ) ,
93109} ) )
94110
95111vi . mock ( '@/ lib / workflows / comparison ', () => ({
96112 hasWorkflowChanged : vi . fn ( ) . mockReturnValue ( false ) ,
97113} ) )
98114
99115vi . mock ( '@/lib/workflows/schedules' , ( ) => ( {
100- cleanupDeploymentVersion : vi . fn ( ) ,
116+ cleanupDeploymentVersion : ( ... args : unknown [ ] ) => mockCleanupDeploymentVersion ( ... args ) ,
101117 createSchedulesForDeploy : ( ...args : unknown [ ] ) => mockCreateSchedulesForDeploy ( ...args ) ,
102118 validateWorkflowSchedules : vi . fn ( ) . mockReturnValue ( { isValid : true } ) ,
103119} ) )
104120
105121vi . mock ( '@/lib/webhooks/deploy' , ( ) => ( {
106122 cleanupWebhooksForWorkflow : ( ...args : unknown [ ] ) => mockCleanupWebhooksForWorkflow ( ...args ) ,
107- restorePreviousVersionWebhooks : vi . fn ( ) ,
123+ restorePreviousVersionWebhooks : ( ...args : unknown [ ] ) =>
124+ mockRestorePreviousVersionWebhooks ( ...args ) ,
108125 saveTriggerWebhooksForDeploy : ( ...args : unknown [ ] ) => mockSaveTriggerWebhooksForDeploy ( ...args ) ,
109126} ) )
110127
@@ -130,20 +147,26 @@ describe('Workflow deploy route', () => {
130147 beforeEach ( ( ) => {
131148 vi . clearAllMocks ( )
132149 mockDbSelect . mockReturnValue ( { from : mockDbFrom } )
133- mockDbFrom . mockReturnValue ( { where : mockDbWhere } )
150+ mockDbFrom . mockReturnValue ( { where : mockDbWhere , innerJoin : mockDbInnerJoin } )
151+ mockDbInnerJoin . mockReturnValue ( { where : mockDbWhere } )
134152 mockDbWhere . mockReturnValue ( { limit : mockDbLimit , orderBy : mockDbOrderBy } )
135153 mockDbOrderBy . mockReturnValue ( { limit : mockDbLimit } )
136154 mockDbLimit . mockResolvedValue ( [ ] )
137155 mockDbUpdate . mockReturnValue ( { set : mockDbSet } )
138156 mockDbSet . mockReturnValue ( { where : mockDbWhere } )
139157 mockCleanupWebhooksForWorkflow . mockResolvedValue ( undefined )
140158 mockCreateSchedulesForDeploy . mockResolvedValue ( { success : true } )
159+ mockCleanupDeploymentVersion . mockResolvedValue ( undefined )
160+ mockDeleteDeploymentVersionById . mockResolvedValue ( { success : true } )
141161 mockLoadWorkflowFromNormalizedTables . mockResolvedValue ( {
142162 blocks : { 'block-1' : { id : 'block-1' , type : 'start_trigger' , name : 'Start' } } ,
143163 edges : [ ] ,
144164 loops : { } ,
145165 parallels : { } ,
146166 } )
167+ mockActivateWorkflowVersionById . mockResolvedValue ( { success : true } )
168+ mockReactivateWorkflowVersionForRollback . mockResolvedValue ( { success : true } )
169+ mockRestorePreviousVersionWebhooks . mockResolvedValue ( undefined )
147170 mockSaveTriggerWebhooksForDeploy . mockResolvedValue ( { success : true , warnings : [ ] } )
148171 mockRemoveMcpToolsForWorkflow . mockResolvedValue ( undefined )
149172 mockSyncMcpToolsForWorkflow . mockResolvedValue ( undefined )
@@ -221,6 +244,94 @@ describe('Workflow deploy route', () => {
221244 expect ( mockRecordAudit ) . toHaveBeenCalled ( )
222245 } )
223246
247+ it ( 'preserves prior deployedAt when failed redeploy rolls back' , async ( ) => {
248+ mockDbLimit . mockResolvedValue ( [
249+ { id : 'prev-1' , deployedAt : new Date ( '2024-01-01T00:00:00.000Z' ) } ,
250+ ] )
251+ mockValidateWorkflowAccess . mockResolvedValue ( {
252+ workflow : { id : 'wf-1' , name : 'Test Workflow' , workspaceId : 'ws-1' } ,
253+ auth : {
254+ success : true ,
255+ userId : 'api-user' ,
256+ userName : 'API Key Actor' ,
257+ userEmail : 'api@example.com' ,
258+ authType : 'api_key' ,
259+ } ,
260+ } )
261+ mockDeployWorkflow . mockResolvedValue ( {
262+ success : true ,
263+ deployedAt : '2024-02-01T00:00:00Z' ,
264+ deploymentVersionId : 'dep-failed' ,
265+ } )
266+ mockSaveTriggerWebhooksForDeploy . mockResolvedValue ( {
267+ success : false ,
268+ error : { message : 'Failed to save trigger configuration' , status : 500 } ,
269+ } )
270+
271+ const req = new NextRequest ( 'http://localhost:3000/api/workflows/wf-1/deploy' , {
272+ method : 'POST' ,
273+ headers : { 'x-api-key' : 'test-key' } ,
274+ } )
275+ const response = await POST ( req , { params : Promise . resolve ( { id : 'wf-1' } ) } )
276+
277+ expect ( response . status ) . toBe ( 500 )
278+ expect ( mockReactivateWorkflowVersionForRollback ) . toHaveBeenCalledWith ( {
279+ workflowId : 'wf-1' ,
280+ deploymentVersionId : 'prev-1' ,
281+ deployedAt : new Date ( '2024-01-01T00:00:00.000Z' ) ,
282+ } )
283+ expect ( mockActivateWorkflowVersionById ) . not . toHaveBeenCalled ( )
284+ expect ( mockRestorePreviousVersionWebhooks ) . toHaveBeenCalledWith (
285+ expect . objectContaining ( {
286+ workflow : { id : 'wf-1' , name : 'Test Workflow' , workspaceId : 'ws-1' } ,
287+ previousVersionId : 'prev-1' ,
288+ requestId : 'req-123' ,
289+ userId : 'api-user' ,
290+ } )
291+ )
292+ expect ( mockDeleteDeploymentVersionById ) . toHaveBeenCalledWith ( {
293+ workflowId : 'wf-1' ,
294+ deploymentVersionId : 'dep-failed' ,
295+ } )
296+ } )
297+
298+ it ( 'deletes failed created deployment version when first deploy rollback runs' , async ( ) => {
299+ mockValidateWorkflowAccess . mockResolvedValue ( {
300+ workflow : { id : 'wf-1' , name : 'Test Workflow' , workspaceId : 'ws-1' } ,
301+ auth : {
302+ success : true ,
303+ userId : 'api-user' ,
304+ userName : 'API Key Actor' ,
305+ userEmail : 'api@example.com' ,
306+ authType : 'api_key' ,
307+ } ,
308+ } )
309+ mockDeployWorkflow . mockResolvedValue ( {
310+ success : true ,
311+ deployedAt : '2024-02-01T00:00:00Z' ,
312+ deploymentVersionId : 'dep-failed' ,
313+ } )
314+ mockSaveTriggerWebhooksForDeploy . mockResolvedValue ( {
315+ success : false ,
316+ error : { message : 'Failed to save trigger configuration' , status : 500 } ,
317+ } )
318+ mockDbLimit . mockResolvedValue ( [ ] )
319+ mockUndeployWorkflow . mockResolvedValue ( { success : true } )
320+
321+ const req = new NextRequest ( 'http://localhost:3000/api/workflows/wf-1/deploy' , {
322+ method : 'POST' ,
323+ headers : { 'x-api-key' : 'test-key' } ,
324+ } )
325+ const response = await POST ( req , { params : Promise . resolve ( { id : 'wf-1' } ) } )
326+
327+ expect ( response . status ) . toBe ( 500 )
328+ expect ( mockDeleteDeploymentVersionById ) . toHaveBeenCalledWith ( {
329+ workflowId : 'wf-1' ,
330+ deploymentVersionId : 'dep-failed' ,
331+ } )
332+ expect ( mockUndeployWorkflow ) . toHaveBeenCalledWith ( { workflowId : 'wf-1' } )
333+ } )
334+
224335 it ( 'allows API-key auth for undeploy using hybrid auth userId' , async ( ) => {
225336 mockValidateWorkflowAccess . mockResolvedValue ( {
226337 workflow : { id : 'wf-1' , name : 'Test Workflow' , workspaceId : 'ws-1' } ,
0 commit comments