From fe76e2242239149258e5207b28abc664b290dfc9 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 11 Jun 2026 16:20:11 -0400 Subject: [PATCH 1/8] Fix #1449 - Resolve duration expressions; Improve wait DSL contract Signed-off-by: Ricardo Zanini --- .../plans/2026-06-11-waittask-ergonomics.md | 1099 +++++++++++++++++ .../2026-06-11-waittask-ergonomics-design.md | 161 +++ .../fluent/func/FuncTaskItemListBuilder.java | 14 + .../fluent/func/dsl/FuncDSL.java | 216 ++++ .../fluent/func/FuncDSLWaitTest.java | 197 +++ .../fluent/spec/dsl/DSL.java | 160 +++ .../fluent/spec/dsl/DSLWaitTest.java | 196 +++ .../impl/executors/WaitExecutor.java | 83 +- .../impl/test/WaitExecutorTest.java | 303 +++++ .../wait-expression-context.yaml | 11 + .../wait-expression-input.yaml | 8 + 11 files changed, 2434 insertions(+), 14 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-11-waittask-ergonomics.md create mode 100644 docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md create mode 100644 experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java create mode 100644 fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java create mode 100644 impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java create mode 100644 impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml create mode 100644 impl/test/src/test/resources/workflows-samples/wait-expression-input.yaml diff --git a/docs/superpowers/plans/2026-06-11-waittask-ergonomics.md b/docs/superpowers/plans/2026-06-11-waittask-ergonomics.md new file mode 100644 index 000000000..bdd5dd452 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-waittask-ergonomics.md @@ -0,0 +1,1099 @@ +# WaitTask Ergonomics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add ergonomic convenience methods for wait tasks to DSL and FuncDSL, matching the existing timeout* pattern. + +**Architecture:** Add static helper methods that delegate to existing wait builder infrastructure. Methods follow the established pattern of returning TasksConfigurer/FuncTaskConfigurer and using inline duration builders. + +**Tech Stack:** Java, JUnit 5, AssertJ + +--- + +## File Structure + +**Files to modify:** +- `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` - Add wait convenience methods +- `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` - Add wait convenience methods and basic wait support + +**Test files to create:** +- `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` - Comprehensive tests for DSL wait methods +- `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` - Comprehensive tests for FuncDSL wait methods + +--- + +### Task 1: Add DSL waitSeconds and waitMinutes methods with tests + +**Files:** +- Modify: `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` +- Create: `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` + +- [ ] **Step 1: Write failing test for waitSeconds** + +Create `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java`: + +```java +/* + * 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.serverlessworkflow.fluent.spec.dsl; + +import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitSeconds; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.DurationInline; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +public class DSLWaitTest { + + @Test + public void when_wait_seconds_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(waitSeconds(30)) + .build(); + + assertThat(wf.getDo()).hasSize(1); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getSeconds()).isEqualTo(30); + assertThat(inline.getMinutes()).isZero(); + assertThat(inline.getHours()).isZero(); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn test -Dtest=DSLWaitTest#when_wait_seconds_unnamed -pl fluent/spec` + +Expected: FAIL with "cannot resolve method 'waitSeconds'" + +- [ ] **Step 3: Implement waitSeconds methods in DSL.java** + +Add to `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` after the existing wait methods (around line 871): + +```java + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with seconds. + * + *

Example: {@code tasks(waitSeconds(30))} + * + * @param seconds wait duration in seconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitSeconds(int seconds) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with seconds. + * + *

Example: {@code tasks(waitSeconds("pause", 30))} + * + * @param name task name + * @param seconds wait duration in seconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitSeconds(String name, int seconds) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with minutes. + * + *

Example: {@code tasks(waitMinutes(5))} + * + * @param minutes wait duration in minutes + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMinutes(int minutes) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with minutes. + * + *

Example: {@code tasks(waitMinutes("pause", 5))} + * + * @param name task name + * @param minutes wait duration in minutes + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMinutes(String name, int minutes) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } +``` + +- [ ] **Step 4: Add test for named waitSeconds** + +Add to `DSLWaitTest.java`: + +```java + @Test + public void when_wait_seconds_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(waitSeconds("pause", 45)) + .build(); + + assertThat(wf.getDo()).hasSize(1); + assertThat(wf.getDo().get(0).getName()).isEqualTo("pause"); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getSeconds()).isEqualTo(45); + } +``` + +- [ ] **Step 5: Add test for waitMinutes** + +Add to `DSLWaitTest.java`: + +```java + @Test + public void when_wait_minutes_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(waitMinutes(10)) + .build(); + + assertThat(wf.getDo()).hasSize(1); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getMinutes()).isEqualTo(10); + assertThat(inline.getSeconds()).isZero(); + } + + @Test + public void when_wait_minutes_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(waitMinutes("delay", 15)) + .build(); + + assertThat(wf.getDo()).hasSize(1); + assertThat(wf.getDo().get(0).getName()).isEqualTo("delay"); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getMinutes()).isEqualTo(15); + } +``` + +- [ ] **Step 6: Run all tests to verify they pass** + +Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` + +Expected: All 4 tests PASS + +- [ ] **Step 7: Commit** + +```bash +git add fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java \ + fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java +git commit -m "feat: add waitSeconds and waitMinutes convenience methods to DSL" +``` + +--- + +### Task 2: Add DSL waitHours, waitDays, and waitMillis methods + +**Files:** +- Modify: `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` +- Modify: `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` + +- [ ] **Step 1: Write failing tests** + +Add to `DSLWaitTest.java`: + +```java + @Test + public void when_wait_hours_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitHours(2)) + .build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getHours()).isEqualTo(2); + assertThat(inline.getMinutes()).isZero(); + } + + @Test + public void when_wait_hours_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitHours("longPause", 3)) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("longPause"); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask.getWait().getDurationInline().getHours()).isEqualTo(3); + } + + @Test + public void when_wait_days_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitDays(1)) + .build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getDays()).isEqualTo(1); + assertThat(inline.getHours()).isZero(); + } + + @Test + public void when_wait_days_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitDays("dailyDelay", 5)) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("dailyDelay"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()) + .isEqualTo(5); + } + + @Test + public void when_wait_millis_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitMillis(500)) + .build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getMilliseconds()).isEqualTo(500); + assertThat(inline.getSeconds()).isZero(); + } + + @Test + public void when_wait_millis_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitMillis("shortPause", 250)) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("shortPause"); + assertThat( + wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getMilliseconds()) + .isEqualTo(250); + } +``` + +Add import at top of file: +```java +import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitDays; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitHours; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitMillis; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitMinutes; +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` + +Expected: FAIL with "cannot resolve method" errors + +- [ ] **Step 3: Implement waitHours, waitDays, and waitMillis** + +Add to `DSL.java` after the waitMinutes methods: + +```java + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours(2))} + * + * @param hours wait duration in hours + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitHours(int hours) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours("longPause", 2))} + * + * @param name task name + * @param hours wait duration in hours + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitHours(String name, int hours) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays(1))} + * + * @param days wait duration in days + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitDays(int days) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays("dailyDelay", 1))} + * + * @param name task name + * @param days wait duration in days + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitDays(String name, int days) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with milliseconds. + * + *

Example: {@code tasks(waitMillis(500))} + * + * @param milliseconds wait duration in milliseconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMillis(int milliseconds) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with + * milliseconds. + * + *

Example: {@code tasks(waitMillis("shortPause", 500))} + * + * @param name task name + * @param milliseconds wait duration in milliseconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMillis(String name, int milliseconds) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` + +Expected: All 10 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java \ + fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java +git commit -m "feat: add waitHours, waitDays, and waitMillis convenience methods to DSL" +``` + +--- + +### Task 3: Add DSL wait(Duration) method + +**Files:** +- Modify: `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` +- Modify: `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` + +- [ ] **Step 1: Write failing test** + +Add to `DSLWaitTest.java`: + +```java + @Test + public void when_wait_with_duration_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30))) + .build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getMinutes()).isEqualTo(5); + assertThat(inline.getSeconds()).isEqualTo(30); + } + + @Test + public void when_wait_with_duration_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.wait("customDelay", java.time.Duration.ofHours(1).plusMinutes(15))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("customDelay"); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getHours()).isEqualTo(1); + assertThat(inline.getMinutes()).isEqualTo(15); + } +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn test -Dtest=DSLWaitTest#when_wait_with_duration_unnamed -pl fluent/spec` + +Expected: FAIL (ambiguous method call or compile error) + +- [ ] **Step 3: Implement wait(Duration) methods** + +Add to `DSL.java` after the waitMillis methods: + +```java + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with a Java {@link + * java.time.Duration}. + * + *

The Duration is converted to DurationInline format with days, hours, minutes, seconds, and + * milliseconds. + * + *

Example: {@code tasks(wait(Duration.ofMinutes(5).plusSeconds(30)))} + * + * @param duration wait duration as a Java Duration + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer wait(java.time.Duration duration) { + return list -> list.wait(w -> w.wait(duration)); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with a Java + * {@link java.time.Duration}. + * + *

The Duration is converted to DurationInline format with days, hours, minutes, seconds, and + * milliseconds. + * + *

Example: {@code tasks(wait("pause", Duration.ofMinutes(5).plusSeconds(30)))} + * + * @param name task name + * @param duration wait duration as a Java Duration + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer wait(String name, java.time.Duration duration) { + return list -> list.wait(name, w -> w.wait(duration)); + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` + +Expected: All 12 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java \ + fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java +git commit -m "feat: add wait(Duration) convenience method to DSL" +``` + +--- + +### Task 4: Add FuncDSL basic wait support + +**Files:** +- Modify: `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` +- Create: `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` + +- [ ] **Step 1: Write failing test for basic wait** + +Create `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java`: + +```java +/* + * 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.serverlessworkflow.fluent.func; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.tasks; +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.wait; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutSeconds; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Workflow; +import org.junit.jupiter.api.Test; + +public class FuncDSLWaitTest { + + @Test + public void when_wait_with_string_expression() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(wait("PT5S"))) + .build(); + + assertThat(wf.getDo()).hasSize(1); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + assertThat(waitTask.getWait().get()).isEqualTo("PT5S"); + } + + @Test + public void when_wait_with_timeout_builder() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(wait(timeoutSeconds(10)))) + .build(); + + assertThat(wf.getDo()).hasSize(1); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + assertThat(waitTask.getWait().getDurationInline().getSeconds()).isEqualTo(10); + } + + @Test + public void when_wait_named_with_string() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(wait("pause", "PT15S"))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("pause"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().get()).isEqualTo("PT15S"); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` + +Expected: FAIL with "cannot resolve method 'wait'" + +- [ ] **Step 3: Implement basic wait methods in FuncDSL** + +Add to `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` in the appropriate section (after similar task methods, around line 1088): + +```java + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with a duration + * expression. + * + *

Example: {@code tasks(wait("PT5M"))} + * + * @param durationExpression duration expression or ISO 8601 literal + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String durationExpression) { + return list -> list.wait(w -> w.wait(durationExpression)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with a + * duration expression. + * + *

Example: {@code tasks(wait("pause", "PT5M"))} + * + * @param name task name + * @param durationExpression duration expression or ISO 8601 literal + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String name, String durationExpression) { + return list -> list.wait(name, w -> w.wait(durationExpression)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with an inline + * duration builder. + * + *

Example: {@code tasks(wait(timeoutSeconds(30)))} + * + * @param duration timeout builder consumer + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(Consumer duration) { + return list -> list.wait(w -> w.wait(duration)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with an + * inline duration builder. + * + *

Example: {@code tasks(wait("pause", timeoutSeconds(30)))} + * + * @param name task name + * @param duration timeout builder consumer + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String name, Consumer duration) { + return list -> list.wait(name, w -> w.wait(duration)); + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` + +Expected: All 3 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java \ + experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java +git commit -m "feat: add basic wait methods to FuncDSL" +``` + +--- + +### Task 5: Add FuncDSL waitSeconds and waitMinutes convenience methods + +**Files:** +- Modify: `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` +- Modify: `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` + +- [ ] **Step 1: Write failing tests** + +Add to `FuncDSLWaitTest.java`: + +```java + @Test + public void when_wait_seconds_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitSeconds(30))) + .build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + var inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getSeconds()).isEqualTo(30); + assertThat(inline.getMinutes()).isZero(); + } + + @Test + public void when_wait_seconds_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitSeconds("pause", 45))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("pause"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getSeconds()) + .isEqualTo(45); + } + + @Test + public void when_wait_minutes_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitMinutes(10))) + .build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertThat(inline.getMinutes()).isEqualTo(10); + assertThat(inline.getSeconds()).isZero(); + } + + @Test + public void when_wait_minutes_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitMinutes("delay", 15))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("delay"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getMinutes()) + .isEqualTo(15); + } +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` + +Expected: FAIL with "cannot resolve method" errors + +- [ ] **Step 3: Implement waitSeconds and waitMinutes** + +Add to `FuncDSL.java` after the basic wait methods: + +```java + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with seconds. + * + *

Example: {@code tasks(waitSeconds(30))} + * + * @param seconds wait duration in seconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitSeconds(int seconds) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with + * seconds. + * + *

Example: {@code tasks(waitSeconds("pause", 30))} + * + * @param name task name + * @param seconds wait duration in seconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitSeconds(String name, int seconds) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with minutes. + * + *

Example: {@code tasks(waitMinutes(5))} + * + * @param minutes wait duration in minutes + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMinutes(int minutes) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with + * minutes. + * + *

Example: {@code tasks(waitMinutes("pause", 5))} + * + * @param name task name + * @param minutes wait duration in minutes + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMinutes(String name, int minutes) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` + +Expected: All 7 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java \ + experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java +git commit -m "feat: add waitSeconds and waitMinutes to FuncDSL" +``` + +--- + +### Task 6: Add remaining FuncDSL convenience methods + +**Files:** +- Modify: `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` +- Modify: `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` + +- [ ] **Step 1: Write failing tests** + +Add to `FuncDSLWaitTest.java`: + +```java + @Test + public void when_wait_hours_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitHours(2))) + .build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertThat(inline.getHours()).isEqualTo(2); + } + + @Test + public void when_wait_hours_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitHours("longPause", 3))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("longPause"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getHours()) + .isEqualTo(3); + } + + @Test + public void when_wait_days_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitDays(1))) + .build(); + + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()) + .isEqualTo(1); + } + + @Test + public void when_wait_days_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitDays("dailyDelay", 5))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("dailyDelay"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()) + .isEqualTo(5); + } + + @Test + public void when_wait_millis_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitMillis(500))) + .build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertThat(inline.getMilliseconds()).isEqualTo(500); + } + + @Test + public void when_wait_millis_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitMillis("shortPause", 250))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("shortPause"); + assertThat( + wf.getDo() + .get(0) + .getTask() + .getWaitTask() + .getWait() + .getDurationInline() + .getMilliseconds()) + .isEqualTo(250); + } + + @Test + public void when_wait_with_duration() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30)))) + .build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertThat(inline.getMinutes()).isEqualTo(5); + assertThat(inline.getSeconds()).isEqualTo(30); + } + + @Test + public void when_wait_with_duration_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.wait("custom", java.time.Duration.ofHours(1)))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("custom"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getHours()) + .isEqualTo(1); + } +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` + +Expected: FAIL with "cannot resolve method" errors + +- [ ] **Step 3: Implement waitHours, waitDays, waitMillis, and wait(Duration)** + +Add to `FuncDSL.java` after the waitMinutes methods: + +```java + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours(2))} + * + * @param hours wait duration in hours + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitHours(int hours) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours("longPause", 2))} + * + * @param name task name + * @param hours wait duration in hours + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitHours(String name, int hours) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays(1))} + * + * @param days wait duration in days + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitDays(int days) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays("dailyDelay", 1))} + * + * @param name task name + * @param days wait duration in days + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitDays(String name, int days) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with + * milliseconds. + * + *

Example: {@code tasks(waitMillis(500))} + * + * @param milliseconds wait duration in milliseconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMillis(int milliseconds) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with + * milliseconds. + * + *

Example: {@code tasks(waitMillis("shortPause", 500))} + * + * @param name task name + * @param milliseconds wait duration in milliseconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMillis(String name, int milliseconds) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with a Java + * {@link java.time.Duration}. + * + *

The Duration is converted to DurationInline format. + * + *

Example: {@code tasks(wait(Duration.ofMinutes(5).plusSeconds(30)))} + * + * @param duration wait duration as a Java Duration + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(java.time.Duration duration) { + return list -> list.wait(w -> w.wait(duration)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with a Java + * {@link java.time.Duration}. + * + *

The Duration is converted to DurationInline format. + * + *

Example: {@code tasks(wait("pause", Duration.ofMinutes(5)))} + * + * @param name task name + * @param duration wait duration as a Java Duration + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String name, java.time.Duration duration) { + return list -> list.wait(name, w -> w.wait(duration)); + } +``` + +- [ ] **Step 4: Run all tests to verify they pass** + +Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` + +Expected: All 15 tests PASS + +- [ ] **Step 5: Run all fluent tests to ensure no regressions** + +Run: `mvn test -pl fluent/spec,experimental/fluent/func` + +Expected: All tests PASS + +- [ ] **Step 6: Commit** + +```bash +git add experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java \ + experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java +git commit -m "feat: add waitHours, waitDays, waitMillis, and wait(Duration) to FuncDSL" +``` + +--- + +## Summary + +This plan implements ergonomic wait convenience methods for both DSL and FuncDSL: + +**DSL.java additions:** +- `waitSeconds(int)` / `waitSeconds(String, int)` +- `waitMinutes(int)` / `waitMinutes(String, int)` +- `waitHours(int)` / `waitHours(String, int)` +- `waitDays(int)` / `waitDays(String, int)` +- `waitMillis(int)` / `waitMillis(String, int)` +- `wait(Duration)` / `wait(String, Duration)` + +**FuncDSL.java additions:** +- All basic wait methods (string expression, TimeoutBuilder consumer) +- All convenience methods from DSL (matching signatures but returning FuncTaskConfigurer) + +**Test Coverage:** +- 12 tests for DSL wait methods +- 15 tests for FuncDSL wait methods +- Tests verify both named and unnamed variants +- Tests verify Duration conversion to DurationInline diff --git a/docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md b/docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md new file mode 100644 index 000000000..f3be236e2 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md @@ -0,0 +1,161 @@ +# WaitTask Ergonomics and Executor Fix + +**Date**: 2026-06-11 +**Status**: Approved + +## Overview + +Improve the WaitTask API by adding ergonomic convenience methods to DSL/FuncDSL and fix the WaitExecutor to properly handle all three duration formats (inline, literal, expression). + +## Problems + +1. DSL has ergonomic `timeout*` methods but no equivalent for wait tasks +2. FuncDSL has zero wait task support +3. WaitExecutor only handles `durationInline` and `durationExpression`, crashes on `durationLiteral` +4. WaitExecutor parses expressions at build time instead of evaluating at runtime + +## Solution + +### 1. DSL Convenience Methods + +Add to `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java`: + +**Unnamed variants:** +```java +public static TasksConfigurer waitDays(int days) +public static TasksConfigurer waitHours(int hours) +public static TasksConfigurer waitMinutes(int minutes) +public static TasksConfigurer waitSeconds(int seconds) +public static TasksConfigurer waitMillis(int milliseconds) +public static TasksConfigurer wait(Duration duration) +``` + +**Named variants:** +```java +public static TasksConfigurer waitDays(String name, int days) +public static TasksConfigurer waitHours(String name, int hours) +public static TasksConfigurer waitMinutes(String name, int minutes) +public static TasksConfigurer waitSeconds(String name, int seconds) +public static TasksConfigurer waitMillis(String name, int milliseconds) +public static TasksConfigurer wait(String name, Duration duration) +``` + +**Implementation pattern:** +```java +public static TasksConfigurer waitSeconds(int seconds) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); +} + +public static TasksConfigurer waitSeconds(String name, int seconds) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); +} +``` + +The `wait(Duration)` variant converts to `DurationInline` using the existing logic from `WaitTaskBuilder.wait(Duration)`. + +### 2. FuncDSL Wait Support + +Add to `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java`: + +All methods from section 1, but returning `FuncTaskConfigurer` instead of `TasksConfigurer`, plus the basic wait methods: + +```java +public static FuncTaskConfigurer wait(Consumer duration) +public static FuncTaskConfigurer wait(String name, Consumer duration) +public static FuncTaskConfigurer wait(String durationExpression) +public static FuncTaskConfigurer wait(String name, String durationExpression) +``` + +These delegate to `list.wait()` on the `FuncTaskItemListBuilder`, following the same pattern as other FuncDSL task methods. + +### 3. WaitExecutor Duration Handling + +Modify `impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java`: + +**Hybrid approach**: Validate static durations at build time, defer expressions to runtime. + +**Changes to WaitExecutorBuilder:** + +Add field: +```java +private final String runtimeExpression; +``` + +Update constructor logic: +```java +protected WaitExecutorBuilder( + WorkflowMutablePosition position, WaitTask task, WorkflowDefinition definition) { + super(position, task, definition); + + if (task.getWait().getDurationInline() != null) { + this.millisToWait = toLong(task.getWait().getDurationInline()); + this.runtimeExpression = null; + } else if (task.getWait().getDurationLiteral() != null) { + this.millisToWait = Duration.parse(task.getWait().getDurationLiteral()); + this.runtimeExpression = null; + } else if (task.getWait().getDurationExpression() != null) { + this.millisToWait = null; + this.runtimeExpression = task.getWait().getDurationExpression(); + } else { + throw new IllegalStateException("Wait task has no duration specified"); + } +} +``` + +**Changes to WaitExecutor:** + +Add field: +```java +private final String runtimeExpression; +``` + +Update constructor: +```java +protected WaitExecutor(WaitExecutorBuilder builder) { + super(builder); + this.millisToWait = builder.millisToWait; + this.runtimeExpression = builder.runtimeExpression; +} +``` + +Update `internalExecute()`: +```java +@Override +protected CompletableFuture internalExecute( + WorkflowContext workflow, TaskContext taskContext) { + ((WorkflowMutableInstance) workflow.instance()).status(WorkflowStatus.WAITING); + + Duration waitDuration; + if (runtimeExpression != null) { + // Evaluate expression at runtime using workflow/task context + String evaluatedExpression = evaluateExpression(runtimeExpression, workflow, taskContext); + waitDuration = Duration.parse(evaluatedExpression); + } else { + waitDuration = millisToWait; + } + + return new CompletableFuture() + .completeOnTimeout(taskContext.output(), waitDuration.toMillis(), TimeUnit.MILLISECONDS) + .thenApply(this::complete); +} +``` + +Note: The `evaluateExpression()` method will need to be implemented or use existing expression evaluation utilities from the workflow context. + +## Benefits + +- Consistent API between timeout and wait methods +- Better developer experience with concise method calls +- Full support for all three duration formats +- Early validation for static durations +- Proper runtime evaluation for dynamic expressions +- FuncDSL users can now use wait tasks + +## Testing Considerations + +- Test all convenience methods (days, hours, minutes, seconds, millis) +- Test `wait(Duration)` conversion +- Test WaitExecutor with inline, literal, and expression durations +- Test runtime expression evaluation with various workflow contexts +- Verify build-time validation catches invalid static durations +- Verify runtime errors for invalid expression results diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java index e283735be..ae2bee648 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java @@ -215,4 +215,18 @@ public FuncTaskItemListBuilder tryCatch( itemsConfigurer.accept(tryTaskBuilder); return this.addTaskItem(new TaskItem(name, new Task().withTryTask(tryTaskBuilder.build()))); } + + public FuncTaskItemListBuilder wait( + Consumer itemsConfigurer) { + return wait(null, itemsConfigurer); + } + + public FuncTaskItemListBuilder wait( + String name, Consumer itemsConfigurer) { + name = this.defaultNameAndRequireConfig(name, itemsConfigurer, "wait"); + final io.serverlessworkflow.fluent.spec.WaitTaskBuilder waitTaskBuilder = + new io.serverlessworkflow.fluent.spec.WaitTaskBuilder(); + itemsConfigurer.accept(waitTaskBuilder); + return this.addTaskItem(new TaskItem(name, new Task().withWaitTask(waitTaskBuilder.build()))); + } } diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java index f06d8d90c..3f96a8fcf 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java @@ -50,6 +50,7 @@ import io.serverlessworkflow.impl.TaskContextData; import io.serverlessworkflow.impl.WorkflowContextData; import java.net.URI; +import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Map; @@ -1087,6 +1088,221 @@ public static FuncTaskConfigurer tryCatch(String name, Consumer list.tryCatch(name, configurer); } + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with a duration + * expression. + * + *

Example: {@code tasks(wait("PT5M"))} + * + * @param durationExpression duration expression or ISO 8601 literal + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String durationExpression) { + return taskList -> taskList.wait(w -> w.wait(durationExpression)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with a + * duration expression. + * + *

Example: {@code tasks(wait("pause", "PT5M"))} + * + * @param name task name + * @param durationExpression duration expression or ISO 8601 literal + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String name, String durationExpression) { + return taskList -> taskList.wait(name, w -> w.wait(durationExpression)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with an inline + * duration builder. + * + *

Example: {@code tasks(wait(timeoutSeconds(30)))} + * + * @param duration timeout builder consumer + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(Consumer duration) { + return taskList -> taskList.wait(w -> w.wait(duration)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with an + * inline duration builder. + * + *

Example: {@code tasks(wait("pause", timeoutSeconds(30)))} + * + * @param name task name + * @param duration timeout builder consumer + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String name, Consumer duration) { + return taskList -> taskList.wait(name, w -> w.wait(duration)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with seconds. + * + *

Example: {@code tasks(waitSeconds(30))} + * + * @param seconds wait duration in seconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitSeconds(int seconds) { + return taskList -> taskList.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with + * seconds. + * + *

Example: {@code tasks(waitSeconds("pause", 30))} + * + * @param name task name + * @param seconds wait duration in seconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitSeconds(String name, int seconds) { + return taskList -> taskList.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with minutes. + * + *

Example: {@code tasks(waitMinutes(5))} + * + * @param minutes wait duration in minutes + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMinutes(int minutes) { + return taskList -> taskList.wait(w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with + * minutes. + * + *

Example: {@code tasks(waitMinutes("pause", 5))} + * + * @param name task name + * @param minutes wait duration in minutes + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMinutes(String name, int minutes) { + return taskList -> taskList.wait(name, w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours(2))} + * + * @param hours wait duration in hours + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitHours(int hours) { + return taskList -> taskList.wait(w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours("longPause", 2))} + * + * @param name task name + * @param hours wait duration in hours + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitHours(String name, int hours) { + return taskList -> taskList.wait(name, w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays(1))} + * + * @param days wait duration in days + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitDays(int days) { + return taskList -> taskList.wait(w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays("dailyDelay", 1))} + * + * @param name task name + * @param days wait duration in days + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitDays(String name, int days) { + return taskList -> taskList.wait(name, w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with milliseconds. + * + *

Example: {@code tasks(waitMillis(500))} + * + * @param milliseconds wait duration in milliseconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMillis(int milliseconds) { + return taskList -> + taskList.wait(w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with + * milliseconds. + * + *

Example: {@code tasks(waitMillis("shortPause", 500))} + * + * @param name task name + * @param milliseconds wait duration in milliseconds + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer waitMillis(String name, int milliseconds) { + return taskList -> + taskList.wait(name, w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with a Java {@link + * java.time.Duration}. + * + *

The Duration is converted to DurationInline format. + * + *

Example: {@code tasks(wait(Duration.ofMinutes(5).plusSeconds(30)))} + * + * @param duration wait duration as a Java Duration + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(Duration duration) { + return taskList -> taskList.wait(w -> w.wait(duration)); + } + + /** + * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with a Java + * {@link java.time.Duration}. + * + *

The Duration is converted to DurationInline format. + * + *

Example: {@code tasks(wait("pause", Duration.ofMinutes(5)))} + * + * @param name task name + * @param duration wait duration as a Java Duration + * @return a {@link FuncTaskConfigurer} that adds a WaitTask + */ + public static FuncTaskConfigurer wait(String name, Duration duration) { + return taskList -> taskList.wait(name, w -> w.wait(duration)); + } + /** * Sugar for a single-case switch: if predicate matches, jump to {@code thenTask}. * diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java new file mode 100644 index 000000000..49d9c2b53 --- /dev/null +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java @@ -0,0 +1,197 @@ +/* + * 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.serverlessworkflow.fluent.func; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.tasks; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutSeconds; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.func.dsl.FuncDSL; +import org.junit.jupiter.api.Test; + +public class FuncDSLWaitTest { + + @Test + public void when_wait_with_string_expression() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow").tasks(tasks(FuncDSL.wait("PT5S"))).build(); + + assertEquals(1, wf.getDo().size()); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertNotNull(waitTask); + assertEquals("PT5S", waitTask.getWait().get()); + } + + @Test + public void when_wait_with_timeout_builder() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.wait(timeoutSeconds(10)))) + .build(); + + assertEquals(1, wf.getDo().size()); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertNotNull(waitTask); + assertEquals(10, waitTask.getWait().getDurationInline().getSeconds()); + } + + @Test + public void when_wait_named_with_string() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.wait("pause", "PT15S"))) + .build(); + + assertEquals("pause", wf.getDo().get(0).getName()); + assertEquals("PT15S", wf.getDo().get(0).getTask().getWaitTask().getWait().get()); + } + + @Test + public void when_wait_seconds_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow").tasks(tasks(FuncDSL.waitSeconds(30))).build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + var inline = waitTask.getWait().getDurationInline(); + assertNotNull(inline); + assertEquals(30, inline.getSeconds()); + assertEquals(0, inline.getMinutes()); + } + + @Test + public void when_wait_seconds_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitSeconds("pause", 45))) + .build(); + + assertEquals("pause", wf.getDo().get(0).getName()); + assertEquals( + 45, wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getSeconds()); + } + + @Test + public void when_wait_minutes_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow").tasks(tasks(FuncDSL.waitMinutes(10))).build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertEquals(10, inline.getMinutes()); + assertEquals(0, inline.getSeconds()); + } + + @Test + public void when_wait_minutes_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitMinutes("delay", 15))) + .build(); + + assertEquals("delay", wf.getDo().get(0).getName()); + assertEquals( + 15, wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getMinutes()); + } + + @Test + public void when_wait_hours_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow").tasks(tasks(FuncDSL.waitHours(2))).build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertEquals(2, inline.getHours()); + } + + @Test + public void when_wait_hours_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitHours("longPause", 3))) + .build(); + + assertEquals("longPause", wf.getDo().get(0).getName()); + assertEquals( + 3, wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getHours()); + } + + @Test + public void when_wait_days_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow").tasks(tasks(FuncDSL.waitDays(1))).build(); + + assertEquals( + 1, wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()); + } + + @Test + public void when_wait_days_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitDays("dailyDelay", 5))) + .build(); + + assertEquals("dailyDelay", wf.getDo().get(0).getName()); + assertEquals( + 5, wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()); + } + + @Test + public void when_wait_millis_unnamed() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow").tasks(tasks(FuncDSL.waitMillis(500))).build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertEquals(500, inline.getMilliseconds()); + } + + @Test + public void when_wait_millis_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.waitMillis("shortPause", 250))) + .build(); + + assertEquals("shortPause", wf.getDo().get(0).getName()); + assertEquals( + 250, + wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getMilliseconds()); + } + + @Test + public void when_wait_with_duration() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30)))) + .build(); + + var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); + assertEquals(5, inline.getMinutes()); + assertEquals(30, inline.getSeconds()); + } + + @Test + public void when_wait_with_duration_named() { + Workflow wf = + FuncWorkflowBuilder.workflow("waitFlow") + .tasks(tasks(FuncDSL.wait("custom", java.time.Duration.ofHours(1)))) + .build(); + + assertEquals("custom", wf.getDo().get(0).getName()); + assertEquals( + 1, wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getHours()); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java index 2dc7323e2..ab7ebea47 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java @@ -41,6 +41,7 @@ import io.serverlessworkflow.fluent.spec.configurers.WorkflowConfigurer; import io.serverlessworkflow.types.Errors; import java.net.URI; +import java.time.Duration; import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -871,6 +872,165 @@ public static TasksConfigurer wait(String name, String durationExpression) { return list -> list.wait(name, w -> w.wait(durationExpression)); } + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with seconds. + * + *

Example: {@code tasks(waitSeconds(30))} + * + * @param seconds wait duration in seconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitSeconds(int seconds) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with seconds. + * + *

Example: {@code tasks(waitSeconds("pause", 30))} + * + * @param name task name + * @param seconds wait duration in seconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitSeconds(String name, int seconds) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with minutes. + * + *

Example: {@code tasks(waitMinutes(5))} + * + * @param minutes wait duration in minutes + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMinutes(int minutes) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with minutes. + * + *

Example: {@code tasks(waitMinutes("pause", 5))} + * + * @param name task name + * @param minutes wait duration in minutes + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMinutes(String name, int minutes) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours(2))} + * + * @param hours wait duration in hours + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitHours(int hours) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with hours. + * + *

Example: {@code tasks(waitHours("longPause", 2))} + * + * @param name task name + * @param hours wait duration in hours + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitHours(String name, int hours) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.hours(hours)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays(1))} + * + * @param days wait duration in days + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitDays(int days) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with days. + * + *

Example: {@code tasks(waitDays("dailyDelay", 1))} + * + * @param name task name + * @param days wait duration in days + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitDays(String name, int days) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.days(days)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with milliseconds. + * + *

Example: {@code tasks(waitMillis(500))} + * + * @param milliseconds wait duration in milliseconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMillis(int milliseconds) { + return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with + * milliseconds. + * + *

Example: {@code tasks(waitMillis("shortPause", 500))} + * + * @param name task name + * @param milliseconds wait duration in milliseconds + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer waitMillis(String name, int milliseconds) { + return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); + } + + /** + * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with a Java {@link + * java.time.Duration}. + * + *

The Duration is converted to DurationInline format with days, hours, minutes, seconds, and + * milliseconds. + * + *

Example: {@code tasks(wait(Duration.ofMinutes(5).plusSeconds(30)))} + * + * @param duration wait duration as a Java Duration + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer wait(Duration duration) { + return list -> list.wait(w -> w.wait(duration)); + } + + /** + * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with a Java + * {@link java.time.Duration}. + * + *

The Duration is converted to DurationInline format with days, hours, minutes, seconds, and + * milliseconds. + * + *

Example: {@code tasks(wait("pause", Duration.ofMinutes(5).plusSeconds(30)))} + * + * @param name task name + * @param duration wait duration as a Java Duration + * @return a {@link TasksConfigurer} that adds a WaitTask + */ + public static TasksConfigurer wait(String name, Duration duration) { + return list -> list.wait(name, w -> w.wait(duration)); + } + /** * Create a {@link TasksConfigurer} that adds a {@code forEach} task. * diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java new file mode 100644 index 000000000..b8c221e07 --- /dev/null +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java @@ -0,0 +1,196 @@ +/* + * 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.serverlessworkflow.fluent.spec.dsl; + +import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitMinutes; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitSeconds; +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.DurationInline; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import org.junit.jupiter.api.Test; + +public class DSLWaitTest { + + @Test + public void when_wait_seconds_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0").tasks(waitSeconds(30)).build(); + + assertThat(wf.getDo()).hasSize(1); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getSeconds()).isEqualTo(30); + assertThat(inline.getMinutes()).isZero(); + assertThat(inline.getHours()).isZero(); + } + + @Test + public void when_wait_seconds_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(waitSeconds("pause", 45)) + .build(); + + assertThat(wf.getDo()).hasSize(1); + assertThat(wf.getDo().get(0).getName()).isEqualTo("pause"); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getSeconds()).isEqualTo(45); + } + + @Test + public void when_wait_minutes_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0").tasks(waitMinutes(10)).build(); + + assertThat(wf.getDo()).hasSize(1); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getMinutes()).isEqualTo(10); + assertThat(inline.getSeconds()).isZero(); + } + + @Test + public void when_wait_minutes_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(waitMinutes("delay", 15)) + .build(); + + assertThat(wf.getDo()).hasSize(1); + assertThat(wf.getDo().get(0).getName()).isEqualTo("delay"); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getMinutes()).isEqualTo(15); + } + + @Test + public void when_wait_hours_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0").tasks(DSL.waitHours(2)).build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getHours()).isEqualTo(2); + assertThat(inline.getMinutes()).isZero(); + } + + @Test + public void when_wait_hours_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitHours("longPause", 3)) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("longPause"); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask.getWait().getDurationInline().getHours()).isEqualTo(3); + } + + @Test + public void when_wait_days_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0").tasks(DSL.waitDays(1)).build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getDays()).isEqualTo(1); + assertThat(inline.getHours()).isZero(); + } + + @Test + public void when_wait_days_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitDays("dailyDelay", 5)) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("dailyDelay"); + assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()) + .isEqualTo(5); + } + + @Test + public void when_wait_millis_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0").tasks(DSL.waitMillis(500)).build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getMilliseconds()).isEqualTo(500); + assertThat(inline.getSeconds()).isZero(); + } + + @Test + public void when_wait_millis_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.waitMillis("shortPause", 250)) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("shortPause"); + assertThat( + wf.getDo() + .get(0) + .getTask() + .getWaitTask() + .getWait() + .getDurationInline() + .getMilliseconds()) + .isEqualTo(250); + } + + @Test + public void when_wait_with_duration_unnamed() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30))) + .build(); + + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline).isNotNull(); + assertThat(inline.getMinutes()).isEqualTo(5); + assertThat(inline.getSeconds()).isEqualTo(30); + } + + @Test + public void when_wait_with_duration_named() { + Workflow wf = + WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") + .tasks(DSL.wait("customDelay", java.time.Duration.ofHours(1).plusMinutes(15))) + .build(); + + assertThat(wf.getDo().get(0).getName()).isEqualTo("customDelay"); + var waitTask = wf.getDo().get(0).getTask().getWaitTask(); + DurationInline inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getHours()).isEqualTo(1); + assertThat(inline.getMinutes()).isEqualTo(15); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java index 40838dacf..a0ab5fe80 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java @@ -20,31 +20,40 @@ import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowFilter; import io.serverlessworkflow.impl.WorkflowModel; -import io.serverlessworkflow.impl.WorkflowMutableInstance; import io.serverlessworkflow.impl.WorkflowMutablePosition; import io.serverlessworkflow.impl.WorkflowStatus; +import io.serverlessworkflow.impl.WorkflowUtils; import java.time.Duration; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public class WaitExecutor extends RegularTaskExecutor { - private final Duration millisToWait; + private final Duration duration; + private final WorkflowFilter durationExpressionFilter; public static class WaitExecutorBuilder extends RegularTaskExecutorBuilder { - private final Duration millisToWait; + private Duration duration = null; + private WorkflowFilter durationExpressionFilter; protected WaitExecutorBuilder( WorkflowMutablePosition position, WaitTask task, WorkflowDefinition definition) { super(position, task, definition); - this.millisToWait = - task.getWait().getDurationInline() != null - ? toLong(task.getWait().getDurationInline()) - : Duration.parse(task.getWait().getDurationExpression()); + if (task.getWait().getDurationExpression() == null) { + this.duration = + task.getWait().getDurationInline() != null + ? toDuration(task.getWait().getDurationInline()) + : parseDurationLiteral(task.getWait().getDurationLiteral()); + } else { + this.durationExpressionFilter = + WorkflowUtils.buildWorkflowFilter(application, task.getWait().getDurationExpression()); + } } - private Duration toLong(DurationInline durationInline) { + private Duration toDuration(DurationInline durationInline) { return Duration.ofMillis(durationInline.getMilliseconds()) .plusSeconds(durationInline.getSeconds()) .plusMinutes(durationInline.getMinutes()) @@ -52,6 +61,24 @@ private Duration toLong(DurationInline durationInline) { .plusDays(durationInline.getDays()); } + private Duration parseDurationLiteral(String literal) { + if (!WorkflowUtils.isValid(literal)) { + throw new IllegalArgumentException( + "Wait task duration literal cannot be null or empty at position: " + + position.jsonPointer()); + } + try { + return Duration.parse(literal); + } catch (Exception e) { + throw new IllegalArgumentException( + "Invalid ISO 8601 duration literal '" + + literal + + "' at position: " + + position.jsonPointer(), + e); + } + } + @Override public WaitExecutor buildInstance() { return new WaitExecutor(this); @@ -60,19 +87,47 @@ public WaitExecutor buildInstance() { protected WaitExecutor(WaitExecutorBuilder builder) { super(builder); - this.millisToWait = builder.millisToWait; + this.duration = builder.duration; + this.durationExpressionFilter = builder.durationExpressionFilter; } @Override protected CompletableFuture internalExecute( WorkflowContext workflow, TaskContext taskContext) { - ((WorkflowMutableInstance) workflow.instance()).status(WorkflowStatus.WAITING); + workflow.instance().status(WorkflowStatus.WAITING); return new CompletableFuture() - .completeOnTimeout(taskContext.output(), millisToWait.toMillis(), TimeUnit.MILLISECONDS) - .thenApply(this::complete); + .completeOnTimeout( + taskContext.output(), + Objects.requireNonNullElseGet( + duration, () -> evaluateDurationExpression(workflow, taskContext)) + .toMillis(), + TimeUnit.MILLISECONDS); } - private WorkflowModel complete(WorkflowModel model) { - return model; + private Duration evaluateDurationExpression(WorkflowContext workflow, TaskContext taskContext) { + String durationString = + durationExpressionFilter + .apply(workflow, taskContext, taskContext.rawInput()) + .as(String.class) + .orElse(null); + + if (!WorkflowUtils.isValid(durationString)) { + throw new IllegalArgumentException( + "Wait duration expression evaluated to empty or null at task: " + + taskContext.position().jsonPointer() + + ". Expression must return a valid ISO 8601 duration string."); + } + + try { + return Duration.parse(durationString.trim()); + } catch (Exception e) { + throw new IllegalArgumentException( + "Wait duration expression returned invalid ISO 8601 duration '" + + durationString + + "' at task: " + + taskContext.position().jsonPointer() + + ". Expected format: PT[n]H[n]M[n]S (e.g., PT1H30M, PT5S)", + e); + } } } diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java new file mode 100644 index 000000000..f4585e523 --- /dev/null +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java @@ -0,0 +1,303 @@ +/* + * 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.serverlessworkflow.impl.test; + +import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import io.serverlessworkflow.fluent.spec.dsl.DSL; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowInstance; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowStatus; +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class WaitExecutorTest { + + private static WorkflowApplication appl; + + @BeforeAll + static void init() { + appl = WorkflowApplication.builder().build(); + } + + @AfterAll + static void tearDown() { + appl.close(); + } + + // ========== DurationInline Tests ========== + + @Test + void testWaitWithDurationInlineSeconds() { + Workflow workflow = + WorkflowBuilder.workflow("wait-inline-seconds", "test", "0.1.0") + .tasks(DSL.waitSeconds(1)) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1000); + } + + @Test + void testWaitWithDurationInlineMilliseconds() { + Workflow workflow = + WorkflowBuilder.workflow("wait-inline-millis", "test", "0.1.0") + .tasks(DSL.waitMillis(100)) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(100); + } + + @Test + void testWaitWithDurationInlineComposite() { + // Test composite duration with multiple components + Workflow workflow = + WorkflowBuilder.workflow("wait-inline-composite", "test", "0.1.0") + .tasks(DSL.wait(Duration.ofSeconds(1).plusMillis(500))) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1500); // 1 second + 500 milliseconds + } + + // ========== DurationLiteral Tests (via Duration.parse) ========== + + @Test + void testWaitWithDurationLiteralISO8601Seconds() { + Workflow workflow = + WorkflowBuilder.workflow("wait-literal-seconds", "test", "0.1.0") + .tasks(DSL.wait(Duration.parse("PT1S"))) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1000); + } + + @Test + void testWaitWithDurationLiteralISO8601Composite() { + // PT1.5S = 1 second 500 milliseconds (keep test fast) + Workflow workflow = + WorkflowBuilder.workflow("wait-literal-composite", "test", "0.1.0") + .tasks(DSL.wait(Duration.parse("PT1.5S"))) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1500); + } + + @Test + void testWaitWithDurationLiteralISO8601Milliseconds() { + // PT0.1S = 100 milliseconds + Workflow workflow = + WorkflowBuilder.workflow("wait-literal-millis", "test", "0.1.0") + .tasks(DSL.wait(Duration.parse("PT0.1S"))) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(100); + } + + // ========== DurationExpression Tests (via YAML) ========== + + @Test + void testWaitWithDurationExpressionFromInput() throws IOException { + Workflow workflow = readWorkflowFromClasspath("workflows-samples/wait-expression-input.yaml"); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = + appl.workflowDefinition(workflow).instance(Map.of("timeout", "PT1S")).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1000); + } + + @Test + void testWaitWithDurationExpressionFromContext() throws IOException { + Workflow workflow = readWorkflowFromClasspath("workflows-samples/wait-expression-context.yaml"); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(500); + } + + @Test + void testWaitWithDurationExpressionInvalidValue() throws IOException { + Workflow workflow = readWorkflowFromClasspath("workflows-samples/wait-expression-input.yaml"); + + assertThatThrownBy( + () -> + appl.workflowDefinition(workflow) + .instance(Map.of("timeout", "not-a-duration")) + .start() + .join()) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("invalid ISO 8601 duration"); + } + + @Test + void testWaitWithDurationExpressionMissingValue() throws IOException { + Workflow workflow = readWorkflowFromClasspath("workflows-samples/wait-expression-input.yaml"); + + // When the expression resolves to empty/null, we throw IllegalArgumentException with helpful + // message + assertThatThrownBy(() -> appl.workflowDefinition(workflow).instance(Map.of()).start().join()) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("duration 'null'"); + } + + // ========== Workflow Status Tests ========== + + @Test + void testWaitSetsWorkflowStatusToWaiting() { + Workflow workflow = + WorkflowBuilder.workflow("wait-status-waiting", "test", "0.1.0") + .tasks(DSL.waitSeconds(2)) + .build(); + + WorkflowInstance instance = appl.workflowDefinition(workflow).instance(Map.of()); + CompletableFuture future = instance.start(); + + await() + .pollDelay(Duration.ofMillis(5)) + .atMost(Duration.ofMillis(100)) + .until(() -> instance.status() == WorkflowStatus.WAITING); + + assertThat(instance.status()).isEqualTo(WorkflowStatus.WAITING); + + future.join(); + assertThat(instance.status()).isEqualTo(WorkflowStatus.COMPLETED); + } + + @Test + void testWaitWithSuspendAndResume() { + Workflow workflow = + WorkflowBuilder.workflow("wait-suspend-resume", "test", "0.1.0") + .tasks(DSL.waitSeconds(2)) + .build(); + + WorkflowInstance instance = appl.workflowDefinition(workflow).instance(Map.of()); + CompletableFuture future = instance.start(); + + await() + .pollDelay(Duration.ofMillis(5)) + .atMost(Duration.ofMillis(100)) + .until(() -> instance.status() == WorkflowStatus.WAITING); + + instance.suspend(); + assertThat(instance.status()).isEqualTo(WorkflowStatus.SUSPENDED); + + instance.resume(); + WorkflowModel model = future.join(); + assertThat(instance.status()).isEqualTo(WorkflowStatus.COMPLETED); + assertThat(model).isNotNull(); + } + + // ========== YAML Sample Test ========== + + @Test + void testWaitFromYamlSample() throws IOException { + Workflow workflow = readWorkflowFromClasspath("workflows-samples/wait-set.yaml"); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + assertThat(model).isNotNull(); + } + + // ========== Convenience Methods Tests ========== + + @Test + void testWaitSecondsConvenienceMethod() { + Workflow workflow = + WorkflowBuilder.workflow("wait-convenience-seconds", "test", "0.1.0") + .tasks(DSL.waitSeconds(1)) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1000); + } + + @Test + void testWaitMillisConvenienceMethod() { + Workflow workflow = + WorkflowBuilder.workflow("wait-convenience-millis", "test", "0.1.0") + .tasks(DSL.waitMillis(100)) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(100); + } + + @Test + void testWaitWithJavaDurationConvenienceMethod() { + Workflow workflow = + WorkflowBuilder.workflow("wait-convenience-duration", "test", "0.1.0") + .tasks(DSL.wait(Duration.ofSeconds(1).plusMillis(500))) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1500); + } +} diff --git a/impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml b/impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml new file mode 100644 index 000000000..f7cb43b8a --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: wait-expression-context + version: '0.1.0' +do: + - setDuration: + set: + waitTime: PT0.5S + - waitExpression: + wait: ${.waitTime} diff --git a/impl/test/src/test/resources/workflows-samples/wait-expression-input.yaml b/impl/test/src/test/resources/workflows-samples/wait-expression-input.yaml new file mode 100644 index 000000000..5fde79e5f --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/wait-expression-input.yaml @@ -0,0 +1,8 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: wait-expression-input + version: '0.1.0' +do: + - waitExpression: + wait: ${.timeout} From e276e015be5ba1df391628a2f50db924c141efcc Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 11 Jun 2026 16:42:04 -0400 Subject: [PATCH 2/8] Consider co-pilot review Signed-off-by: Ricardo Zanini --- .../impl/executors/WaitExecutor.java | 13 +++---- .../impl/test/WaitExecutorTest.java | 35 ++++++++----------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java index a0ab5fe80..00051c604 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java @@ -105,13 +105,10 @@ protected CompletableFuture internalExecute( } private Duration evaluateDurationExpression(WorkflowContext workflow, TaskContext taskContext) { - String durationString = - durationExpressionFilter - .apply(workflow, taskContext, taskContext.rawInput()) - .as(String.class) - .orElse(null); + final Object durationObject = + durationExpressionFilter.apply(workflow, taskContext, taskContext.input()).asJavaObject(); - if (!WorkflowUtils.isValid(durationString)) { + if (durationObject == null || !WorkflowUtils.isValid(durationObject.toString())) { throw new IllegalArgumentException( "Wait duration expression evaluated to empty or null at task: " + taskContext.position().jsonPointer() @@ -119,11 +116,11 @@ private Duration evaluateDurationExpression(WorkflowContext workflow, TaskContex } try { - return Duration.parse(durationString.trim()); + return Duration.parse(durationObject.toString().trim()); } catch (Exception e) { throw new IllegalArgumentException( "Wait duration expression returned invalid ISO 8601 duration '" - + durationString + + durationObject + "' at task: " + taskContext.position().jsonPointer() + ". Expected format: PT[n]H[n]M[n]S (e.g., PT1H30M, PT5S)", diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java index f4585e523..053b7ab10 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java @@ -195,7 +195,7 @@ void testWaitWithDurationExpressionMissingValue() throws IOException { // message assertThatThrownBy(() -> appl.workflowDefinition(workflow).instance(Map.of()).start().join()) .hasCauseInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("duration 'null'"); + .hasMessageContaining("evaluated to empty or null"); } // ========== Workflow Status Tests ========== @@ -204,7 +204,7 @@ void testWaitWithDurationExpressionMissingValue() throws IOException { void testWaitSetsWorkflowStatusToWaiting() { Workflow workflow = WorkflowBuilder.workflow("wait-status-waiting", "test", "0.1.0") - .tasks(DSL.waitSeconds(2)) + .tasks(DSL.waitMillis(500)) .build(); WorkflowInstance instance = appl.workflowDefinition(workflow).instance(Map.of()); @@ -225,7 +225,7 @@ void testWaitSetsWorkflowStatusToWaiting() { void testWaitWithSuspendAndResume() { Workflow workflow = WorkflowBuilder.workflow("wait-suspend-resume", "test", "0.1.0") - .tasks(DSL.waitSeconds(2)) + .tasks(DSL.waitMillis(500)) .build(); WorkflowInstance instance = appl.workflowDefinition(workflow).instance(Map.of()); @@ -263,12 +263,9 @@ void testWaitSecondsConvenienceMethod() { .tasks(DSL.waitSeconds(1)) .build(); - long startTime = System.currentTimeMillis(); - WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); - long elapsed = System.currentTimeMillis() - startTime; - - assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(1000); + var waitTask = workflow.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + assertThat(waitTask.getWait().getDurationInline().getSeconds()).isEqualTo(1); } @Test @@ -278,12 +275,9 @@ void testWaitMillisConvenienceMethod() { .tasks(DSL.waitMillis(100)) .build(); - long startTime = System.currentTimeMillis(); - WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); - long elapsed = System.currentTimeMillis() - startTime; - - assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(100); + var waitTask = workflow.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + assertThat(waitTask.getWait().getDurationInline().getMilliseconds()).isEqualTo(100); } @Test @@ -293,11 +287,10 @@ void testWaitWithJavaDurationConvenienceMethod() { .tasks(DSL.wait(Duration.ofSeconds(1).plusMillis(500))) .build(); - long startTime = System.currentTimeMillis(); - WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); - long elapsed = System.currentTimeMillis() - startTime; - - assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(1500); + var waitTask = workflow.getDo().get(0).getTask().getWaitTask(); + assertThat(waitTask).isNotNull(); + var inline = waitTask.getWait().getDurationInline(); + assertThat(inline.getSeconds()).isEqualTo(1); + assertThat(inline.getMilliseconds()).isEqualTo(500); } } From b97bd22f36e4ae4d8ae0f541b6451618817d50fd Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 11 Jun 2026 16:55:30 -0400 Subject: [PATCH 3/8] Remove leftovers Signed-off-by: Ricardo Zanini --- .../plans/2026-06-11-waittask-ergonomics.md | 1099 ----------------- .../2026-06-11-waittask-ergonomics-design.md | 161 --- 2 files changed, 1260 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-11-waittask-ergonomics.md delete mode 100644 docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md diff --git a/docs/superpowers/plans/2026-06-11-waittask-ergonomics.md b/docs/superpowers/plans/2026-06-11-waittask-ergonomics.md deleted file mode 100644 index bdd5dd452..000000000 --- a/docs/superpowers/plans/2026-06-11-waittask-ergonomics.md +++ /dev/null @@ -1,1099 +0,0 @@ -# WaitTask Ergonomics Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add ergonomic convenience methods for wait tasks to DSL and FuncDSL, matching the existing timeout* pattern. - -**Architecture:** Add static helper methods that delegate to existing wait builder infrastructure. Methods follow the established pattern of returning TasksConfigurer/FuncTaskConfigurer and using inline duration builders. - -**Tech Stack:** Java, JUnit 5, AssertJ - ---- - -## File Structure - -**Files to modify:** -- `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` - Add wait convenience methods -- `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` - Add wait convenience methods and basic wait support - -**Test files to create:** -- `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` - Comprehensive tests for DSL wait methods -- `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` - Comprehensive tests for FuncDSL wait methods - ---- - -### Task 1: Add DSL waitSeconds and waitMinutes methods with tests - -**Files:** -- Modify: `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` -- Create: `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` - -- [ ] **Step 1: Write failing test for waitSeconds** - -Create `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java`: - -```java -/* - * 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.serverlessworkflow.fluent.spec.dsl; - -import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitSeconds; -import static org.assertj.core.api.Assertions.assertThat; - -import io.serverlessworkflow.api.types.DurationInline; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.fluent.spec.WorkflowBuilder; -import org.junit.jupiter.api.Test; - -public class DSLWaitTest { - - @Test - public void when_wait_seconds_unnamed() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(waitSeconds(30)) - .build(); - - assertThat(wf.getDo()).hasSize(1); - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - assertThat(waitTask).isNotNull(); - - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline).isNotNull(); - assertThat(inline.getSeconds()).isEqualTo(30); - assertThat(inline.getMinutes()).isZero(); - assertThat(inline.getHours()).isZero(); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `mvn test -Dtest=DSLWaitTest#when_wait_seconds_unnamed -pl fluent/spec` - -Expected: FAIL with "cannot resolve method 'waitSeconds'" - -- [ ] **Step 3: Implement waitSeconds methods in DSL.java** - -Add to `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` after the existing wait methods (around line 871): - -```java - /** - * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with seconds. - * - *

Example: {@code tasks(waitSeconds(30))} - * - * @param seconds wait duration in seconds - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitSeconds(int seconds) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with seconds. - * - *

Example: {@code tasks(waitSeconds("pause", 30))} - * - * @param name task name - * @param seconds wait duration in seconds - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitSeconds(String name, int seconds) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with minutes. - * - *

Example: {@code tasks(waitMinutes(5))} - * - * @param minutes wait duration in minutes - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitMinutes(int minutes) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with minutes. - * - *

Example: {@code tasks(waitMinutes("pause", 5))} - * - * @param name task name - * @param minutes wait duration in minutes - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitMinutes(String name, int minutes) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); - } -``` - -- [ ] **Step 4: Add test for named waitSeconds** - -Add to `DSLWaitTest.java`: - -```java - @Test - public void when_wait_seconds_named() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(waitSeconds("pause", 45)) - .build(); - - assertThat(wf.getDo()).hasSize(1); - assertThat(wf.getDo().get(0).getName()).isEqualTo("pause"); - - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - assertThat(waitTask).isNotNull(); - - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline).isNotNull(); - assertThat(inline.getSeconds()).isEqualTo(45); - } -``` - -- [ ] **Step 5: Add test for waitMinutes** - -Add to `DSLWaitTest.java`: - -```java - @Test - public void when_wait_minutes_unnamed() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(waitMinutes(10)) - .build(); - - assertThat(wf.getDo()).hasSize(1); - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - assertThat(waitTask).isNotNull(); - - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline).isNotNull(); - assertThat(inline.getMinutes()).isEqualTo(10); - assertThat(inline.getSeconds()).isZero(); - } - - @Test - public void when_wait_minutes_named() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(waitMinutes("delay", 15)) - .build(); - - assertThat(wf.getDo()).hasSize(1); - assertThat(wf.getDo().get(0).getName()).isEqualTo("delay"); - - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline.getMinutes()).isEqualTo(15); - } -``` - -- [ ] **Step 6: Run all tests to verify they pass** - -Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` - -Expected: All 4 tests PASS - -- [ ] **Step 7: Commit** - -```bash -git add fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java \ - fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java -git commit -m "feat: add waitSeconds and waitMinutes convenience methods to DSL" -``` - ---- - -### Task 2: Add DSL waitHours, waitDays, and waitMillis methods - -**Files:** -- Modify: `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` -- Modify: `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` - -- [ ] **Step 1: Write failing tests** - -Add to `DSLWaitTest.java`: - -```java - @Test - public void when_wait_hours_unnamed() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.waitHours(2)) - .build(); - - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline.getHours()).isEqualTo(2); - assertThat(inline.getMinutes()).isZero(); - } - - @Test - public void when_wait_hours_named() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.waitHours("longPause", 3)) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("longPause"); - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - assertThat(waitTask.getWait().getDurationInline().getHours()).isEqualTo(3); - } - - @Test - public void when_wait_days_unnamed() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.waitDays(1)) - .build(); - - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline.getDays()).isEqualTo(1); - assertThat(inline.getHours()).isZero(); - } - - @Test - public void when_wait_days_named() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.waitDays("dailyDelay", 5)) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("dailyDelay"); - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()) - .isEqualTo(5); - } - - @Test - public void when_wait_millis_unnamed() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.waitMillis(500)) - .build(); - - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline.getMilliseconds()).isEqualTo(500); - assertThat(inline.getSeconds()).isZero(); - } - - @Test - public void when_wait_millis_named() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.waitMillis("shortPause", 250)) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("shortPause"); - assertThat( - wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getMilliseconds()) - .isEqualTo(250); - } -``` - -Add import at top of file: -```java -import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitDays; -import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitHours; -import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitMillis; -import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitMinutes; -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` - -Expected: FAIL with "cannot resolve method" errors - -- [ ] **Step 3: Implement waitHours, waitDays, and waitMillis** - -Add to `DSL.java` after the waitMinutes methods: - -```java - /** - * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with hours. - * - *

Example: {@code tasks(waitHours(2))} - * - * @param hours wait duration in hours - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitHours(int hours) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.hours(hours)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with hours. - * - *

Example: {@code tasks(waitHours("longPause", 2))} - * - * @param name task name - * @param hours wait duration in hours - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitHours(String name, int hours) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.hours(hours)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with days. - * - *

Example: {@code tasks(waitDays(1))} - * - * @param days wait duration in days - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitDays(int days) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.days(days)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with days. - * - *

Example: {@code tasks(waitDays("dailyDelay", 1))} - * - * @param name task name - * @param days wait duration in days - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitDays(String name, int days) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.days(days)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with milliseconds. - * - *

Example: {@code tasks(waitMillis(500))} - * - * @param milliseconds wait duration in milliseconds - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitMillis(int milliseconds) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); - } - - /** - * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with - * milliseconds. - * - *

Example: {@code tasks(waitMillis("shortPause", 500))} - * - * @param name task name - * @param milliseconds wait duration in milliseconds - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer waitMillis(String name, int milliseconds) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); - } -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` - -Expected: All 10 tests PASS - -- [ ] **Step 5: Commit** - -```bash -git add fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java \ - fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java -git commit -m "feat: add waitHours, waitDays, and waitMillis convenience methods to DSL" -``` - ---- - -### Task 3: Add DSL wait(Duration) method - -**Files:** -- Modify: `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java` -- Modify: `fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java` - -- [ ] **Step 1: Write failing test** - -Add to `DSLWaitTest.java`: - -```java - @Test - public void when_wait_with_duration_unnamed() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30))) - .build(); - - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline).isNotNull(); - assertThat(inline.getMinutes()).isEqualTo(5); - assertThat(inline.getSeconds()).isEqualTo(30); - } - - @Test - public void when_wait_with_duration_named() { - Workflow wf = - WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.wait("customDelay", java.time.Duration.ofHours(1).plusMinutes(15))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("customDelay"); - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - DurationInline inline = waitTask.getWait().getDurationInline(); - assertThat(inline.getHours()).isEqualTo(1); - assertThat(inline.getMinutes()).isEqualTo(15); - } -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `mvn test -Dtest=DSLWaitTest#when_wait_with_duration_unnamed -pl fluent/spec` - -Expected: FAIL (ambiguous method call or compile error) - -- [ ] **Step 3: Implement wait(Duration) methods** - -Add to `DSL.java` after the waitMillis methods: - -```java - /** - * Create a {@link TasksConfigurer} that adds a {@code wait} task configured with a Java {@link - * java.time.Duration}. - * - *

The Duration is converted to DurationInline format with days, hours, minutes, seconds, and - * milliseconds. - * - *

Example: {@code tasks(wait(Duration.ofMinutes(5).plusSeconds(30)))} - * - * @param duration wait duration as a Java Duration - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer wait(java.time.Duration duration) { - return list -> list.wait(w -> w.wait(duration)); - } - - /** - * Create a {@link TasksConfigurer} that adds a named {@code wait} task configured with a Java - * {@link java.time.Duration}. - * - *

The Duration is converted to DurationInline format with days, hours, minutes, seconds, and - * milliseconds. - * - *

Example: {@code tasks(wait("pause", Duration.ofMinutes(5).plusSeconds(30)))} - * - * @param name task name - * @param duration wait duration as a Java Duration - * @return a {@link TasksConfigurer} that adds a WaitTask - */ - public static TasksConfigurer wait(String name, java.time.Duration duration) { - return list -> list.wait(name, w -> w.wait(duration)); - } -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `mvn test -Dtest=DSLWaitTest -pl fluent/spec` - -Expected: All 12 tests PASS - -- [ ] **Step 5: Commit** - -```bash -git add fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java \ - fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java -git commit -m "feat: add wait(Duration) convenience method to DSL" -``` - ---- - -### Task 4: Add FuncDSL basic wait support - -**Files:** -- Modify: `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` -- Create: `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` - -- [ ] **Step 1: Write failing test for basic wait** - -Create `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java`: - -```java -/* - * 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.serverlessworkflow.fluent.func; - -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.tasks; -import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.wait; -import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutSeconds; -import static org.assertj.core.api.Assertions.assertThat; - -import io.serverlessworkflow.api.types.Workflow; -import org.junit.jupiter.api.Test; - -public class FuncDSLWaitTest { - - @Test - public void when_wait_with_string_expression() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(wait("PT5S"))) - .build(); - - assertThat(wf.getDo()).hasSize(1); - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - assertThat(waitTask).isNotNull(); - assertThat(waitTask.getWait().get()).isEqualTo("PT5S"); - } - - @Test - public void when_wait_with_timeout_builder() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(wait(timeoutSeconds(10)))) - .build(); - - assertThat(wf.getDo()).hasSize(1); - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - assertThat(waitTask).isNotNull(); - assertThat(waitTask.getWait().getDurationInline().getSeconds()).isEqualTo(10); - } - - @Test - public void when_wait_named_with_string() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(wait("pause", "PT15S"))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("pause"); - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().get()).isEqualTo("PT15S"); - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` - -Expected: FAIL with "cannot resolve method 'wait'" - -- [ ] **Step 3: Implement basic wait methods in FuncDSL** - -Add to `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` in the appropriate section (after similar task methods, around line 1088): - -```java - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with a duration - * expression. - * - *

Example: {@code tasks(wait("PT5M"))} - * - * @param durationExpression duration expression or ISO 8601 literal - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer wait(String durationExpression) { - return list -> list.wait(w -> w.wait(durationExpression)); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with a - * duration expression. - * - *

Example: {@code tasks(wait("pause", "PT5M"))} - * - * @param name task name - * @param durationExpression duration expression or ISO 8601 literal - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer wait(String name, String durationExpression) { - return list -> list.wait(name, w -> w.wait(durationExpression)); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with an inline - * duration builder. - * - *

Example: {@code tasks(wait(timeoutSeconds(30)))} - * - * @param duration timeout builder consumer - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer wait(Consumer duration) { - return list -> list.wait(w -> w.wait(duration)); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with an - * inline duration builder. - * - *

Example: {@code tasks(wait("pause", timeoutSeconds(30)))} - * - * @param name task name - * @param duration timeout builder consumer - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer wait(String name, Consumer duration) { - return list -> list.wait(name, w -> w.wait(duration)); - } -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` - -Expected: All 3 tests PASS - -- [ ] **Step 5: Commit** - -```bash -git add experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java \ - experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java -git commit -m "feat: add basic wait methods to FuncDSL" -``` - ---- - -### Task 5: Add FuncDSL waitSeconds and waitMinutes convenience methods - -**Files:** -- Modify: `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` -- Modify: `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` - -- [ ] **Step 1: Write failing tests** - -Add to `FuncDSLWaitTest.java`: - -```java - @Test - public void when_wait_seconds_unnamed() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitSeconds(30))) - .build(); - - var waitTask = wf.getDo().get(0).getTask().getWaitTask(); - var inline = waitTask.getWait().getDurationInline(); - assertThat(inline).isNotNull(); - assertThat(inline.getSeconds()).isEqualTo(30); - assertThat(inline.getMinutes()).isZero(); - } - - @Test - public void when_wait_seconds_named() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitSeconds("pause", 45))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("pause"); - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getSeconds()) - .isEqualTo(45); - } - - @Test - public void when_wait_minutes_unnamed() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitMinutes(10))) - .build(); - - var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); - assertThat(inline.getMinutes()).isEqualTo(10); - assertThat(inline.getSeconds()).isZero(); - } - - @Test - public void when_wait_minutes_named() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitMinutes("delay", 15))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("delay"); - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getMinutes()) - .isEqualTo(15); - } -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` - -Expected: FAIL with "cannot resolve method" errors - -- [ ] **Step 3: Implement waitSeconds and waitMinutes** - -Add to `FuncDSL.java` after the basic wait methods: - -```java - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with seconds. - * - *

Example: {@code tasks(waitSeconds(30))} - * - * @param seconds wait duration in seconds - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitSeconds(int seconds) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with - * seconds. - * - *

Example: {@code tasks(waitSeconds("pause", 30))} - * - * @param name task name - * @param seconds wait duration in seconds - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitSeconds(String name, int seconds) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with minutes. - * - *

Example: {@code tasks(waitMinutes(5))} - * - * @param minutes wait duration in minutes - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitMinutes(int minutes) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with - * minutes. - * - *

Example: {@code tasks(waitMinutes("pause", 5))} - * - * @param name task name - * @param minutes wait duration in minutes - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitMinutes(String name, int minutes) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.minutes(minutes)))); - } -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` - -Expected: All 7 tests PASS - -- [ ] **Step 5: Commit** - -```bash -git add experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java \ - experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java -git commit -m "feat: add waitSeconds and waitMinutes to FuncDSL" -``` - ---- - -### Task 6: Add remaining FuncDSL convenience methods - -**Files:** -- Modify: `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java` -- Modify: `experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java` - -- [ ] **Step 1: Write failing tests** - -Add to `FuncDSLWaitTest.java`: - -```java - @Test - public void when_wait_hours_unnamed() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitHours(2))) - .build(); - - var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); - assertThat(inline.getHours()).isEqualTo(2); - } - - @Test - public void when_wait_hours_named() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitHours("longPause", 3))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("longPause"); - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getHours()) - .isEqualTo(3); - } - - @Test - public void when_wait_days_unnamed() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitDays(1))) - .build(); - - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()) - .isEqualTo(1); - } - - @Test - public void when_wait_days_named() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitDays("dailyDelay", 5))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("dailyDelay"); - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getDays()) - .isEqualTo(5); - } - - @Test - public void when_wait_millis_unnamed() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitMillis(500))) - .build(); - - var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); - assertThat(inline.getMilliseconds()).isEqualTo(500); - } - - @Test - public void when_wait_millis_named() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.waitMillis("shortPause", 250))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("shortPause"); - assertThat( - wf.getDo() - .get(0) - .getTask() - .getWaitTask() - .getWait() - .getDurationInline() - .getMilliseconds()) - .isEqualTo(250); - } - - @Test - public void when_wait_with_duration() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30)))) - .build(); - - var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); - assertThat(inline.getMinutes()).isEqualTo(5); - assertThat(inline.getSeconds()).isEqualTo(30); - } - - @Test - public void when_wait_with_duration_named() { - Workflow wf = - FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.wait("custom", java.time.Duration.ofHours(1)))) - .build(); - - assertThat(wf.getDo().get(0).getName()).isEqualTo("custom"); - assertThat(wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline().getHours()) - .isEqualTo(1); - } -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` - -Expected: FAIL with "cannot resolve method" errors - -- [ ] **Step 3: Implement waitHours, waitDays, waitMillis, and wait(Duration)** - -Add to `FuncDSL.java` after the waitMinutes methods: - -```java - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with hours. - * - *

Example: {@code tasks(waitHours(2))} - * - * @param hours wait duration in hours - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitHours(int hours) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.hours(hours)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with hours. - * - *

Example: {@code tasks(waitHours("longPause", 2))} - * - * @param name task name - * @param hours wait duration in hours - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitHours(String name, int hours) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.hours(hours)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with days. - * - *

Example: {@code tasks(waitDays(1))} - * - * @param days wait duration in days - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitDays(int days) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.days(days)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with days. - * - *

Example: {@code tasks(waitDays("dailyDelay", 1))} - * - * @param name task name - * @param days wait duration in days - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitDays(String name, int days) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.days(days)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with - * milliseconds. - * - *

Example: {@code tasks(waitMillis(500))} - * - * @param milliseconds wait duration in milliseconds - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitMillis(int milliseconds) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with - * milliseconds. - * - *

Example: {@code tasks(waitMillis("shortPause", 500))} - * - * @param name task name - * @param milliseconds wait duration in milliseconds - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer waitMillis(String name, int milliseconds) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.milliseconds(milliseconds)))); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a {@code wait} task configured with a Java - * {@link java.time.Duration}. - * - *

The Duration is converted to DurationInline format. - * - *

Example: {@code tasks(wait(Duration.ofMinutes(5).plusSeconds(30)))} - * - * @param duration wait duration as a Java Duration - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer wait(java.time.Duration duration) { - return list -> list.wait(w -> w.wait(duration)); - } - - /** - * Create a {@link FuncTaskConfigurer} that adds a named {@code wait} task configured with a Java - * {@link java.time.Duration}. - * - *

The Duration is converted to DurationInline format. - * - *

Example: {@code tasks(wait("pause", Duration.ofMinutes(5)))} - * - * @param name task name - * @param duration wait duration as a Java Duration - * @return a {@link FuncTaskConfigurer} that adds a WaitTask - */ - public static FuncTaskConfigurer wait(String name, java.time.Duration duration) { - return list -> list.wait(name, w -> w.wait(duration)); - } -``` - -- [ ] **Step 4: Run all tests to verify they pass** - -Run: `mvn test -Dtest=FuncDSLWaitTest -pl experimental/fluent/func` - -Expected: All 15 tests PASS - -- [ ] **Step 5: Run all fluent tests to ensure no regressions** - -Run: `mvn test -pl fluent/spec,experimental/fluent/func` - -Expected: All tests PASS - -- [ ] **Step 6: Commit** - -```bash -git add experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java \ - experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java -git commit -m "feat: add waitHours, waitDays, waitMillis, and wait(Duration) to FuncDSL" -``` - ---- - -## Summary - -This plan implements ergonomic wait convenience methods for both DSL and FuncDSL: - -**DSL.java additions:** -- `waitSeconds(int)` / `waitSeconds(String, int)` -- `waitMinutes(int)` / `waitMinutes(String, int)` -- `waitHours(int)` / `waitHours(String, int)` -- `waitDays(int)` / `waitDays(String, int)` -- `waitMillis(int)` / `waitMillis(String, int)` -- `wait(Duration)` / `wait(String, Duration)` - -**FuncDSL.java additions:** -- All basic wait methods (string expression, TimeoutBuilder consumer) -- All convenience methods from DSL (matching signatures but returning FuncTaskConfigurer) - -**Test Coverage:** -- 12 tests for DSL wait methods -- 15 tests for FuncDSL wait methods -- Tests verify both named and unnamed variants -- Tests verify Duration conversion to DurationInline diff --git a/docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md b/docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md deleted file mode 100644 index f3be236e2..000000000 --- a/docs/superpowers/specs/2026-06-11-waittask-ergonomics-design.md +++ /dev/null @@ -1,161 +0,0 @@ -# WaitTask Ergonomics and Executor Fix - -**Date**: 2026-06-11 -**Status**: Approved - -## Overview - -Improve the WaitTask API by adding ergonomic convenience methods to DSL/FuncDSL and fix the WaitExecutor to properly handle all three duration formats (inline, literal, expression). - -## Problems - -1. DSL has ergonomic `timeout*` methods but no equivalent for wait tasks -2. FuncDSL has zero wait task support -3. WaitExecutor only handles `durationInline` and `durationExpression`, crashes on `durationLiteral` -4. WaitExecutor parses expressions at build time instead of evaluating at runtime - -## Solution - -### 1. DSL Convenience Methods - -Add to `fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/DSL.java`: - -**Unnamed variants:** -```java -public static TasksConfigurer waitDays(int days) -public static TasksConfigurer waitHours(int hours) -public static TasksConfigurer waitMinutes(int minutes) -public static TasksConfigurer waitSeconds(int seconds) -public static TasksConfigurer waitMillis(int milliseconds) -public static TasksConfigurer wait(Duration duration) -``` - -**Named variants:** -```java -public static TasksConfigurer waitDays(String name, int days) -public static TasksConfigurer waitHours(String name, int hours) -public static TasksConfigurer waitMinutes(String name, int minutes) -public static TasksConfigurer waitSeconds(String name, int seconds) -public static TasksConfigurer waitMillis(String name, int milliseconds) -public static TasksConfigurer wait(String name, Duration duration) -``` - -**Implementation pattern:** -```java -public static TasksConfigurer waitSeconds(int seconds) { - return list -> list.wait(w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); -} - -public static TasksConfigurer waitSeconds(String name, int seconds) { - return list -> list.wait(name, w -> w.wait(t -> t.duration(d -> d.seconds(seconds)))); -} -``` - -The `wait(Duration)` variant converts to `DurationInline` using the existing logic from `WaitTaskBuilder.wait(Duration)`. - -### 2. FuncDSL Wait Support - -Add to `experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/dsl/FuncDSL.java`: - -All methods from section 1, but returning `FuncTaskConfigurer` instead of `TasksConfigurer`, plus the basic wait methods: - -```java -public static FuncTaskConfigurer wait(Consumer duration) -public static FuncTaskConfigurer wait(String name, Consumer duration) -public static FuncTaskConfigurer wait(String durationExpression) -public static FuncTaskConfigurer wait(String name, String durationExpression) -``` - -These delegate to `list.wait()` on the `FuncTaskItemListBuilder`, following the same pattern as other FuncDSL task methods. - -### 3. WaitExecutor Duration Handling - -Modify `impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java`: - -**Hybrid approach**: Validate static durations at build time, defer expressions to runtime. - -**Changes to WaitExecutorBuilder:** - -Add field: -```java -private final String runtimeExpression; -``` - -Update constructor logic: -```java -protected WaitExecutorBuilder( - WorkflowMutablePosition position, WaitTask task, WorkflowDefinition definition) { - super(position, task, definition); - - if (task.getWait().getDurationInline() != null) { - this.millisToWait = toLong(task.getWait().getDurationInline()); - this.runtimeExpression = null; - } else if (task.getWait().getDurationLiteral() != null) { - this.millisToWait = Duration.parse(task.getWait().getDurationLiteral()); - this.runtimeExpression = null; - } else if (task.getWait().getDurationExpression() != null) { - this.millisToWait = null; - this.runtimeExpression = task.getWait().getDurationExpression(); - } else { - throw new IllegalStateException("Wait task has no duration specified"); - } -} -``` - -**Changes to WaitExecutor:** - -Add field: -```java -private final String runtimeExpression; -``` - -Update constructor: -```java -protected WaitExecutor(WaitExecutorBuilder builder) { - super(builder); - this.millisToWait = builder.millisToWait; - this.runtimeExpression = builder.runtimeExpression; -} -``` - -Update `internalExecute()`: -```java -@Override -protected CompletableFuture internalExecute( - WorkflowContext workflow, TaskContext taskContext) { - ((WorkflowMutableInstance) workflow.instance()).status(WorkflowStatus.WAITING); - - Duration waitDuration; - if (runtimeExpression != null) { - // Evaluate expression at runtime using workflow/task context - String evaluatedExpression = evaluateExpression(runtimeExpression, workflow, taskContext); - waitDuration = Duration.parse(evaluatedExpression); - } else { - waitDuration = millisToWait; - } - - return new CompletableFuture() - .completeOnTimeout(taskContext.output(), waitDuration.toMillis(), TimeUnit.MILLISECONDS) - .thenApply(this::complete); -} -``` - -Note: The `evaluateExpression()` method will need to be implemented or use existing expression evaluation utilities from the workflow context. - -## Benefits - -- Consistent API between timeout and wait methods -- Better developer experience with concise method calls -- Full support for all three duration formats -- Early validation for static durations -- Proper runtime evaluation for dynamic expressions -- FuncDSL users can now use wait tasks - -## Testing Considerations - -- Test all convenience methods (days, hours, minutes, seconds, millis) -- Test `wait(Duration)` conversion -- Test WaitExecutor with inline, literal, and expression durations -- Test runtime expression evaluation with various workflow contexts -- Verify build-time validation catches invalid static durations -- Verify runtime errors for invalid expression results From 3ee7126bc4a1db5f577a1e206189ccfbea345c5f Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 11 Jun 2026 16:58:38 -0400 Subject: [PATCH 4/8] Clean up imports Signed-off-by: Ricardo Zanini --- .../fluent/func/FuncTaskItemListBuilder.java | 7 ++++--- .../io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java | 6 ++++-- .../io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java index ae2bee648..7d9e5dd05 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java @@ -23,6 +23,7 @@ import io.serverlessworkflow.fluent.func.spi.FuncDoFluent; import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; import io.serverlessworkflow.fluent.spec.TaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.WaitTaskBuilder; import io.serverlessworkflow.fluent.spec.WorkflowTaskBuilder; import java.util.List; import java.util.function.Consumer; @@ -222,10 +223,10 @@ public FuncTaskItemListBuilder wait( } public FuncTaskItemListBuilder wait( - String name, Consumer itemsConfigurer) { + String name, Consumer itemsConfigurer) { name = this.defaultNameAndRequireConfig(name, itemsConfigurer, "wait"); - final io.serverlessworkflow.fluent.spec.WaitTaskBuilder waitTaskBuilder = - new io.serverlessworkflow.fluent.spec.WaitTaskBuilder(); + final WaitTaskBuilder waitTaskBuilder = + new WaitTaskBuilder(); itemsConfigurer.accept(waitTaskBuilder); return this.addTaskItem(new TaskItem(name, new Task().withWaitTask(waitTaskBuilder.build()))); } diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java index 49d9c2b53..5768fab82 100644 --- a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java @@ -15,6 +15,8 @@ */ package io.serverlessworkflow.fluent.func; +import java.time.Duration; + import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.tasks; import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutSeconds; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -175,7 +177,7 @@ public void when_wait_millis_named() { public void when_wait_with_duration() { Workflow wf = FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30)))) + .tasks(tasks(FuncDSL.wait(Duration.ofMinutes(5).plusSeconds(30)))) .build(); var inline = wf.getDo().get(0).getTask().getWaitTask().getWait().getDurationInline(); @@ -187,7 +189,7 @@ public void when_wait_with_duration() { public void when_wait_with_duration_named() { Workflow wf = FuncWorkflowBuilder.workflow("waitFlow") - .tasks(tasks(FuncDSL.wait("custom", java.time.Duration.ofHours(1)))) + .tasks(tasks(FuncDSL.wait("custom", Duration.ofHours(1)))) .build(); assertEquals("custom", wf.getDo().get(0).getName()); diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java index b8c221e07..b112687b9 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java @@ -15,6 +15,8 @@ */ package io.serverlessworkflow.fluent.spec.dsl; +import java.time.Duration; + import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitMinutes; import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitSeconds; import static org.assertj.core.api.Assertions.assertThat; @@ -170,7 +172,7 @@ public void when_wait_millis_named() { public void when_wait_with_duration_unnamed() { Workflow wf = WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.wait(java.time.Duration.ofMinutes(5).plusSeconds(30))) + .tasks(DSL.wait(Duration.ofMinutes(5).plusSeconds(30))) .build(); var waitTask = wf.getDo().get(0).getTask().getWaitTask(); @@ -184,7 +186,7 @@ public void when_wait_with_duration_unnamed() { public void when_wait_with_duration_named() { Workflow wf = WorkflowBuilder.workflow("waitFlow", "myNs", "1.0.0") - .tasks(DSL.wait("customDelay", java.time.Duration.ofHours(1).plusMinutes(15))) + .tasks(DSL.wait("customDelay", Duration.ofHours(1).plusMinutes(15))) .build(); assertThat(wf.getDo().get(0).getName()).isEqualTo("customDelay"); From 36e9b27fd25a868d5db26d73879766bc73ccc9e3 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 11 Jun 2026 17:01:05 -0400 Subject: [PATCH 5/8] Formating Signed-off-by: Ricardo Zanini --- .../fluent/func/FuncTaskItemListBuilder.java | 6 ++---- .../io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java | 3 +-- .../io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java index 7d9e5dd05..66575d836 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java @@ -222,11 +222,9 @@ public FuncTaskItemListBuilder wait( return wait(null, itemsConfigurer); } - public FuncTaskItemListBuilder wait( - String name, Consumer itemsConfigurer) { + public FuncTaskItemListBuilder wait(String name, Consumer itemsConfigurer) { name = this.defaultNameAndRequireConfig(name, itemsConfigurer, "wait"); - final WaitTaskBuilder waitTaskBuilder = - new WaitTaskBuilder(); + final WaitTaskBuilder waitTaskBuilder = new WaitTaskBuilder(); itemsConfigurer.accept(waitTaskBuilder); return this.addTaskItem(new TaskItem(name, new Task().withWaitTask(waitTaskBuilder.build()))); } diff --git a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java index 5768fab82..712c328da 100644 --- a/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java +++ b/experimental/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/FuncDSLWaitTest.java @@ -15,8 +15,6 @@ */ package io.serverlessworkflow.fluent.func; -import java.time.Duration; - import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.tasks; import static io.serverlessworkflow.fluent.spec.dsl.DSL.timeoutSeconds; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -24,6 +22,7 @@ import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.func.dsl.FuncDSL; +import java.time.Duration; import org.junit.jupiter.api.Test; public class FuncDSLWaitTest { diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java index b112687b9..d49b77cf6 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/DSLWaitTest.java @@ -15,8 +15,6 @@ */ package io.serverlessworkflow.fluent.spec.dsl; -import java.time.Duration; - import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitMinutes; import static io.serverlessworkflow.fluent.spec.dsl.DSL.waitSeconds; import static org.assertj.core.api.Assertions.assertThat; @@ -24,6 +22,7 @@ import io.serverlessworkflow.api.types.DurationInline; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.spec.WorkflowBuilder; +import java.time.Duration; import org.junit.jupiter.api.Test; public class DSLWaitTest { From 16c471c30e3adb3c1f69463759f0fc84ce5aaab1 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:29:40 -0400 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../fluent/func/FuncTaskItemListBuilder.java | 5 +-- .../impl/test/WaitExecutorTest.java | 38 +++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java index 66575d836..257a1f96b 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java @@ -217,13 +217,12 @@ public FuncTaskItemListBuilder tryCatch( return this.addTaskItem(new TaskItem(name, new Task().withTryTask(tryTaskBuilder.build()))); } - public FuncTaskItemListBuilder wait( - Consumer itemsConfigurer) { + public FuncTaskItemListBuilder wait(Consumer itemsConfigurer) { return wait(null, itemsConfigurer); } public FuncTaskItemListBuilder wait(String name, Consumer itemsConfigurer) { - name = this.defaultNameAndRequireConfig(name, itemsConfigurer, "wait"); + name = this.defaultNameAndRequireConfig(name, itemsConfigurer, TYPE_WAIT); final WaitTaskBuilder waitTaskBuilder = new WaitTaskBuilder(); itemsConfigurer.accept(waitTaskBuilder); return this.addTaskItem(new TaskItem(name, new Task().withWaitTask(waitTaskBuilder.build()))); diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java index 053b7ab10..6fd016cad 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java @@ -97,11 +97,29 @@ void testWaitWithDurationInlineComposite() { assertThat(elapsed).isGreaterThanOrEqualTo(1500); // 1 second + 500 milliseconds } - // ========== DurationLiteral Tests (via Duration.parse) ========== + // ========== DurationLiteral Tests (TimeoutAfter.durationLiteral) ========== @Test void testWaitWithDurationLiteralISO8601Seconds() { Workflow workflow = + WorkflowBuilder.workflow("wait-literal-seconds", "test", "0.1.0") + .tasks( + list -> + list.wait( + w -> + w.build() + .setWait( + new io.serverlessworkflow.api.types.TimeoutAfter() + .withDurationLiteral("PT1S")))) + .build(); + + long startTime = System.currentTimeMillis(); + WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); + long elapsed = System.currentTimeMillis() - startTime; + + assertThat(model).isNotNull(); + assertThat(elapsed).isGreaterThanOrEqualTo(1000); + } WorkflowBuilder.workflow("wait-literal-seconds", "test", "0.1.0") .tasks(DSL.wait(Duration.parse("PT1S"))) .build(); @@ -119,7 +137,14 @@ void testWaitWithDurationLiteralISO8601Composite() { // PT1.5S = 1 second 500 milliseconds (keep test fast) Workflow workflow = WorkflowBuilder.workflow("wait-literal-composite", "test", "0.1.0") - .tasks(DSL.wait(Duration.parse("PT1.5S"))) + .tasks( + list -> + list.wait( + w -> + w.build() + .setWait( + new io.serverlessworkflow.api.types.TimeoutAfter() + .withDurationLiteral("PT1.5S")))) .build(); long startTime = System.currentTimeMillis(); @@ -135,7 +160,14 @@ void testWaitWithDurationLiteralISO8601Milliseconds() { // PT0.1S = 100 milliseconds Workflow workflow = WorkflowBuilder.workflow("wait-literal-millis", "test", "0.1.0") - .tasks(DSL.wait(Duration.parse("PT0.1S"))) + .tasks( + list -> + list.wait( + w -> + w.build() + .setWait( + new io.serverlessworkflow.api.types.TimeoutAfter() + .withDurationLiteral("PT0.1S")))) .build(); long startTime = System.currentTimeMillis(); From 9dcbbd2e66e4eab4ef443e95b5c86cad5998a499 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 11 Jun 2026 19:52:23 -0400 Subject: [PATCH 7/8] Consider co-pilot comments Signed-off-by: Ricardo Zanini --- .../impl/executors/WaitExecutor.java | 12 ++++---- .../impl/test/WaitExecutorTest.java | 28 +++---------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java index 00051c604..ce3f36f07 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java @@ -42,14 +42,14 @@ public static class WaitExecutorBuilder extends RegularTaskExecutorBuilder list.wait( - w -> - w.build() - .setWait( - new io.serverlessworkflow.api.types.TimeoutAfter() - .withDurationLiteral("PT1S")))) - .build(); - - long startTime = System.currentTimeMillis(); - WorkflowModel model = appl.workflowDefinition(workflow).instance(Map.of()).start().join(); - long elapsed = System.currentTimeMillis() - startTime; - - assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(1000); - } - WorkflowBuilder.workflow("wait-literal-seconds", "test", "0.1.0") - .tasks(DSL.wait(Duration.parse("PT1S"))) + w -> w.build().setWait(new TimeoutAfter().withDurationExpression("PT1S")))) .build(); long startTime = System.currentTimeMillis(); @@ -141,10 +127,7 @@ void testWaitWithDurationLiteralISO8601Composite() { list -> list.wait( w -> - w.build() - .setWait( - new io.serverlessworkflow.api.types.TimeoutAfter() - .withDurationLiteral("PT1.5S")))) + w.build().setWait(new TimeoutAfter().withDurationExpression("PT1.5S")))) .build(); long startTime = System.currentTimeMillis(); @@ -164,10 +147,7 @@ void testWaitWithDurationLiteralISO8601Milliseconds() { list -> list.wait( w -> - w.build() - .setWait( - new io.serverlessworkflow.api.types.TimeoutAfter() - .withDurationLiteral("PT0.1S")))) + w.build().setWait(new TimeoutAfter().withDurationExpression("PT0.1S")))) .build(); long startTime = System.currentTimeMillis(); From 48f86f8856635a9f16b7937f84e18bec3fb2519d Mon Sep 17 00:00:00 2001 From: fjtirado Date: Fri, 12 Jun 2026 12:42:02 +0200 Subject: [PATCH 8/8] Review comments Reusing existing code to deal with TimeoutAfter and keep the test execution fast --- .../impl/executors/WaitExecutor.java | 77 ++----------------- .../impl/test/WaitExecutorTest.java | 31 ++++---- .../wait-expression-context.yaml | 2 +- 3 files changed, 22 insertions(+), 88 deletions(-) diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java index ce3f36f07..8e49ee864 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java @@ -15,68 +15,30 @@ */ package io.serverlessworkflow.impl.executors; -import io.serverlessworkflow.api.types.DurationInline; import io.serverlessworkflow.api.types.WaitTask; import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowDefinition; -import io.serverlessworkflow.impl.WorkflowFilter; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.WorkflowMutablePosition; import io.serverlessworkflow.impl.WorkflowStatus; import io.serverlessworkflow.impl.WorkflowUtils; +import io.serverlessworkflow.impl.WorkflowValueResolver; import java.time.Duration; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public class WaitExecutor extends RegularTaskExecutor { - private final Duration duration; - private final WorkflowFilter durationExpressionFilter; + private final WorkflowValueResolver durationResolver; public static class WaitExecutorBuilder extends RegularTaskExecutorBuilder { - private Duration duration = null; - private WorkflowFilter durationExpressionFilter; + private WorkflowValueResolver durationResolver; protected WaitExecutorBuilder( WorkflowMutablePosition position, WaitTask task, WorkflowDefinition definition) { super(position, task, definition); - if (WorkflowUtils.isValid(task.getWait().getDurationExpression())) { - this.durationExpressionFilter = - WorkflowUtils.buildWorkflowFilter(application, task.getWait().getDurationExpression()); - } else { - this.duration = - WorkflowUtils.isValid(task.getWait().getDurationLiteral()) - ? parseDurationLiteral(task.getWait().getDurationLiteral()) - : toDuration(task.getWait().getDurationInline()); - } - } - - private Duration toDuration(DurationInline durationInline) { - return Duration.ofMillis(durationInline.getMilliseconds()) - .plusSeconds(durationInline.getSeconds()) - .plusMinutes(durationInline.getMinutes()) - .plusHours(durationInline.getHours()) - .plusDays(durationInline.getDays()); - } - - private Duration parseDurationLiteral(String literal) { - if (!WorkflowUtils.isValid(literal)) { - throw new IllegalArgumentException( - "Wait task duration literal cannot be null or empty at position: " - + position.jsonPointer()); - } - try { - return Duration.parse(literal); - } catch (Exception e) { - throw new IllegalArgumentException( - "Invalid ISO 8601 duration literal '" - + literal - + "' at position: " - + position.jsonPointer(), - e); - } + durationResolver = WorkflowUtils.fromTimeoutAfter(application, task.getWait()); } @Override @@ -87,8 +49,7 @@ public WaitExecutor buildInstance() { protected WaitExecutor(WaitExecutorBuilder builder) { super(builder); - this.duration = builder.duration; - this.durationExpressionFilter = builder.durationExpressionFilter; + this.durationResolver = builder.durationResolver; } @Override @@ -98,33 +59,7 @@ protected CompletableFuture internalExecute( return new CompletableFuture() .completeOnTimeout( taskContext.output(), - Objects.requireNonNullElseGet( - duration, () -> evaluateDurationExpression(workflow, taskContext)) - .toMillis(), + durationResolver.apply(workflow, taskContext, taskContext.input()).toMillis(), TimeUnit.MILLISECONDS); } - - private Duration evaluateDurationExpression(WorkflowContext workflow, TaskContext taskContext) { - final Object durationObject = - durationExpressionFilter.apply(workflow, taskContext, taskContext.input()).asJavaObject(); - - if (durationObject == null || !WorkflowUtils.isValid(durationObject.toString())) { - throw new IllegalArgumentException( - "Wait duration expression evaluated to empty or null at task: " - + taskContext.position().jsonPointer() - + ". Expression must return a valid ISO 8601 duration string."); - } - - try { - return Duration.parse(durationObject.toString().trim()); - } catch (Exception e) { - throw new IllegalArgumentException( - "Wait duration expression returned invalid ISO 8601 duration '" - + durationObject - + "' at task: " - + taskContext.position().jsonPointer() - + ". Expected format: PT[n]H[n]M[n]S (e.g., PT1H30M, PT5S)", - e); - } - } } diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java index b4397b769..3895f9f8c 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/WaitExecutorTest.java @@ -30,10 +30,12 @@ import io.serverlessworkflow.impl.WorkflowStatus; import java.io.IOException; import java.time.Duration; +import java.time.format.DateTimeParseException; import java.util.Map; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class WaitExecutorTest { @@ -53,6 +55,7 @@ static void tearDown() { // ========== DurationInline Tests ========== @Test + @Disabled("This one slow down") void testWaitWithDurationInlineSeconds() { Workflow workflow = WorkflowBuilder.workflow("wait-inline-seconds", "test", "0.1.0") @@ -87,7 +90,7 @@ void testWaitWithDurationInlineComposite() { // Test composite duration with multiple components Workflow workflow = WorkflowBuilder.workflow("wait-inline-composite", "test", "0.1.0") - .tasks(DSL.wait(Duration.ofSeconds(1).plusMillis(500))) + .tasks(DSL.wait(Duration.ofMillis(100).plusMillis(100))) .build(); long startTime = System.currentTimeMillis(); @@ -95,12 +98,13 @@ void testWaitWithDurationInlineComposite() { long elapsed = System.currentTimeMillis() - startTime; assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(1500); // 1 second + 500 milliseconds + assertThat(elapsed).isGreaterThanOrEqualTo(200); // 1 second + 500 milliseconds } // ========== DurationLiteral Tests (TimeoutAfter.durationLiteral) ========== @Test + @Disabled("Seconds is too slow and is already covered") void testWaitWithDurationLiteralISO8601Seconds() { Workflow workflow = WorkflowBuilder.workflow("wait-literal-seconds", "test", "0.1.0") @@ -120,14 +124,13 @@ void testWaitWithDurationLiteralISO8601Seconds() { @Test void testWaitWithDurationLiteralISO8601Composite() { - // PT1.5S = 1 second 500 milliseconds (keep test fast) Workflow workflow = WorkflowBuilder.workflow("wait-literal-composite", "test", "0.1.0") .tasks( list -> list.wait( w -> - w.build().setWait(new TimeoutAfter().withDurationExpression("PT1.5S")))) + w.build().setWait(new TimeoutAfter().withDurationExpression("PT0.1S")))) .build(); long startTime = System.currentTimeMillis(); @@ -135,7 +138,7 @@ void testWaitWithDurationLiteralISO8601Composite() { long elapsed = System.currentTimeMillis() - startTime; assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(1500); + assertThat(elapsed).isGreaterThanOrEqualTo(100); } @Test @@ -166,11 +169,11 @@ void testWaitWithDurationExpressionFromInput() throws IOException { long startTime = System.currentTimeMillis(); WorkflowModel model = - appl.workflowDefinition(workflow).instance(Map.of("timeout", "PT1S")).start().join(); + appl.workflowDefinition(workflow).instance(Map.of("timeout", "PT0.1S")).start().join(); long elapsed = System.currentTimeMillis() - startTime; assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(1000); + assertThat(elapsed).isGreaterThanOrEqualTo(100); } @Test @@ -182,7 +185,7 @@ void testWaitWithDurationExpressionFromContext() throws IOException { long elapsed = System.currentTimeMillis() - startTime; assertThat(model).isNotNull(); - assertThat(elapsed).isGreaterThanOrEqualTo(500); + assertThat(elapsed).isGreaterThanOrEqualTo(100); } @Test @@ -195,19 +198,15 @@ void testWaitWithDurationExpressionInvalidValue() throws IOException { .instance(Map.of("timeout", "not-a-duration")) .start() .join()) - .hasCauseInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("invalid ISO 8601 duration"); + .hasCauseInstanceOf(DateTimeParseException.class); } @Test void testWaitWithDurationExpressionMissingValue() throws IOException { Workflow workflow = readWorkflowFromClasspath("workflows-samples/wait-expression-input.yaml"); - // When the expression resolves to empty/null, we throw IllegalArgumentException with helpful - // message assertThatThrownBy(() -> appl.workflowDefinition(workflow).instance(Map.of()).start().join()) - .hasCauseInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("evaluated to empty or null"); + .hasCauseInstanceOf(DateTimeParseException.class); } // ========== Workflow Status Tests ========== @@ -216,7 +215,7 @@ void testWaitWithDurationExpressionMissingValue() throws IOException { void testWaitSetsWorkflowStatusToWaiting() { Workflow workflow = WorkflowBuilder.workflow("wait-status-waiting", "test", "0.1.0") - .tasks(DSL.waitMillis(500)) + .tasks(DSL.waitMillis(100)) .build(); WorkflowInstance instance = appl.workflowDefinition(workflow).instance(Map.of()); @@ -237,7 +236,7 @@ void testWaitSetsWorkflowStatusToWaiting() { void testWaitWithSuspendAndResume() { Workflow workflow = WorkflowBuilder.workflow("wait-suspend-resume", "test", "0.1.0") - .tasks(DSL.waitMillis(500)) + .tasks(DSL.waitMillis(100)) .build(); WorkflowInstance instance = appl.workflowDefinition(workflow).instance(Map.of()); diff --git a/impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml b/impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml index f7cb43b8a..fa3b26d29 100644 --- a/impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml +++ b/impl/test/src/test/resources/workflows-samples/wait-expression-context.yaml @@ -6,6 +6,6 @@ document: do: - setDuration: set: - waitTime: PT0.5S + waitTime: PT0.1S - waitExpression: wait: ${.waitTime}