@@ -291,8 +291,8 @@ public TaskOrchestration create() {
291291 // region Parent Instance Tests
292292
293293 @ Test
294- void execute_withParentInstance_getParentReturnsParent () {
295- // Arrange: orchestration that captures getParent ()
294+ void execute_withParentInstance_getParentInstanceReturnsParent () {
295+ // Arrange: orchestration that captures getParentInstance ()
296296 String orchName = "ChildOrch" ;
297297 ParentOrchestrationInstance [] captured = new ParentOrchestrationInstance [1 ];
298298
@@ -303,7 +303,7 @@ void execute_withParentInstance_getParentReturnsParent() {
303303 @ Override
304304 public TaskOrchestration create () {
305305 return ctx -> {
306- captured [0 ] = ctx .getParent ();
306+ captured [0 ] = ctx .getParentInstance ();
307307 ctx .complete ("done" );
308308 };
309309 }
@@ -347,13 +347,13 @@ public TaskOrchestration create() {
347347 executor .execute (Collections .emptyList (), newEvents , null );
348348
349349 // Assert
350- assertNotNull (captured [0 ], "getParent () should not be null for a sub-orchestration" );
350+ assertNotNull (captured [0 ], "getParentInstance () should not be null for a sub-orchestration" );
351351 assertEquals ("ParentOrch" , captured [0 ].getName ());
352352 assertEquals ("parent-123" , captured [0 ].getInstanceId ());
353353 }
354354
355355 @ Test
356- void execute_withoutParentInstance_getParentReturnsNull () {
356+ void execute_withoutParentInstance_getParentInstanceReturnsNull () {
357357 // Arrange: orchestration without parent instance
358358 String orchName = "StandaloneOrch" ;
359359 ParentOrchestrationInstance [] captured = new ParentOrchestrationInstance [1 ];
@@ -366,7 +366,7 @@ void execute_withoutParentInstance_getParentReturnsNull() {
366366 @ Override
367367 public TaskOrchestration create () {
368368 return ctx -> {
369- captured [0 ] = ctx .getParent ();
369+ captured [0 ] = ctx .getParentInstance ();
370370 wasCalled [0 ] = true ;
371371 ctx .complete ("done" );
372372 };
@@ -406,7 +406,7 @@ public TaskOrchestration create() {
406406
407407 // Assert
408408 assertTrue (wasCalled [0 ], "Orchestrator should have been called" );
409- assertNull (captured [0 ], "getParent () should be null for a standalone orchestration" );
409+ assertNull (captured [0 ], "getParentInstance () should be null for a standalone orchestration" );
410410 }
411411
412412 @ Test
@@ -424,7 +424,7 @@ void execute_withParentInstance_preservesExactValues() {
424424 @ Override
425425 public TaskOrchestration create () {
426426 return ctx -> {
427- captured [0 ] = ctx .getParent ();
427+ captured [0 ] = ctx .getParentInstance ();
428428 ctx .complete ("done" );
429429 };
430430 }
@@ -486,7 +486,7 @@ void execute_withParentInstance_emptyFields_acceptsValues() {
486486 @ Override
487487 public TaskOrchestration create () {
488488 return ctx -> {
489- captured [0 ] = ctx .getParent ();
489+ captured [0 ] = ctx .getParentInstance ();
490490 ctx .complete ("done" );
491491 };
492492 }
@@ -530,45 +530,69 @@ public TaskOrchestration create() {
530530 executor .execute (Collections .emptyList (), newEvents , null );
531531
532532 // Assert: permissive — empty values accepted as-is, matching .NET behavior
533- assertNotNull (captured [0 ], "getParent () should not be null when parentInstance is present" );
533+ assertNotNull (captured [0 ], "getParentInstance () should not be null when parentInstance is present" );
534534 assertEquals ("" , captured [0 ].getName ());
535535 assertEquals ("" , captured [0 ].getInstanceId ());
536536 }
537537
538538 @ Test
539- void taskOrchestrationContext_defaultGetParent_returnsNull () {
540- // Minimal implementation that does NOT override getParent().
541- // All abstract methods are stubbed to satisfy the interface contract.
542- TaskOrchestrationContext minimalContext = new TaskOrchestrationContext () {
543- @ Override public String getName () { return "test" ; }
544- @ Override public <V > V getInput (Class <V > t ) { return null ; }
545- @ Override public String getInstanceId () { return "id" ; }
546- @ Override public Instant getCurrentInstant () { return Instant .now (); }
547- @ Override public boolean getIsReplaying () { return false ; }
548- @ Override public String getVersion () { return "" ; }
549- @ Override public <V > Task <List <V >> allOf (List <Task <V >> tasks ) { return null ; }
550- @ Override public Task <Task <?>> anyOf (List <Task <?>> tasks ) { return null ; }
551- @ Override public <V > Task <V > callActivity (String name , Object input , TaskOptions options , Class <V > returnType ) { return null ; }
552- @ Override public <V > Task <V > callSubOrchestrator (String name , Object input , String instanceId , TaskOptions options , Class <V > returnType ) { return null ; }
553- @ Override public Task <Void > createTimer (Duration delay ) { return null ; }
554- @ Override public Task <Void > createTimer (ZonedDateTime zonedDateTime ) { return null ; }
555- @ Override public <V > Task <V > waitForExternalEvent (String name , Duration timeout , Class <V > dataType ) { return null ; }
556- @ Override public void continueAsNew (Object input , boolean preserveUnprocessedEvents ) {}
557- @ Override public void sendEvent (String instanceId , String eventName , Object eventData ) {}
558- @ Override public void complete (Object output ) {}
559- @ Override public UUID newUUID () { return UUID .randomUUID (); }
560- @ Override public void signalEntity (EntityInstanceId entityId , String operationName , Object input , SignalEntityOptions options ) {}
561- @ Override public <V > Task <V > callEntity (EntityInstanceId entityId , String operationName , Object input , Class <V > returnType ) { return null ; }
562- @ Override public <V > Task <V > callEntity (EntityInstanceId entityId , String operationName , Object input , Class <V > returnType , CallEntityOptions options ) { return null ; }
563- @ Override public Task <AutoCloseable > lockEntities (List <EntityInstanceId > entityIds ) { return null ; }
564- @ Override public boolean isInCriticalSection () { return false ; }
565- @ Override public List <EntityInstanceId > getLockedEntities () { return Collections .emptyList (); }
566- @ Override public void setCustomStatus (Object customStatus ) {}
567- @ Override public void clearCustomStatus () {}
568- };
569-
570- // Assert: default method returns null
571- assertNull (minimalContext .getParent (), "Default getParent() should return null" );
539+ void execute_withParentInstance_unsetProtoFields_defaultsToEmpty () {
540+ // Arrange: ParentInstanceInfo is present, but its inner fields
541+ // (name StringValue and OrchestrationInstance) are unset.
542+ // The executor must not NPE — it should default to empty strings.
543+ String orchName = "ChildOrch" ;
544+ ParentOrchestrationInstance [] captured = new ParentOrchestrationInstance [1 ];
545+
546+ HashMap <String , TaskOrchestrationFactory > factories = new HashMap <>();
547+ factories .put (orchName , new TaskOrchestrationFactory () {
548+ @ Override
549+ public String getName () { return orchName ; }
550+ @ Override
551+ public TaskOrchestration create () {
552+ return ctx -> {
553+ captured [0 ] = ctx .getParentInstance ();
554+ ctx .complete ("done" );
555+ };
556+ }
557+ });
558+
559+ TaskOrchestrationExecutor executor = new TaskOrchestrationExecutor (
560+ factories , new JacksonDataConverter (), Duration .ofDays (3 ), logger , null );
561+
562+ List <HistoryEvent > newEvents = Arrays .asList (
563+ HistoryEvent .newBuilder ()
564+ .setEventId (-1 )
565+ .setTimestamp (Timestamp .getDefaultInstance ())
566+ .setOrchestratorStarted (OrchestratorStartedEvent .getDefaultInstance ())
567+ .build (),
568+ HistoryEvent .newBuilder ()
569+ .setEventId (-1 )
570+ .setTimestamp (Timestamp .getDefaultInstance ())
571+ .setExecutionStarted (ExecutionStartedEvent .newBuilder ()
572+ .setName (orchName )
573+ .setVersion (StringValue .of ("" ))
574+ .setInput (StringValue .of ("\" test\" " ))
575+ .setOrchestrationInstance (OrchestrationInstance .newBuilder ()
576+ .setInstanceId ("child-instance" )
577+ .build ())
578+ // ParentInstanceInfo present but inner fields unset (no setName, no setOrchestrationInstance)
579+ .setParentInstance (ParentInstanceInfo .getDefaultInstance ())
580+ .build ())
581+ .build (),
582+ HistoryEvent .newBuilder ()
583+ .setEventId (-1 )
584+ .setTimestamp (Timestamp .getDefaultInstance ())
585+ .setOrchestratorCompleted (OrchestratorCompletedEvent .getDefaultInstance ())
586+ .build ()
587+ );
588+
589+ // Act — must not throw NPE
590+ executor .execute (Collections .emptyList (), newEvents , null );
591+
592+ // Assert: parent is non-null, fields default to empty
593+ assertNotNull (captured [0 ], "getParentInstance() should not be null when ParentInstanceInfo is present" );
594+ assertEquals ("" , captured [0 ].getName ());
595+ assertEquals ("" , captured [0 ].getInstanceId ());
572596 }
573597
574598 @ Test
@@ -585,7 +609,7 @@ void executorReplay_parentValueStableAcrossReplays() {
585609 @ Override
586610 public TaskOrchestration create () {
587611 return ctx -> {
588- captured [0 ] = ctx .getParent ();
612+ captured [0 ] = ctx .getParentInstance ();
589613 ctx .complete ("done" );
590614 };
591615 }
@@ -644,7 +668,7 @@ public TaskOrchestration create() {
644668 executor .execute (pastEvents , newEvents , null );
645669
646670 // Assert: parent is still available and unchanged during replay
647- assertNotNull (captured [0 ], "getParent () should not be null during replay" );
671+ assertNotNull (captured [0 ], "getParentInstance () should not be null during replay" );
648672 assertEquals ("ParentOrch" , captured [0 ].getName ());
649673 assertEquals ("parent-456" , captured [0 ].getInstanceId ());
650674 }
0 commit comments