@@ -28,6 +28,7 @@ import { ActivityContext } from "../src/task/context/activity-context";
2828import { CompletableTask } from "../src/task/completable-task" ;
2929import { Task } from "../src/task/task" ;
3030import { getName , whenAll , whenAny } from "../src/task" ;
31+ import { RuntimeOrchestrationContext } from "../src/worker/runtime-orchestration-context" ;
3132
3233// Use NoOpLogger to suppress log output during tests
3334const testLogger = new NoOpLogger ( ) ;
@@ -271,36 +272,55 @@ describe("Orchestration Executor", () => {
271272 expect ( result . actions . length ) . toEqual ( 0 ) ;
272273 } ) ;
273274
274- it ( "should handle a task completion event with taskScheduledId 0 without error" , async ( ) => {
275+ it ( "should handle a task completion event with taskScheduledId 0 by looking up the task (not skipping due to falsy 0)" , async ( ) => {
276+ // This test validates the fix for issue #148: the old code used `if (taskId)` which
277+ // treated taskId === 0 as falsy and skipped the lookup entirely. The fix uses
278+ // `if (taskId !== undefined)` so that 0 is properly looked up.
275279 const dummyActivity = async ( _ : ActivityContext ) => {
276280 // do nothing
277281 } ;
282+ const injectedTask = new CompletableTask < string > ( ) ;
283+
284+ // Use an orchestrator that injects a CompletableTask at _pendingTasks[0]
285+ // to simulate a task with taskScheduledId = 0
278286 const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext , _ : any ) : any {
287+ const runtimeCtx = ctx as unknown as RuntimeOrchestrationContext ;
288+ runtimeCtx . _pendingTasks [ 0 ] = injectedTask ;
279289 const result = yield ctx . callActivity ( dummyActivity ) ;
280290 return result ;
281291 } ;
282292 const registry = new Registry ( ) ;
283293 const name = registry . addOrchestrator ( orchestrator ) ;
294+ registry . addActivity ( dummyActivity ) ;
284295 const oldEvents = [
285296 newOrchestratorStartedEvent ( ) ,
286297 newExecutionStartedEvent ( name , TEST_INSTANCE_ID , undefined ) ,
287298 newTaskScheduledEvent ( 1 , dummyActivity . name ) ,
288299 ] ;
289- // Send a completion event with taskScheduledId 0 — should not be silently skipped
290- const newEvents = [ newTaskCompletedEvent ( 0 , JSON . stringify ( "result" ) ) ] ;
300+ // Send completion for taskId 0 — with the fix, this completes the injected task
301+ const newEvents = [ newTaskCompletedEvent ( 0 , JSON . stringify ( "result-for-zero " ) ) ] ;
291302 const executor = new OrchestrationExecutor ( registry , testLogger ) ;
292303
293- // Should not throw — taskId 0 is properly handled (lookup attempted, no match found)
294- const result = await executor . execute ( TEST_INSTANCE_ID , oldEvents , newEvents ) ;
295- // Orchestration should still be waiting (task at id 1 was not completed)
296- expect ( result . actions . length ) . toEqual ( 0 ) ;
304+ await executor . execute ( TEST_INSTANCE_ID , oldEvents , newEvents ) ;
305+
306+ // With the fix (taskId !== undefined): the lookup at _pendingTasks[0] finds the injected
307+ // task and completes it. With the old code (if (taskId)): 0 is falsy, the lookup is
308+ // skipped, and the task is never completed.
309+ expect ( injectedTask . isComplete ) . toBe ( true ) ;
310+ expect ( injectedTask . getResult ( ) ) . toEqual ( "result-for-zero" ) ;
297311 } ) ;
298312
299- it ( "should handle a sub-orchestration completion event with taskScheduledId 0 without error" , async ( ) => {
313+ it ( "should handle a sub-orchestration completion event with taskScheduledId 0 by looking up the task" , async ( ) => {
314+ // Same regression test as above but for handleSubOrchestrationCompleted
300315 const subOrchestrator = async ( _ : OrchestrationContext ) => {
301316 // do nothing
302317 } ;
318+ const injectedTask = new CompletableTask < string > ( ) ;
319+
303320 const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext , _ : any ) : any {
321+ const runtimeCtx = ctx as unknown as RuntimeOrchestrationContext ;
322+ // Inject a CompletableTask at ID 0
323+ runtimeCtx . _pendingTasks [ 0 ] = injectedTask ;
304324 const res = yield ctx . callSubOrchestrator ( subOrchestrator ) ;
305325 return res ;
306326 } ;
@@ -312,14 +332,16 @@ describe("Orchestration Executor", () => {
312332 newExecutionStartedEvent ( orchestratorName , TEST_INSTANCE_ID , undefined ) ,
313333 newSubOrchestrationCreatedEvent ( 1 , subOrchestratorName , "sub-orch-123" ) ,
314334 ] ;
315- // Send a completion event with taskScheduledId 0 — should not be silently skipped
316- const newEvents = [ newSubOrchestrationCompletedEvent ( 0 , JSON . stringify ( "sub-result" ) ) ] ;
335+ // Send completion for taskId 0
336+ const newEvents = [ newSubOrchestrationCompletedEvent ( 0 , JSON . stringify ( "sub-result-zero " ) ) ] ;
317337 const executor = new OrchestrationExecutor ( registry , testLogger ) ;
318338
319- // Should not throw — taskId 0 is properly handled
320- const result = await executor . execute ( TEST_INSTANCE_ID , oldEvents , newEvents ) ;
321- // Orchestration should still be waiting (sub-orch task at id 1 was not completed)
322- expect ( result . actions . length ) . toEqual ( 0 ) ;
339+ await executor . execute ( TEST_INSTANCE_ID , oldEvents , newEvents ) ;
340+
341+ // With the fix: the lookup at _pendingTasks[0] finds the injected task and completes it.
342+ // With the old code: 0 is falsy, lookup is skipped, task never completed.
343+ expect ( injectedTask . isComplete ) . toBe ( true ) ;
344+ expect ( injectedTask . getResult ( ) ) . toEqual ( "sub-result-zero" ) ;
323345 } ) ;
324346
325347 it ( "should test the non-determinism detection logic when callTimer is expected but some other method (callActivity) is called instead" , async ( ) => {
0 commit comments