From 5425d808a0f40a0739f26dd0904fd462bbea28c0 Mon Sep 17 00:00:00 2001 From: Matheus Andre Date: Sat, 4 Apr 2026 22:18:25 -0300 Subject: [PATCH 1/2] fix: Exception on loop after agent call Signed-off-by: Matheus Andre Signed-off-by: Matheus Andre --- .../func/JavaForExecutorBuilder.java | 2 +- .../func/ForTaskFunctionRegressionTest.java | 60 +++++++++++++++++++ .../api/types/func/ForTaskFunction.java | 6 +- 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java index 0d07bbdb..e28a4d77 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java @@ -69,7 +69,7 @@ protected WorkflowValueResolver> buildCollectionFilter() { } private Object collectionFilterObject(ForTaskFunction taskFunctions) { - return taskFunctions.getForClass().isPresent() + return taskFunctions.getForClass() != null && taskFunctions.getForClass().isPresent() ? new TypedFunction( taskFunctions.getCollection(), taskFunctions.getForClass().orElseThrow()) : taskFunctions.getCollection(); diff --git a/experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java b/experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java new file mode 100644 index 00000000..4c52be94 --- /dev/null +++ b/experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverless.workflow.impl.executors.func; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.impl.WorkflowApplication; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.Test; + +class ForTaskFunctionRegressionTest { + + @Test + void initializesOptionalFieldsAsEmpty() { + ForTaskFunction taskFunction = new ForTaskFunction(); + + assertThat(taskFunction.getWhileClass()).isNotNull().isEmpty(); + assertThat(taskFunction.getItemClass()).isNotNull().isEmpty(); + assertThat(taskFunction.getForClass()).isNotNull().isEmpty(); + } + + @Test + void forLoopWithoutExplicitCollectionClassExecutesSuccessfully() + throws InterruptedException, ExecutionException { + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + Workflow workflow = + FuncWorkflowBuilder.workflow("loop-without-collection-class") + .tasks( + tasks -> + tasks.forEach( + f -> + f.whileC(CallTest::isEven) + .collection(v -> (Collection) v) + .tasks(CallTest::sum))) + .build(); + + var result = app.workflowDefinition(workflow).instance(List.of(2, 4, 6)).start().get(); + + assertThat(result.asNumber().orElseThrow()).isEqualTo(12); + } + } +} diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java index 9b88fd01..56572f03 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java @@ -24,9 +24,9 @@ public class ForTaskFunction extends ForTask { private static final long serialVersionUID = 1L; private LoopPredicateIndexFilter whilePredicate; - private Optional> whileClass; - private Optional> itemClass; - private Optional> forClass; + private Optional> whileClass = Optional.empty(); + private Optional> itemClass = Optional.empty(); + private Optional> forClass = Optional.empty(); private Function> collection; public ForTaskFunction withWhile(LoopPredicate whilePredicate) { From 459f8dc0e94643e9fcf52d49e58e5f4bb001ba48 Mon Sep 17 00:00:00 2001 From: Matheus Andre Date: Tue, 7 Apr 2026 13:12:28 -0300 Subject: [PATCH 2/2] fix: normalize optional fields in ForTaskFunction and enhance serialization tests Signed-off-by: Matheus Andre --- .../func/JavaForExecutorBuilder.java | 9 +- .../func/ForTaskFunctionRegressionTest.java | 84 ++++++++++++++++--- .../api/types/func/ForTaskFunction.java | 27 +++++- 3 files changed, 103 insertions(+), 17 deletions(-) diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java index e28a4d77..b7deee0f 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/executors/func/JavaForExecutorBuilder.java @@ -69,9 +69,10 @@ protected WorkflowValueResolver> buildCollectionFilter() { } private Object collectionFilterObject(ForTaskFunction taskFunctions) { - return taskFunctions.getForClass() != null && taskFunctions.getForClass().isPresent() - ? new TypedFunction( - taskFunctions.getCollection(), taskFunctions.getForClass().orElseThrow()) - : taskFunctions.getCollection(); + return taskFunctions + .getForClass() + .map( + forClass -> (Object) new TypedFunction(taskFunctions.getCollection(), (Class) forClass)) + .orElse(taskFunctions.getCollection()); } } diff --git a/experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java b/experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java index 4c52be94..e000f498 100644 --- a/experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java +++ b/experimental/lambda/src/test/java/io/serverless/workflow/impl/executors/func/ForTaskFunctionRegressionTest.java @@ -17,10 +17,20 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.serverlessworkflow.api.types.Document; +import io.serverlessworkflow.api.types.ForTaskConfiguration; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.CallJava; +import io.serverlessworkflow.api.types.func.CallTaskJava; import io.serverlessworkflow.api.types.func.ForTaskFunction; -import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; import io.serverlessworkflow.impl.WorkflowApplication; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutionException; @@ -38,23 +48,75 @@ void initializesOptionalFieldsAsEmpty() { } @Test - void forLoopWithoutExplicitCollectionClassExecutesSuccessfully() + void optionalFieldsSurviveJavaSerializationRoundTrip() throws Exception { + ForTaskFunction taskFunction = new ForTaskFunction(); + clearField(taskFunction, "whileClass"); + clearField(taskFunction, "itemClass"); + clearField(taskFunction, "forClass"); + clearField(taskFunction, "collection"); + + ForTaskFunction copy = roundTrip(taskFunction); + + assertThat(copy.getWhileClass()).isNotNull().isEmpty(); + assertThat(copy.getItemClass()).isNotNull().isEmpty(); + assertThat(copy.getForClass()).isNotNull().isEmpty(); + } + + @Test + void forLoopWithExplicitCollectionClassExecutesSuccessfully() throws InterruptedException, ExecutionException { try (WorkflowApplication app = WorkflowApplication.builder().build()) { + ForTaskConfiguration forConfig = new ForTaskConfiguration(); Workflow workflow = - FuncWorkflowBuilder.workflow("loop-without-collection-class") - .tasks( - tasks -> - tasks.forEach( - f -> - f.whileC(CallTest::isEven) - .collection(v -> (Collection) v) - .tasks(CallTest::sum))) - .build(); + new Workflow() + .withDocument( + new Document() + .withNamespace("test") + .withName("loop-with-collection-class") + .withVersion("1.0")) + .withDo( + List.of( + new TaskItem( + "forLoop", + new Task() + .withForTask( + new ForTaskFunction() + .withWhile(CallTest::isEven) + .withCollection(v -> v, Collection.class) + .withFor(forConfig) + .withDo( + List.of( + new TaskItem( + "javaCall", + new Task() + .withCallTask( + new CallTaskJava( + CallJava.loopFunction( + CallTest::sum, + forConfig.getEach())))))))))); var result = app.workflowDefinition(workflow).instance(List.of(2, 4, 6)).start().get(); assertThat(result.asNumber().orElseThrow()).isEqualTo(12); } } + + private static ForTaskFunction roundTrip(ForTaskFunction taskFunction) throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(output)) { + oos.writeObject(taskFunction); + } + + try (ObjectInputStream ois = + new ObjectInputStream(new ByteArrayInputStream(output.toByteArray()))) { + return (ForTaskFunction) ois.readObject(); + } + } + + private static void clearField(Object target, String fieldName) + throws ReflectiveOperationException { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, null); + } } diff --git a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java index 56572f03..05470d3d 100644 --- a/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java +++ b/experimental/types/src/main/java/io/serverlessworkflow/api/types/func/ForTaskFunction.java @@ -16,6 +16,8 @@ package io.serverlessworkflow.api.types.func; import io.serverlessworkflow.api.types.ForTask; +import java.io.IOException; +import java.io.ObjectInputStream; import java.util.Collection; import java.util.Optional; import java.util.function.Function; @@ -29,6 +31,10 @@ public class ForTaskFunction extends ForTask { private Optional> forClass = Optional.empty(); private Function> collection; + public ForTaskFunction() { + normalizeOptionalFields(); + } + public ForTaskFunction withWhile(LoopPredicate whilePredicate) { return withWhile(toPredicate(whilePredicate)); } @@ -112,8 +118,8 @@ private ForTaskFunction withWhile( Optional> modelClass, Optional> itemClass) { this.whilePredicate = whilePredicate; - this.whileClass = modelClass; - this.itemClass = itemClass; + this.whileClass = modelClass != null ? modelClass : Optional.empty(); + this.itemClass = itemClass != null ? itemClass : Optional.empty(); return this; } @@ -147,4 +153,21 @@ public Optional> getItemClass() { public Function> getCollection() { return collection; } + + private void normalizeOptionalFields() { + if (whileClass == null) { + whileClass = Optional.empty(); + } + if (itemClass == null) { + itemClass = Optional.empty(); + } + if (forClass == null) { + forClass = Optional.empty(); + } + } + + private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { + input.defaultReadObject(); + normalizeOptionalFields(); + } }