@@ -169,142 +169,109 @@ describe("WorkflowExecutor", () => {
169169 expect ( workflowStatus ?. status ) . toBeTruthy ( ) ;
170170 expect ( workflowStatus ?. tasks ?. length ) . toBe ( 1 ) ;
171171 } ) ;
172- test ( "Should run workflow with http task with asyncComplete true" , async ( ) => {
173- const client = await clientPromise ;
174- const executor = new WorkflowExecutor ( client ) ;
175- const workflowName = `jsSdkTest-wf_with_asyncComplete_http_task-${ Date . now ( ) } ` ;
176- const taskName = `jsSdkTest-http_task_with_asyncComplete_true-${ Date . now ( ) } ` ;
177-
178- // Both v4 and v5 target the same in-cluster httpbin service. (An earlier note
179- // here claimed v4 couldn't reach httpbin-server and fell back to a public URL;
180- // that was a misdiagnosis. The HTTP call to httpbin actually succeeds on v4 with
181- // a 200 - the task simply never *surfaces* as IN_PROGRESS. See the version-specific
182- // readiness check below for the real reason.)
183- const asyncHttpUri = `${ HTTPBIN_BASE_URL } /api/hello?name=test1` ;
184-
185- await executor . registerWorkflow ( true , {
186- name : workflowName ,
187- version : 1 ,
188- ownerEmail : "developers@orkes.io" ,
189- tasks : [
190- httpTask ( taskName , { uri : asyncHttpUri , method : "GET" } , true ) ,
191- ] ,
192- inputParameters : [ ] ,
193- outputParameters : { } ,
194- timeoutSeconds : 30 ,
195- } ) ;
172+ // Gated to v5 only. An asyncComplete HTTP task settles differently depending on
173+ // whether HTTP is a registered in-server *system task*:
174+ // - v5 ships the api-orchestration module, so HTTP is a system task
175+ // (isSystemTask("HTTP") === true). An asyncComplete HTTP task genuinely
176+ // *settles* as IN_PROGRESS, awaiting external completion. That is a stable,
177+ // observable state we can assert on.
178+ // - v4 has no api-orchestration module, so HTTP is a plain external worker task
179+ // (isSystemTask("HTTP") === false). When the worker returns IN_PROGRESS with
180+ // callbackAfterSeconds = Integer.MAX_VALUE, OrkesWorkflowExecutor.updateTask
181+ // rewrites it to SCHEDULED and parks it ~68 years out. It therefore never
182+ // *settles* as IN_PROGRESS.
183+ //
184+ // We deliberately do NOT run this on v4. The old (yahoo.com) version of this test
185+ // only ever "passed" on v4 by catching the task in the transient IN_PROGRESS window
186+ // between the worker polling it (server sets IN_PROGRESS) and the worker returning
187+ // its result (server rewrites to SCHEDULED) - i.e. it was exploiting a race, not
188+ // checking the settled state of this task type on v4. Rather than assert two
189+ // different shapes in one test, we scope this to the version where IN_PROGRESS is
190+ // the real, settled outcome.
191+ describeForOrkesV5 ( "asyncComplete HTTP task (v5 system-task semantics)" , ( ) => {
192+ test ( "Should run workflow with http task with asyncComplete true" , async ( ) => {
193+ const client = await clientPromise ;
194+ const executor = new WorkflowExecutor ( client ) ;
195+ const workflowName = `jsSdkTest-wf_with_asyncComplete_http_task-${ Date . now ( ) } ` ;
196+ const taskName = `jsSdkTest-http_task_with_asyncComplete_true-${ Date . now ( ) } ` ;
197+
198+ const asyncHttpUri = `${ HTTPBIN_BASE_URL } /api/hello?name=test1` ;
199+
200+ await executor . registerWorkflow ( true , {
201+ name : workflowName ,
202+ version : 1 ,
203+ ownerEmail : "developers@orkes.io" ,
204+ tasks : [
205+ httpTask ( taskName , { uri : asyncHttpUri , method : "GET" } , true ) ,
206+ ] ,
207+ inputParameters : [ ] ,
208+ outputParameters : { } ,
209+ timeoutSeconds : 30 ,
210+ } ) ;
196211
197- const executionId = await executor . startWorkflow ( {
198- name : workflowName ,
199- input : { } ,
200- version : 1 ,
201- } ) ;
212+ const executionId = await executor . startWorkflow ( {
213+ name : workflowName ,
214+ input : { } ,
215+ version : 1 ,
216+ } ) ;
202217
203- if ( ! executionId ) {
204- throw new Error ( "Execution ID is undefined" ) ;
205- }
218+ if ( ! executionId ) {
219+ throw new Error ( "Execution ID is undefined" ) ;
220+ }
206221
207- // Log the id so a failed/incomplete run can be inspected in the Conductor UI.
208- console . log (
209- `[asyncComplete http] workflowId=${ executionId } workflow=${ workflowName } task=${ taskName } `
210- ) ;
222+ // Log the id so a failed/incomplete run can be inspected in the Conductor UI.
223+ console . log (
224+ `[asyncComplete http] workflowId=${ executionId } workflow=${ workflowName } task=${ taskName } `
225+ ) ;
211226
212- await waitForWorkflowStatus ( executor , executionId , "RUNNING" ) ;
213-
214- // Wait for the async HTTP task to finish executing and sit "open", awaiting an
215- // external completion. CRUCIALLY, the *observable* shape of that open state
216- // differs between server versions:
217- //
218- // - v5: the task reports status === "IN_PROGRESS".
219- // - v4: the task reports status === "SCHEDULED", with callbackAfterSeconds ===
220- // Integer.MAX_VALUE (2147483647, ~68 years). An HTTP task literally
221- // cannot be made to show IN_PROGRESS on a v4 server.
222- //
223- // Why: when a worker returns an IN_PROGRESS result, OrkesWorkflowExecutor.updateTask
224- // treats *system tasks* and *worker tasks* differently:
225- // if (!systemTaskRegistry.isSystemTask(type) && result == IN_PROGRESS)
226- // task.setStatus(SCHEDULED); // worker task -> re-queued / deferred
227- // else
228- // task.setStatus(result); // system task -> IN_PROGRESS preserved
229- // For asyncComplete=true the HTTP worker returns IN_PROGRESS with
230- // callbackAfterSeconds = Integer.MAX_VALUE, so a *worker-task* HTTP gets rewritten
231- // to SCHEDULED and parked ~68 years out.
232- //
233- // The deciding factor is whether HTTP is a registered in-server system task:
234- // - v5 ships the api-orchestration module, which registers HTTP as a
235- // WorkflowSystemTask -> isSystemTask("HTTP") === true -> IN_PROGRESS sticks.
236- // - v4 has no api-orchestration module, so HTTP is purely an external
237- // orkes-workers task -> isSystemTask("HTTP") === false -> the worker's
238- // IN_PROGRESS is rewritten to SCHEDULED.
239- // Note: the updateTask branch itself is identical in v4 and v5; the behavioral
240- // difference comes entirely from that system-task registration, not from a change
241- // in the status-handling code.
242- const isV5 = Number ( process . env . ORKES_BACKEND_VERSION ) >= 5 ;
243- const INTEGER_MAX_VALUE = 2147483647 ; // Integer.MAX_VALUE; v4's "park forever" callbackAfterSeconds
244- const taskReadyTimeout = 30000 ;
245- const pollInterval = 3000 ;
246- const taskReadyStart = Date . now ( ) ;
247- let taskStatus : string | undefined ;
248- let taskCallbackAfterSeconds : number | undefined ;
249- let taskPollCount : number | undefined ;
250- while ( Date . now ( ) - taskReadyStart < taskReadyTimeout ) {
251- const wf = await executor . getWorkflow ( executionId , true ) ;
252- const task = wf ?. tasks ?. [ 0 ] ;
253- taskStatus = task ?. status ;
254- taskCallbackAfterSeconds = task ?. callbackAfterSeconds ;
255- taskPollCount = task ?. pollCount ;
256- // v5 surfaces IN_PROGRESS directly; on v4 the only observable "open" signal is
257- // a SCHEDULED task that has been polled (executed) and parked indefinitely.
258- const taskReady = isV5
259- ? taskStatus === "IN_PROGRESS"
260- : taskStatus === "SCHEDULED" &&
261- ( taskPollCount ?? 0 ) > 0 &&
262- taskCallbackAfterSeconds === INTEGER_MAX_VALUE ;
263- if ( taskReady ) break ;
264- if ( taskStatus === "FAILED" || taskStatus === "COMPLETED" ) {
265- throw new Error (
266- `Task ended in unexpected state: ${ taskStatus } ` +
267- `(workflowId=${ executionId } , workflow=${ workflowName } , task=${ taskName } ` +
268- ( task ?. reasonForIncompletion
269- ? `, reason=${ task . reasonForIncompletion } `
270- : "" ) +
271- ")"
272- ) ;
227+ await waitForWorkflowStatus ( executor , executionId , "RUNNING" ) ;
228+
229+ // Wait for the async HTTP task to finish executing and settle as IN_PROGRESS,
230+ // awaiting external completion (see the block comment above for why this is the
231+ // stable, settled state on v5).
232+ const taskReadyTimeout = 30000 ;
233+ const pollInterval = 3000 ;
234+ const taskReadyStart = Date . now ( ) ;
235+ let taskStatus : string | undefined ;
236+ while ( Date . now ( ) - taskReadyStart < taskReadyTimeout ) {
237+ const wf = await executor . getWorkflow ( executionId , true ) ;
238+ const task = wf ?. tasks ?. [ 0 ] ;
239+ taskStatus = task ?. status ;
240+ if ( taskStatus === "IN_PROGRESS" ) break ;
241+ if ( taskStatus === "FAILED" || taskStatus === "COMPLETED" ) {
242+ throw new Error (
243+ `Task ended in unexpected state: ${ taskStatus } ` +
244+ `(workflowId=${ executionId } , workflow=${ workflowName } , task=${ taskName } ` +
245+ ( task ?. reasonForIncompletion
246+ ? `, reason=${ task . reasonForIncompletion } `
247+ : "" ) +
248+ ")"
249+ ) ;
250+ }
251+ await new Promise ( ( resolve ) => setTimeout ( resolve , pollInterval ) ) ;
273252 }
274- await new Promise ( ( resolve ) => setTimeout ( resolve , pollInterval ) ) ;
275- }
276253
277- // Assert the version-specific "open" shape explicitly, so the divergence stays
278- // documented and this fails loudly if v4 ever starts reporting IN_PROGRESS (e.g.
279- // if HTTP becomes a registered system task there too).
280- if ( isV5 ) {
281- console . log ( "V5: expecting HTTP task to be IN_PROGRESS" ) ;
282254 expect ( taskStatus ) . toEqual ( "IN_PROGRESS" ) ;
283- } else {
284- console . log ( "V4: expecting HTTP task to be SCHEDULED" ) ;
285- expect ( taskStatus ) . toEqual ( "SCHEDULED" ) ;
286- expect ( taskPollCount ?? 0 ) . toBeGreaterThan ( 0 ) ;
287- expect ( taskCallbackAfterSeconds ) . toEqual ( INTEGER_MAX_VALUE ) ;
288- }
289255
290- const taskClient = new TaskClient ( client ) ;
291- await taskClient . updateTaskResult ( executionId , taskName , "COMPLETED" , {
292- hello : "From manuall api call updating task result" ,
293- } ) ;
256+ const taskClient = new TaskClient ( client ) ;
257+ await taskClient . updateTaskResult ( executionId , taskName , "COMPLETED" , {
258+ hello : "From manuall api call updating task result" ,
259+ } ) ;
294260
295- const workflowStatusAfter = await waitForWorkflowStatus (
296- executor ,
297- executionId ,
298- "COMPLETED" ,
299- 30000 ,
300- 5000
301- ) ;
261+ const workflowStatusAfter = await waitForWorkflowStatus (
262+ executor ,
263+ executionId ,
264+ "COMPLETED" ,
265+ 30000 ,
266+ 5000
267+ ) ;
302268
303- expect ( workflowStatusAfter . tasks ?. [ 0 ] ?. status ) . toEqual ( "COMPLETED" ) ;
269+ expect ( workflowStatusAfter . tasks ?. [ 0 ] ?. status ) . toEqual ( "COMPLETED" ) ;
304270
305- await cleanupWorkflowsAndTasks ( metadataClient , {
306- workflows : [ { name : workflowName , version : 1 } ] ,
307- tasks : [ taskName ] ,
271+ await cleanupWorkflowsAndTasks ( metadataClient , {
272+ workflows : [ { name : workflowName , version : 1 } ] ,
273+ tasks : [ taskName ] ,
274+ } ) ;
308275 } ) ;
309276 } ) ;
310277
0 commit comments