@@ -166,7 +166,7 @@ public void runChild_secondUseAsOutput_failsTheCaller() {
166166 }
167167
168168 @ Test
169- public void runChild_useAsOutputThenCallerProducesOutputOnItsState_failsTheCaller () {
169+ public void runChild_useAsOutputThenCallerProducesOutputOnItsState_dropsIt () {
170170 Node dispatcher =
171171 dispatcher (
172172 "dispatcher" ,
@@ -177,10 +177,32 @@ public void runChild_useAsOutputThenCallerProducesOutputOnItsState_failsTheCalle
177177 Completable .fromAction (() -> ctx .requireNodeState ().produceOutput ("OWN" )))
178178 .toFlowable ());
179179
180+ Context context = run (workflow (new Edge (START , dispatcher )));
181+
182+ assertThat (context .requireNodeState ().failure ()).isEmpty ();
183+ assertThat (context .requireNodeState ().output ()).hasValue ("CHILD_OUT" );
184+ assertThat (outputs ()).doesNotContain ("OWN" );
185+ }
186+
187+ @ Test
188+ public void runChild_useAsOutputThenCallerProducesTwoOutputs_failsTheCaller () {
189+ Node dispatcher =
190+ dispatcher (
191+ "dispatcher" ,
192+ (ctx , input ) ->
193+ NodeRunner .runChild (ctx , emitter ("child" , "CHILD_OUT" ), null , USE_AS_OUTPUT )
194+ .ignoreElement ()
195+ .doOnComplete (
196+ () -> {
197+ ctx .requireNodeState ().produceOutput ("OWN" );
198+ ctx .requireNodeState ().produceOutput ("SECOND" );
199+ })
200+ .toFlowable ());
201+
180202 Exception failure = runToFailure (workflow (new Edge (START , dispatcher )));
181203
182204 assertThat (failure ).isInstanceOf (IllegalStateException .class );
183- assertThat (failure ).hasMessageThat ().contains ("delegated its output" );
205+ assertThat (failure ).hasMessageThat ().contains ("produced a second output" );
184206 }
185207
186208 @ Test
@@ -249,6 +271,29 @@ public void runChild_delegateThatFailsBeforeEmittingItsOutput_letsTheCallerProdu
249271 assertThat (outputs ()).doesNotContain ("CHILD_OUT" );
250272 }
251273
274+ @ Test
275+ public void runChild_callerOutputDroppedThenDelegateFailsBeforeEmitting_failsTheFallback () {
276+ Node child =
277+ new TestNode (
278+ "child" ,
279+ (ctx , input ) -> {
280+ // Simulates the caller producing an output while its delegate still runs.
281+ ctx .requireNodeState ()
282+ .parent ()
283+ .orElseThrow ()
284+ .requireNodeState ()
285+ .produceOutput ("EARLY" );
286+ return Flowable .error (new IllegalStateException ("failed before emitting" ));
287+ });
288+ Node dispatcher =
289+ handling ("dispatcher" , child , USE_AS_OUTPUT , DynamicNodeFailedException .class , "FALLBACK" );
290+
291+ Exception failure = runToFailure (workflow (new Edge (START , dispatcher )));
292+
293+ assertThat (failure ).isInstanceOf (IllegalStateException .class );
294+ assertThat (failure ).hasMessageThat ().contains ("produced a second output" );
295+ }
296+
252297 @ Test
253298 public void runChild_delegateThatFailsBeforeEmitting_letsTheCallerDelegateAgain () {
254299 Node dispatcher =
@@ -277,7 +322,7 @@ ctx, emitter("second", "SECOND"), null, USE_AS_OUTPUT)
277322 }
278323
279324 @ Test
280- public void runChild_delegateCanceledBeforeEmitting_keepsTheCallersOutputDelegated () {
325+ public void runChild_delegateCanceledBeforeEmitting_keepsTheDelegationAndDropsTheFallback () {
281326 MaybeSubject <Boolean > cancel = MaybeSubject .create ();
282327 Node child =
283328 new TestNode (
@@ -286,14 +331,16 @@ public void runChild_delegateCanceledBeforeEmitting_keepsTheCallersOutputDelegat
286331 Completable .fromAction (() -> cancel .onSuccess (true )).andThen (Flowable .never ()));
287332 Node dispatcher = cancelingAndFallingBack ("dispatcher" , child , cancel );
288333
289- Exception failure = runToFailure (workflow (new Edge (START , dispatcher )));
334+ Context context = run (workflow (new Edge (START , dispatcher )));
290335
291- assertThat (failure ).isInstanceOf (IllegalStateException .class );
292- assertThat (failure ).hasMessageThat ().contains ("delegated its output" );
336+ // Only a failed delegate releases the delegation, so the caller ends without an output.
337+ assertThat (context .requireNodeState ().failure ()).isEmpty ();
338+ assertThat (context .requireNodeState ().output ()).isEmpty ();
339+ assertThat (outputs ()).doesNotContain ("FALLBACK" );
293340 }
294341
295342 @ Test
296- public void runChild_delegateCanceledAfterEmittingItsOutput_failsACallerProducingItsOwn () {
343+ public void runChild_delegateCanceledAfterEmittingItsOutput_keepsThatOutputAsTheCallers () {
297344 MaybeSubject <Boolean > cancel = MaybeSubject .create ();
298345 Node child =
299346 new TestNode (
@@ -304,15 +351,16 @@ public void runChild_delegateCanceledAfterEmittingItsOutput_failsACallerProducin
304351 .concatWith (Flowable .never ()));
305352 Node dispatcher = cancelingAndFallingBack ("dispatcher" , child , cancel );
306353
307- Exception failure = runToFailure (workflow (new Edge (START , dispatcher )));
354+ Context context = run (workflow (new Edge (START , dispatcher )));
308355
309- assertThat (failure ). isInstanceOf ( IllegalStateException . class );
310- assertThat (failure ). hasMessageThat (). contains ( "delegated its output " );
356+ assertThat (context . requireNodeState (). failure ()). isEmpty ( );
357+ assertThat (context . requireNodeState (). output ()). hasValue ( "CHILD_OUT " );
311358 assertThat (eventAt ("wf@1/dispatcher@1/child@1" ).output ()).hasValue ("CHILD_OUT" );
359+ assertThat (outputs ()).doesNotContain ("FALLBACK" );
312360 }
313361
314362 @ Test
315- public void runChild_delegateThatFailsAfterEmittingItsOutput_failsACallerProducingItsOwn () {
363+ public void runChild_delegateThatFailsAfterEmittingItsOutput_dropsTheCallersFallback () {
316364 Node dispatcher =
317365 handling (
318366 "dispatcher" ,
@@ -321,22 +369,24 @@ public void runChild_delegateThatFailsAfterEmittingItsOutput_failsACallerProduci
321369 DynamicNodeFailedException .class ,
322370 "FALLBACK" );
323371
324- Exception failure = runToFailure (workflow (new Edge (START , dispatcher )));
372+ Context context = run (workflow (new Edge (START , dispatcher )));
325373
326- assertThat (failure ).isInstanceOf (IllegalStateException .class );
327- assertThat (failure ).hasMessageThat ().contains ("delegated its output" );
374+ assertThat (context .requireNodeState ().failure ()).isEmpty ();
375+ assertThat (context .requireNodeState ().output ()).hasValue ("CHILD_OUT" );
376+ assertThat (outputs ()).doesNotContain ("FALLBACK" );
328377 }
329378
330379 @ Test
331- public void runChild_delegateWorkflowFailingAfterItsOutput_failsACallerProducingItsOwn () {
380+ public void runChild_delegateWorkflowFailingAfterItsOutput_dropsTheCallersFallback () {
332381 Workflow inner = workflow ("inner_wf" , new Edge (START , emitThenFail ("term" , "CHILD_OUT" )));
333382 Node dispatcher =
334383 handling ("dispatcher" , inner , USE_AS_OUTPUT , DynamicNodeFailedException .class , "FALLBACK" );
335384
336- Exception failure = runToFailure (workflow (new Edge (START , dispatcher )));
385+ Context context = run (workflow (new Edge (START , dispatcher )));
337386
338- assertThat (failure ).isInstanceOf (IllegalStateException .class );
339- assertThat (failure ).hasMessageThat ().contains ("delegated its output" );
387+ assertThat (context .requireNodeState ().failure ()).isEmpty ();
388+ assertThat (context .requireNodeState ().output ()).hasValue ("CHILD_OUT" );
389+ assertThat (outputs ()).doesNotContain ("FALLBACK" );
340390 }
341391
342392 @ Test
0 commit comments