55import { NextRequest } from 'next/server'
66import { beforeEach , describe , expect , it , vi } from 'vitest'
77
8- const mockCheckHybridAuth = vi . fn ( )
9- const mockAuthorizeWorkflowByWorkspacePermission = vi . fn ( )
10- const mockMarkExecutionCancelled = vi . fn ( )
11- const mockAbortManualExecution = vi . fn ( )
12-
13- vi . mock ( '@sim/logger' , ( ) => ( {
14- createLogger : ( ) => ( { info : vi . fn ( ) , warn : vi . fn ( ) , error : vi . fn ( ) } ) ,
8+ const {
9+ mockCheckHybridAuth,
10+ mockAuthorizeWorkflowByWorkspacePermission,
11+ mockMarkExecutionCancelled,
12+ mockAbortManualExecution,
13+ mockCancelPausedExecution,
14+ } = vi . hoisted ( ( ) => ( {
15+ mockCheckHybridAuth : vi . fn ( ) ,
16+ mockAuthorizeWorkflowByWorkspacePermission : vi . fn ( ) ,
17+ mockMarkExecutionCancelled : vi . fn ( ) ,
18+ mockAbortManualExecution : vi . fn ( ) ,
19+ mockCancelPausedExecution : vi . fn ( ) ,
1520} ) )
1621
1722vi . mock ( '@/lib/auth/hybrid' , ( ) => ( {
@@ -26,19 +31,37 @@ vi.mock('@/lib/execution/manual-cancellation', () => ({
2631 abortManualExecution : ( ...args : unknown [ ] ) => mockAbortManualExecution ( ...args ) ,
2732} ) )
2833
34+ vi . mock ( '@/lib/workflows/executor/human-in-the-loop-manager' , ( ) => ( {
35+ PauseResumeManager : {
36+ cancelPausedExecution : ( ...args : unknown [ ] ) => mockCancelPausedExecution ( ...args ) ,
37+ } ,
38+ } ) )
39+
2940vi . mock ( '@/lib/workflows/utils' , ( ) => ( {
3041 authorizeWorkflowByWorkspacePermission : ( params : unknown ) =>
3142 mockAuthorizeWorkflowByWorkspacePermission ( params ) ,
3243} ) )
3344
45+ vi . mock ( '@/lib/posthog/server' , ( ) => ( {
46+ captureServerEvent : vi . fn ( ) ,
47+ } ) )
48+
3449import { POST } from './route'
3550
51+ const makeRequest = ( ) =>
52+ new NextRequest ( 'http://localhost/api/workflows/wf-1/executions/ex-1/cancel' , {
53+ method : 'POST' ,
54+ } )
55+
56+ const makeParams = ( ) => ( { params : Promise . resolve ( { id : 'wf-1' , executionId : 'ex-1' } ) } )
57+
3658describe ( 'POST /api/workflows/[id]/executions/[executionId]/cancel' , ( ) => {
3759 beforeEach ( ( ) => {
3860 vi . clearAllMocks ( )
3961 mockCheckHybridAuth . mockResolvedValue ( { success : true , userId : 'user-1' } )
4062 mockAuthorizeWorkflowByWorkspacePermission . mockResolvedValue ( { allowed : true } )
4163 mockAbortManualExecution . mockReturnValue ( false )
64+ mockCancelPausedExecution . mockResolvedValue ( false )
4265 } )
4366
4467 it ( 'returns success when cancellation was durably recorded' , async ( ) => {
@@ -47,14 +70,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
4770 reason : 'recorded' ,
4871 } )
4972
50- const response = await POST (
51- new NextRequest ( 'http://localhost/api/workflows/wf-1/executions/ex-1/cancel' , {
52- method : 'POST' ,
53- } ) ,
54- {
55- params : Promise . resolve ( { id : 'wf-1' , executionId : 'ex-1' } ) ,
56- }
57- )
73+ const response = await POST ( makeRequest ( ) , makeParams ( ) )
5874
5975 expect ( response . status ) . toBe ( 200 )
6076 await expect ( response . json ( ) ) . resolves . toEqual ( {
@@ -63,6 +79,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
6379 redisAvailable : true ,
6480 durablyRecorded : true ,
6581 locallyAborted : false ,
82+ pausedCancelled : false ,
6683 reason : 'recorded' ,
6784 } )
6885 } )
@@ -73,14 +90,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
7390 reason : 'redis_unavailable' ,
7491 } )
7592
76- const response = await POST (
77- new NextRequest ( 'http://localhost/api/workflows/wf-1/executions/ex-1/cancel' , {
78- method : 'POST' ,
79- } ) ,
80- {
81- params : Promise . resolve ( { id : 'wf-1' , executionId : 'ex-1' } ) ,
82- }
83- )
93+ const response = await POST ( makeRequest ( ) , makeParams ( ) )
8494
8595 expect ( response . status ) . toBe ( 200 )
8696 await expect ( response . json ( ) ) . resolves . toEqual ( {
@@ -89,6 +99,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
8999 redisAvailable : false ,
90100 durablyRecorded : false ,
91101 locallyAborted : false ,
102+ pausedCancelled : false ,
92103 reason : 'redis_unavailable' ,
93104 } )
94105 } )
@@ -99,14 +110,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
99110 reason : 'redis_write_failed' ,
100111 } )
101112
102- const response = await POST (
103- new NextRequest ( 'http://localhost/api/workflows/wf-1/executions/ex-1/cancel' , {
104- method : 'POST' ,
105- } ) ,
106- {
107- params : Promise . resolve ( { id : 'wf-1' , executionId : 'ex-1' } ) ,
108- }
109- )
113+ const response = await POST ( makeRequest ( ) , makeParams ( ) )
110114
111115 expect ( response . status ) . toBe ( 200 )
112116 await expect ( response . json ( ) ) . resolves . toEqual ( {
@@ -115,6 +119,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
115119 redisAvailable : true ,
116120 durablyRecorded : false ,
117121 locallyAborted : false ,
122+ pausedCancelled : false ,
118123 reason : 'redis_write_failed' ,
119124 } )
120125 } )
@@ -126,14 +131,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
126131 } )
127132 mockAbortManualExecution . mockReturnValue ( true )
128133
129- const response = await POST (
130- new NextRequest ( 'http://localhost/api/workflows/wf-1/executions/ex-1/cancel' , {
131- method : 'POST' ,
132- } ) ,
133- {
134- params : Promise . resolve ( { id : 'wf-1' , executionId : 'ex-1' } ) ,
135- }
136- )
134+ const response = await POST ( makeRequest ( ) , makeParams ( ) )
137135
138136 expect ( response . status ) . toBe ( 200 )
139137 await expect ( response . json ( ) ) . resolves . toEqual ( {
@@ -142,7 +140,50 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => {
142140 redisAvailable : false ,
143141 durablyRecorded : false ,
144142 locallyAborted : true ,
143+ pausedCancelled : false ,
144+ reason : 'redis_unavailable' ,
145+ } )
146+ } )
147+
148+ it ( 'returns success when a paused HITL execution is cancelled directly in the database' , async ( ) => {
149+ mockMarkExecutionCancelled . mockResolvedValue ( {
150+ durablyRecorded : false ,
151+ reason : 'redis_unavailable' ,
152+ } )
153+ mockCancelPausedExecution . mockResolvedValue ( true )
154+
155+ const response = await POST ( makeRequest ( ) , makeParams ( ) )
156+
157+ expect ( response . status ) . toBe ( 200 )
158+ await expect ( response . json ( ) ) . resolves . toEqual ( {
159+ success : true ,
160+ executionId : 'ex-1' ,
161+ redisAvailable : false ,
162+ durablyRecorded : false ,
163+ locallyAborted : false ,
164+ pausedCancelled : true ,
145165 reason : 'redis_unavailable' ,
146166 } )
147167 } )
168+
169+ it ( 'returns 401 when auth fails' , async ( ) => {
170+ mockCheckHybridAuth . mockResolvedValue ( { success : false , error : 'Unauthorized' } )
171+
172+ const response = await POST ( makeRequest ( ) , makeParams ( ) )
173+
174+ expect ( response . status ) . toBe ( 401 )
175+ } )
176+
177+ it ( 'returns 403 when workflow access is denied' , async ( ) => {
178+ mockMarkExecutionCancelled . mockResolvedValue ( { durablyRecorded : true , reason : 'recorded' } )
179+ mockAuthorizeWorkflowByWorkspacePermission . mockResolvedValue ( {
180+ allowed : false ,
181+ message : 'Access denied' ,
182+ status : 403 ,
183+ } )
184+
185+ const response = await POST ( makeRequest ( ) , makeParams ( ) )
186+
187+ expect ( response . status ) . toBe ( 403 )
188+ } )
148189} )
0 commit comments