@@ -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 ( ) ;
@@ -246,6 +247,103 @@ describe("Orchestration Executor", () => {
246247 // const userCodeStatement = "ctx.callActivity(dummyActivity, orchestratorInput)";
247248 // expect(completeAction?.getFailuredetails()?.getStacktrace()?.getValue()).toContain(userCodeStatement);
248249 } ) ;
250+ it ( "should handle a task completion event with an unmatched taskScheduledId without error" , async ( ) => {
251+ const dummyActivity = async ( _ : ActivityContext ) => {
252+ // do nothing
253+ } ;
254+ const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext , _ : any ) : any {
255+ const result = yield ctx . callActivity ( dummyActivity ) ;
256+ return result ;
257+ } ;
258+ const registry = new Registry ( ) ;
259+ const name = registry . addOrchestrator ( orchestrator ) ;
260+ const oldEvents = [
261+ newOrchestratorStartedEvent ( ) ,
262+ newExecutionStartedEvent ( name , TEST_INSTANCE_ID , undefined ) ,
263+ newTaskScheduledEvent ( 1 , dummyActivity . name ) ,
264+ ] ;
265+ // Send a completion event with a non-matching taskScheduledId (999)
266+ const newEvents = [ newTaskCompletedEvent ( 999 , JSON . stringify ( "result" ) ) ] ;
267+ const executor = new OrchestrationExecutor ( registry , testLogger ) ;
268+
269+ // Should not throw — the unmatched event is logged and the orchestration continues waiting
270+ const result = await executor . execute ( TEST_INSTANCE_ID , oldEvents , newEvents ) ;
271+ // Orchestration should still be waiting (task at id 1 was not completed)
272+ expect ( result . actions . length ) . toEqual ( 0 ) ;
273+ } ) ;
274+
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.
279+ const dummyActivity = async ( _ : ActivityContext ) => {
280+ // do nothing
281+ } ;
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
286+ const orchestrator : TOrchestrator = async function * ( ctx : OrchestrationContext , _ : any ) : any {
287+ const runtimeCtx = ctx as unknown as RuntimeOrchestrationContext ;
288+ runtimeCtx . _pendingTasks [ 0 ] = injectedTask ;
289+ const result = yield ctx . callActivity ( dummyActivity ) ;
290+ return result ;
291+ } ;
292+ const registry = new Registry ( ) ;
293+ const name = registry . addOrchestrator ( orchestrator ) ;
294+ registry . addActivity ( dummyActivity ) ;
295+ const oldEvents = [
296+ newOrchestratorStartedEvent ( ) ,
297+ newExecutionStartedEvent ( name , TEST_INSTANCE_ID , undefined ) ,
298+ newTaskScheduledEvent ( 1 , dummyActivity . name ) ,
299+ ] ;
300+ // Send completion for taskId 0 — with the fix, this completes the injected task
301+ const newEvents = [ newTaskCompletedEvent ( 0 , JSON . stringify ( "result-for-zero" ) ) ] ;
302+ const executor = new OrchestrationExecutor ( registry , testLogger ) ;
303+
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" ) ;
311+ } ) ;
312+
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
315+ const subOrchestrator = async ( _ : OrchestrationContext ) => {
316+ // do nothing
317+ } ;
318+ const injectedTask = new CompletableTask < string > ( ) ;
319+
320+ 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 ;
324+ const res = yield ctx . callSubOrchestrator ( subOrchestrator ) ;
325+ return res ;
326+ } ;
327+ const registry = new Registry ( ) ;
328+ const subOrchestratorName = registry . addOrchestrator ( subOrchestrator ) ;
329+ const orchestratorName = registry . addOrchestrator ( orchestrator ) ;
330+ const oldEvents = [
331+ newOrchestratorStartedEvent ( ) ,
332+ newExecutionStartedEvent ( orchestratorName , TEST_INSTANCE_ID , undefined ) ,
333+ newSubOrchestrationCreatedEvent ( 1 , subOrchestratorName , "sub-orch-123" ) ,
334+ ] ;
335+ // Send completion for taskId 0
336+ const newEvents = [ newSubOrchestrationCompletedEvent ( 0 , JSON . stringify ( "sub-result-zero" ) ) ] ;
337+ const executor = new OrchestrationExecutor ( registry , testLogger ) ;
338+
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" ) ;
345+ } ) ;
346+
249347 it ( "should test the non-determinism detection logic when callTimer is expected but some other method (callActivity) is called instead" , async ( ) => {
250348 const dummyActivity = async ( _ : ActivityContext ) => {
251349 // do nothing
0 commit comments