@@ -3,7 +3,12 @@ import type { Logger } from "@voltagent/internal";
33import { describe , expect , it , vi } from "vitest" ;
44import type { ErrorResponse } from "../types/responses" ;
55import { isErrorResponse } from "../types/responses" ;
6- import { handleAttachWorkflowStream , handleStreamWorkflow } from "./workflow.handlers" ;
6+ import {
7+ handleAttachWorkflowStream ,
8+ handleCancelWorkflow ,
9+ handleStreamWorkflow ,
10+ handleSuspendWorkflow ,
11+ } from "./workflow.handlers" ;
712
813type ParsedSSEEvent = {
914 id ?: string ;
@@ -48,7 +53,7 @@ function createDeps(options?: {
4853 const stream = options ?. streamFactory ? options . streamFactory : vi . fn ( ) ;
4954
5055 const workflow = {
51- createSuspendController : vi . fn ( ) . mockReturnValue ( createSuspendController ( ) ) ,
56+ createSuspendController : vi . fn ( ) . mockImplementation ( createSuspendController ) ,
5257 stream,
5358 memory : {
5459 getWorkflowState,
@@ -120,6 +125,65 @@ function assertErrorResponse(
120125 expect ( isErrorResponse ( value ) ) . toBe ( true ) ;
121126}
122127
128+ async function startBlockingWorkflowExecution ( action : string ) {
129+ const logger = createLogger ( ) ;
130+ let releaseStream = ( ) => { } ;
131+ const streamBlocked = new Promise < void > ( ( resolve ) => {
132+ releaseStream = resolve ;
133+ } ) ;
134+ const executionId = `exec-${ action } -1` ;
135+ const { deps } = createDeps ( {
136+ workflowState : {
137+ id : executionId ,
138+ workflowId : "wf-1" ,
139+ workflowName : "Workflow 1" ,
140+ status : "running" ,
141+ createdAt : new Date ( ) ,
142+ updatedAt : new Date ( ) ,
143+ } ,
144+ streamFactory : ( ) => ( {
145+ executionId,
146+ [ Symbol . asyncIterator ] : async function * ( ) {
147+ yield {
148+ type : "workflow-start" ,
149+ executionId,
150+ from : "Workflow 1" ,
151+ status : "running" ,
152+ timestamp : new Date ( ) . toISOString ( ) ,
153+ } ;
154+ await streamBlocked ;
155+ } ,
156+ result : Promise . resolve ( { ok : true } ) ,
157+ status : Promise . resolve ( "completed" ) ,
158+ endAt : Promise . resolve ( new Date ( "2026-01-01T00:00:00.000Z" ) ) ,
159+ } ) ,
160+ } ) ;
161+
162+ const streamResponse = await handleStreamWorkflow ( "wf-1" , { input : { } } , deps , logger ) ;
163+ expect ( isErrorResponse ( streamResponse ) ) . toBe ( false ) ;
164+
165+ if ( isErrorResponse ( streamResponse ) ) {
166+ throw new Error ( "Expected active workflow stream" ) ;
167+ }
168+
169+ const streamReader = streamResponse . getReader ( ) ;
170+ await readSSEEvent ( streamReader ) ;
171+
172+ const controller = deps . workflowRegistry . activeExecutions ?. get ( executionId ) as ReturnType <
173+ typeof createSuspendController
174+ > ;
175+ expect ( controller ) . toBeDefined ( ) ;
176+
177+ return {
178+ controller,
179+ deps,
180+ executionId,
181+ logger,
182+ releaseStream,
183+ streamReader,
184+ } ;
185+ }
186+
123187describe ( "workflow stream attach handler" , ( ) => {
124188 it ( "returns 404 when workflow does not exist" , async ( ) => {
125189 const logger = createLogger ( ) ;
@@ -260,4 +324,63 @@ describe("workflow stream attach handler", () => {
260324 const attachedFinal = await readSSEEvent ( attachedReader ) ;
261325 expect ( attachedFinal . data . type ) . toBe ( "workflow-result" ) ;
262326 } ) ;
327+
328+ it . each ( [
329+ {
330+ action : "suspend" ,
331+ error : "not found" ,
332+ handler : handleSuspendWorkflow ,
333+ method : "suspend" ,
334+ } ,
335+ {
336+ action : "cancel" ,
337+ error : "No active execution found" ,
338+ handler : handleCancelWorkflow ,
339+ method : "cancel" ,
340+ } ,
341+ ] ) (
342+ "rejects $action requests on the wrong workflow route" ,
343+ async ( { action, error, handler, method } ) => {
344+ const { controller, deps, executionId, logger, releaseStream, streamReader } =
345+ await startBlockingWorkflowExecution ( action ) ;
346+
347+ const wrongRouteResponse = await handler (
348+ executionId ,
349+ { __workflowId : "wf-2" , reason : "wrong route" } ,
350+ deps ,
351+ logger ,
352+ ) ;
353+
354+ expect ( wrongRouteResponse . success ) . toBe ( false ) ;
355+ expect ( ( wrongRouteResponse as ErrorResponse ) . error ) . toContain ( error ) ;
356+ expect ( controller [ method ] ) . not . toHaveBeenCalled ( ) ;
357+ expect ( deps . workflowRegistry . activeExecutions ?. has ( executionId ) ) . toBe ( true ) ;
358+
359+ const missingRouteResponse = await handler (
360+ executionId ,
361+ { reason : "missing route id" } as any ,
362+ deps ,
363+ logger ,
364+ ) ;
365+
366+ expect ( missingRouteResponse . success ) . toBe ( false ) ;
367+ expect ( ( missingRouteResponse as ErrorResponse ) . error ) . toContain ( error ) ;
368+ expect ( controller [ method ] ) . not . toHaveBeenCalled ( ) ;
369+ expect ( deps . workflowRegistry . activeExecutions ?. has ( executionId ) ) . toBe ( true ) ;
370+
371+ const correctRouteResponse = await handler (
372+ executionId ,
373+ { __workflowId : "wf-1" , reason : "correct route" } ,
374+ deps ,
375+ logger ,
376+ ) ;
377+
378+ expect ( correctRouteResponse . success ) . toBe ( true ) ;
379+ expect ( controller [ method ] ) . toHaveBeenCalledWith ( "correct route" ) ;
380+ expect ( deps . workflowRegistry . activeExecutions ?. has ( executionId ) ) . toBe ( false ) ;
381+
382+ releaseStream ( ) ;
383+ await streamReader . cancel ( ) ;
384+ } ,
385+ ) ;
263386} ) ;
0 commit comments