From d66ba94ab5126b930e4cd43a26b4bcd04bdaa8a7 Mon Sep 17 00:00:00 2001 From: Akanksha Trehun Date: Wed, 9 Sep 2026 21:50:21 +0530 Subject: [PATCH 01/15] Expose a replay-safe new_guid on WorkflowContext The underlying orchestration context already generates deterministic GUIDs internally (used by the worker itself for task execution ids), but nothing on the public WorkflowContext exposed it to workflow authors, so anyone needing a stable id inside a workflow had to reach for uuid4 and break replay determinism. Adds new_guid as an abstract method on WorkflowContext and implements it on DaprWorkflowContext by delegating to the wrapped context, matching the .NET SDK's NewGuid. Signed-off-by: Akanksha Trehun --- dapr/ext/workflow/dapr_workflow_context.py | 4 ++++ dapr/ext/workflow/workflow_context.py | 17 +++++++++++++++++ .../ext/workflow/test_dapr_workflow_context.py | 7 +++++++ 3 files changed, 28 insertions(+) diff --git a/dapr/ext/workflow/dapr_workflow_context.py b/dapr/ext/workflow/dapr_workflow_context.py index 7ceee0cbf..3cde70d55 100644 --- a/dapr/ext/workflow/dapr_workflow_context.py +++ b/dapr/ext/workflow/dapr_workflow_context.py @@ -15,6 +15,7 @@ from datetime import datetime, timedelta from typing import Any, Callable, List, Optional, TypeVar, Union +from uuid import UUID from dapr.ext.workflow._durabletask import task from dapr.ext.workflow.logger import Logger, LoggerOptions @@ -57,6 +58,9 @@ def set_custom_status(self, custom_status: str) -> None: self._logger.debug(f'{self.instance_id}: Setting custom status to {custom_status}') self.__obj.set_custom_status(custom_status) + def new_guid(self) -> UUID: + return self.__obj.new_guid() + def create_timer(self, fire_at: Union[datetime, timedelta]) -> task.Task: self._logger.debug(f'{self.instance_id}: Creating timer to fire at {fire_at} time') return self.__obj.create_timer(fire_at) diff --git a/dapr/ext/workflow/workflow_context.py b/dapr/ext/workflow/workflow_context.py index e3e98fe9e..956bac326 100644 --- a/dapr/ext/workflow/workflow_context.py +++ b/dapr/ext/workflow/workflow_context.py @@ -18,6 +18,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timedelta from typing import Any, Callable, Generator, Optional, TypeVar, Union +from uuid import UUID from dapr.ext.workflow._durabletask import task from dapr.ext.workflow.propagation import PropagatedHistory, PropagationScope @@ -90,6 +91,22 @@ def set_custom_status(self, custom_status: str) -> None: """Set the custom status.""" pass + @abstractmethod + def new_guid(self) -> UUID: + """Create a new GUID that is safe for replay within a workflow. + + The GUID is deterministically derived from the workflow instance ID, + the current replay-safe time, and a counter that increments on each + call, so calling this repeatedly returns different values within a + single execution while remaining stable across replays. + + Returns + ------- + uuid.UUID + A replay-safe, deterministically generated GUID. + """ + pass + @abstractmethod def create_timer(self, fire_at: Union[datetime, timedelta]) -> task.Task: """Create a Timer Task to fire after at the specified deadline. diff --git a/tests/ext/workflow/test_dapr_workflow_context.py b/tests/ext/workflow/test_dapr_workflow_context.py index 22350dda8..7236a4588 100644 --- a/tests/ext/workflow/test_dapr_workflow_context.py +++ b/tests/ext/workflow/test_dapr_workflow_context.py @@ -16,6 +16,7 @@ import unittest from datetime import datetime from unittest import mock +from uuid import UUID import dapr.ext.workflow._durabletask.internal.protos as pb from dapr.ext.workflow import PropagatedHistory @@ -54,6 +55,9 @@ def set_custom_status(self, custom_status): def get_propagated_history(self): return self._propagated_history + def new_guid(self): + return UUID('12345678-1234-5678-1234-567812345678') + class DaprWorkflowContextTest(unittest.TestCase): def mock_client_activity(ctx: WorkflowActivityContext, input): @@ -83,6 +87,9 @@ def test_workflow_context_functions(self): dapr_wf_ctx.set_custom_status(mock_custom_status) assert fakeContext.custom_status == mock_custom_status + new_guid_result = dapr_wf_ctx.new_guid() + assert new_guid_result == UUID('12345678-1234-5678-1234-567812345678') + def test_get_propagated_history_proxies_inner_context(self): with mock.patch( 'dapr.ext.workflow._durabletask.worker._RuntimeOrchestrationContext', From 72e282ba93d063185a5f5db1bcc773ec5694d0de Mon Sep 17 00:00:00 2001 From: Akanksha Trehun Date: Fri, 11 Sep 2026 14:11:35 +0530 Subject: [PATCH 02/15] Declare new_guid on the vendored OrchestrationContext interface self.__obj is typed as the vendored task.OrchestrationContext, which never declared new_guid even though the concrete runtime context has had it all along through DeterministicContextMixin. mypy correctly flagged this as attr-defined. Added it as an abstract method there and implemented it directly on _RuntimeOrchestrationContext, since the mixin comes after OrchestrationContext in the MRO and its version would otherwise be shadowed by the abstract one. Signed-off-by: Akanksha Trehun --- dapr/ext/workflow/_durabletask/task.py | 12 ++++++++++++ dapr/ext/workflow/_durabletask/worker.py | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/dapr/ext/workflow/_durabletask/task.py b/dapr/ext/workflow/_durabletask/task.py index e3a190c41..6a3dc7451 100644 --- a/dapr/ext/workflow/_durabletask/task.py +++ b/dapr/ext/workflow/_durabletask/task.py @@ -13,6 +13,7 @@ from __future__ import annotations import math +import uuid from abc import ABC, abstractmethod from datetime import datetime, timedelta from typing import Any, Callable, Generator, Generic, Optional, TypeVar, Union @@ -89,6 +90,17 @@ def set_custom_status(self, custom_status: str) -> None: """ pass + @abstractmethod + def new_guid(self) -> uuid.UUID: + """Create a new GUID that is safe for replay within an orchestration. + + Returns + ------- + uuid.UUID + A new, deterministically generated GUID. + """ + pass + @abstractmethod def create_timer(self, fire_at: Union[datetime, timedelta]) -> Task: """Create a Timer Task to fire after at the specified deadline. diff --git a/dapr/ext/workflow/_durabletask/worker.py b/dapr/ext/workflow/_durabletask/worker.py index 090eb747a..e0c0946e1 100644 --- a/dapr/ext/workflow/_durabletask/worker.py +++ b/dapr/ext/workflow/_durabletask/worker.py @@ -17,6 +17,7 @@ import random import threading import time +import uuid import warnings from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta, timezone @@ -1609,6 +1610,9 @@ def current_utc_datetime(self, value: datetime): def is_replaying(self) -> bool: return self._is_replaying + def new_guid(self) -> uuid.UUID: + return self.uuid4() + def set_custom_status(self, custom_status: str) -> None: if custom_status is not None and not isinstance(custom_status, str): warnings.warn( From fbc20f212d6d1c154675c43f45c6bb87187e591f Mon Sep 17 00:00:00 2001 From: Akanksha Trehun <146705736+magic-peach@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:41:22 +0530 Subject: [PATCH 03/15] Update dapr/ext/workflow/_durabletask/worker.py Co-authored-by: Jeffrey Zhang <72636309+JeffreyJPZ@users.noreply.github.com> Signed-off-by: Akanksha Trehun <146705736+magic-peach@users.noreply.github.com> --- dapr/ext/workflow/_durabletask/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/ext/workflow/_durabletask/worker.py b/dapr/ext/workflow/_durabletask/worker.py index e0c0946e1..98ec8722c 100644 --- a/dapr/ext/workflow/_durabletask/worker.py +++ b/dapr/ext/workflow/_durabletask/worker.py @@ -1611,7 +1611,7 @@ def is_replaying(self) -> bool: return self._is_replaying def new_guid(self) -> uuid.UUID: - return self.uuid4() + return self.new_guid() def set_custom_status(self, custom_status: str) -> None: if custom_status is not None and not isinstance(custom_status, str): From 2bbc04242f3250c3c36ea73da7685e565836981b Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sat, 10 Oct 2026 23:29:24 -0700 Subject: [PATCH 04/15] delegate to mixin Signed-off-by: Jeffrey Zhang --- dapr/ext/workflow/_durabletask/worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dapr/ext/workflow/_durabletask/worker.py b/dapr/ext/workflow/_durabletask/worker.py index 98ec8722c..9b2dd78ff 100644 --- a/dapr/ext/workflow/_durabletask/worker.py +++ b/dapr/ext/workflow/_durabletask/worker.py @@ -1611,7 +1611,9 @@ def is_replaying(self) -> bool: return self._is_replaying def new_guid(self) -> uuid.UUID: - return self.new_guid() + # Call the mixin directly because ``super()`` resolves to the abstract base method first, + # which returns ``None``. + return deterministic.DeterministicContextMixin.new_guid(self) def set_custom_status(self, custom_status: str) -> None: if custom_status is not None and not isinstance(custom_status, str): From 19ad02fdb1b3a1dc1039e274a3ec37d119814b74 Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sat, 10 Oct 2026 23:45:38 -0700 Subject: [PATCH 05/15] add unit test Signed-off-by: Jeffrey Zhang --- tests/ext/workflow/test_dapr_workflow_context.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/ext/workflow/test_dapr_workflow_context.py b/tests/ext/workflow/test_dapr_workflow_context.py index 7236a4588..c7d43881c 100644 --- a/tests/ext/workflow/test_dapr_workflow_context.py +++ b/tests/ext/workflow/test_dapr_workflow_context.py @@ -14,9 +14,9 @@ """ import unittest +import uuid from datetime import datetime from unittest import mock -from uuid import UUID import dapr.ext.workflow._durabletask.internal.protos as pb from dapr.ext.workflow import PropagatedHistory @@ -55,8 +55,6 @@ def set_custom_status(self, custom_status): def get_propagated_history(self): return self._propagated_history - def new_guid(self): - return UUID('12345678-1234-5678-1234-567812345678') class DaprWorkflowContextTest(unittest.TestCase): @@ -87,8 +85,16 @@ def test_workflow_context_functions(self): dapr_wf_ctx.set_custom_status(mock_custom_status) assert fakeContext.custom_status == mock_custom_status - new_guid_result = dapr_wf_ctx.new_guid() - assert new_guid_result == UUID('12345678-1234-5678-1234-567812345678') + def test_new_guid_is_unique_within_execution_and_deterministic_across_replays(self): + instance_id = 'abc' + ctx1 = worker._RuntimeOrchestrationContext(instance_id) + ctx2 = worker._RuntimeOrchestrationContext(instance_id) + g1 = [ctx1.new_guid() for _ in range(3)] + g2 = [ctx2.new_guid() for _ in range(3)] + + assert all(isinstance(g, uuid.UUID) for g in g1) + assert len(set(g1)) == 3 # unique within an execution + assert g1 == g2 # deterministic across replays def test_get_propagated_history_proxies_inner_context(self): with mock.patch( From 4e039932b3217223db530244c8a4f2818005c144 Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sun, 11 Oct 2026 01:08:17 -0700 Subject: [PATCH 06/15] add integration tests for replay safety Signed-off-by: Jeffrey Zhang --- .../test_workflow_replay_safety.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/integration/test_workflow_replay_safety.py diff --git a/tests/integration/test_workflow_replay_safety.py b/tests/integration/test_workflow_replay_safety.py new file mode 100644 index 000000000..c36d76a46 --- /dev/null +++ b/tests/integration/test_workflow_replay_safety.py @@ -0,0 +1,131 @@ +# Copyright 2026 The Dapr 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. + +"""End-to-end verification of replay-safe workflow context behavior. + +These tests run workflows through a real Dapr sidecar. After an activity +completes, the workflow is resumed by replaying its history, so the replay +assertions exercise ``DaprWorkflowContext`` rather than only the underlying +deterministic context. +""" + +from __future__ import annotations + +import json +from typing import Any +from uuid import UUID + +import pytest + +pytestmark = pytest.mark.dapr_head + +HOST = '127.0.0.1' +GRPC_PORT = '13501' +COMPLETION_TIMEOUT_S = 60 +GUID_COUNT = 3 + + +@pytest.fixture(scope='module') +def sidecar(dapr_env): + """Start the Dapr sidecar used by the workflow replay tests.""" + return dapr_env.start_sidecar(app_id='test-workflow-replay-safety') + + +def _run_workflow( + sidecar, workflow, *, activities: tuple[Any, ...] = () +) -> dict[str, Any] | list[str]: + """Run a workflow to completion through a real Dapr sidecar and decode its JSON output.""" + from dapr.ext.workflow import DaprWorkflowClient, WorkflowRuntime + + runtime = WorkflowRuntime(host=HOST, port=GRPC_PORT) + runtime.register_workflow(workflow) + for activity in activities: + runtime.register_activity(activity) + + client = DaprWorkflowClient(host=HOST, port=GRPC_PORT) + runtime.start() + try: + instance_id = client.schedule_new_workflow(workflow) + state = client.wait_for_workflow_completion( + instance_id, timeout_in_seconds=COMPLETION_TIMEOUT_S + ) + finally: + client.close() + runtime.shutdown() + + assert state is not None, 'workflow did not reach a terminal state' + assert state.runtime_status.name == 'COMPLETED' + return json.loads(state.serialized_output) + + +def test_new_guid_generates_unique_guids_within_execution(sidecar): + """Each call to ``new_guid()`` produces a unique GUID within one execution.""" + from dapr.ext.workflow import DaprWorkflowContext, WorkflowActivityContext + + def echo_guids(ctx: WorkflowActivityContext, values: list[str]) -> list[str]: + return values + + def generate_guids(ctx: DaprWorkflowContext, _input: None): + generated_guids = [str(ctx.new_guid()) for _ in range(GUID_COUNT)] + yield ctx.call_activity(echo_guids, input=generated_guids) + return generated_guids + + generated_guids = _run_workflow(sidecar, generate_guids, activities=(echo_guids,)) + + assert isinstance(generated_guids, list) + + # Validate serialized GUIDs + for value in generated_guids: + UUID(value) + + assert len(set(generated_guids)) == len(generated_guids) + + +def test_new_guid_is_deterministic_across_replays(sidecar): + """GUID sequences remain unchanged when the workflow replays its history. + + The echo activity returns the GUIDS from the initial execution. + During replay, that result comes from history instead of re-running the + activity, so comparing the generated and echoed values detects if replay + produced different GUIDs than the original execution. + """ + from dapr.ext.workflow import DaprWorkflowContext, WorkflowActivityContext + + def echo_guids(ctx: WorkflowActivityContext, values: list[str]) -> list[str]: + return values + + def generate_guids(ctx: DaprWorkflowContext, _input: None): + first_guids = [str(ctx.new_guid()) for _ in range(GUID_COUNT)] + first_echo = yield ctx.call_activity(echo_guids, input=first_guids) + + second_guids = [str(ctx.new_guid()) for _ in range(GUID_COUNT)] + second_echo = yield ctx.call_activity(echo_guids, input=second_guids) + + return { + 'first_guids': first_guids, + 'first_echo': first_echo, + 'second_guids': second_guids, + 'second_echo': second_echo, + } + + result = _run_workflow(sidecar, generate_guids, activities=(echo_guids,)) + + assert isinstance(result, dict) + assert result['first_guids'] == result['first_echo'] + assert result['second_guids'] == result['second_echo'] + + # Validate serialized GUIDs + for values in (result['first_guids'], result['second_guids']): + for value in values: + UUID(value) + + all_guids = result['first_guids'] + result['second_guids'] + assert len(set(all_guids)) == len(all_guids) From edc06685dfad9221ab886d251df943e0ab14bf4a Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sun, 11 Oct 2026 01:25:10 -0700 Subject: [PATCH 07/15] format Signed-off-by: Jeffrey Zhang --- tests/ext/workflow/test_dapr_workflow_context.py | 3 +-- tests/integration/test_workflow_replay_safety.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/ext/workflow/test_dapr_workflow_context.py b/tests/ext/workflow/test_dapr_workflow_context.py index c7d43881c..846ff3c56 100644 --- a/tests/ext/workflow/test_dapr_workflow_context.py +++ b/tests/ext/workflow/test_dapr_workflow_context.py @@ -56,7 +56,6 @@ def get_propagated_history(self): return self._propagated_history - class DaprWorkflowContextTest(unittest.TestCase): def mock_client_activity(ctx: WorkflowActivityContext, input): print(f'{input}!', flush=True) @@ -94,7 +93,7 @@ def test_new_guid_is_unique_within_execution_and_deterministic_across_replays(se assert all(isinstance(g, uuid.UUID) for g in g1) assert len(set(g1)) == 3 # unique within an execution - assert g1 == g2 # deterministic across replays + assert g1 == g2 # deterministic across replays def test_get_propagated_history_proxies_inner_context(self): with mock.patch( diff --git a/tests/integration/test_workflow_replay_safety.py b/tests/integration/test_workflow_replay_safety.py index c36d76a46..b77be37fd 100644 --- a/tests/integration/test_workflow_replay_safety.py +++ b/tests/integration/test_workflow_replay_safety.py @@ -11,9 +11,9 @@ """End-to-end verification of replay-safe workflow context behavior. -These tests run workflows through a real Dapr sidecar. After an activity -completes, the workflow is resumed by replaying its history, so the replay -assertions exercise ``DaprWorkflowContext`` rather than only the underlying +These tests run workflows through a real Dapr sidecar. After an activity +completes, the workflow is resumed by replaying its history, so the replay +assertions exercise ``DaprWorkflowContext`` rather than only the underlying deterministic context. """ @@ -127,5 +127,5 @@ def generate_guids(ctx: DaprWorkflowContext, _input: None): for value in values: UUID(value) - all_guids = result['first_guids'] + result['second_guids'] + all_guids = result['first_guids'] + result['second_guids'] assert len(set(all_guids)) == len(all_guids) From dd340b6c37992d7e31a3610acf368c45e89a87d7 Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sun, 11 Oct 2026 01:42:45 -0700 Subject: [PATCH 08/15] adjust comment Signed-off-by: Jeffrey Zhang --- dapr/ext/workflow/_durabletask/worker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dapr/ext/workflow/_durabletask/worker.py b/dapr/ext/workflow/_durabletask/worker.py index 9b2dd78ff..7103e7dcc 100644 --- a/dapr/ext/workflow/_durabletask/worker.py +++ b/dapr/ext/workflow/_durabletask/worker.py @@ -1611,7 +1611,8 @@ def is_replaying(self) -> bool: return self._is_replaying def new_guid(self) -> uuid.UUID: - # Call the mixin directly because ``super()`` resolves to the abstract base method first, + # Call the mixin implementation directly because ``super().new_guid()`` + # first resolves to the abstract method ``OrchestrationContext.new_guid()``, # which returns ``None``. return deterministic.DeterministicContextMixin.new_guid(self) From f976ce38f3811fdae4ccd5e3d27f92c2dd18731d Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sun, 11 Oct 2026 01:51:19 -0700 Subject: [PATCH 09/15] use DaprWorkflowContext in unit test Signed-off-by: Jeffrey Zhang --- .../workflow/test_dapr_workflow_context.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/ext/workflow/test_dapr_workflow_context.py b/tests/ext/workflow/test_dapr_workflow_context.py index 846ff3c56..d210a91ed 100644 --- a/tests/ext/workflow/test_dapr_workflow_context.py +++ b/tests/ext/workflow/test_dapr_workflow_context.py @@ -84,17 +84,6 @@ def test_workflow_context_functions(self): dapr_wf_ctx.set_custom_status(mock_custom_status) assert fakeContext.custom_status == mock_custom_status - def test_new_guid_is_unique_within_execution_and_deterministic_across_replays(self): - instance_id = 'abc' - ctx1 = worker._RuntimeOrchestrationContext(instance_id) - ctx2 = worker._RuntimeOrchestrationContext(instance_id) - g1 = [ctx1.new_guid() for _ in range(3)] - g2 = [ctx2.new_guid() for _ in range(3)] - - assert all(isinstance(g, uuid.UUID) for g in g1) - assert len(set(g1)) == 3 # unique within an execution - assert g1 == g2 # deterministic across replays - def test_get_propagated_history_proxies_inner_context(self): with mock.patch( 'dapr.ext.workflow._durabletask.worker._RuntimeOrchestrationContext', @@ -132,3 +121,14 @@ def test_get_propagated_history_returns_none_when_not_set(self): fake = worker._RuntimeOrchestrationContext(mock_instance_id) dapr_wf_ctx = DaprWorkflowContext(fake) assert dapr_wf_ctx.get_propagated_history() is None + + def test_new_guid_is_unique_within_execution_and_deterministic_across_replays(self): + instance_id = 'abc' + ctx1 = DaprWorkflowContext(worker._RuntimeOrchestrationContext(instance_id)) + ctx2 = DaprWorkflowContext(worker._RuntimeOrchestrationContext(instance_id)) + g1 = [ctx1.new_guid() for _ in range(3)] + g2 = [ctx2.new_guid() for _ in range(3)] + + assert all(isinstance(g, uuid.UUID) for g in g1) + assert len(set(g1)) == 3 # unique within an execution + assert g1 == g2 # deterministic across replays From 42c69c7354a62a1a8b0f69736be4f5b8f74ae34f Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sun, 11 Oct 2026 02:56:08 -0700 Subject: [PATCH 10/15] add debug log Signed-off-by: Jeffrey Zhang --- dapr/ext/workflow/dapr_workflow_context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dapr/ext/workflow/dapr_workflow_context.py b/dapr/ext/workflow/dapr_workflow_context.py index 3cde70d55..16e8050ce 100644 --- a/dapr/ext/workflow/dapr_workflow_context.py +++ b/dapr/ext/workflow/dapr_workflow_context.py @@ -59,6 +59,7 @@ def set_custom_status(self, custom_status: str) -> None: self.__obj.set_custom_status(custom_status) def new_guid(self) -> UUID: + self._logger.debug(f'{self.instance_id}: Generating new GUID') return self.__obj.new_guid() def create_timer(self, fire_at: Union[datetime, timedelta]) -> task.Task: From e3dea6404bb8c43550f653c70a9cba914c79e819 Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sun, 11 Oct 2026 10:57:21 -0700 Subject: [PATCH 11/15] comment consistency Signed-off-by: Jeffrey Zhang --- tests/integration/test_workflow_replay_safety.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_workflow_replay_safety.py b/tests/integration/test_workflow_replay_safety.py index b77be37fd..1bbccd355 100644 --- a/tests/integration/test_workflow_replay_safety.py +++ b/tests/integration/test_workflow_replay_safety.py @@ -95,7 +95,7 @@ def test_new_guid_is_deterministic_across_replays(sidecar): The echo activity returns the GUIDS from the initial execution. During replay, that result comes from history instead of re-running the activity, so comparing the generated and echoed values detects if replay - produced different GUIDs than the original execution. + produced different GUIDs than the initial execution. """ from dapr.ext.workflow import DaprWorkflowContext, WorkflowActivityContext From 467655343fff4dcb877b318ecd5b1224ca90d0bc Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Sun, 11 Oct 2026 18:24:58 -0700 Subject: [PATCH 12/15] update test comment Signed-off-by: Jeffrey Zhang --- tests/integration/test_workflow_replay_safety.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_workflow_replay_safety.py b/tests/integration/test_workflow_replay_safety.py index 1bbccd355..20e516594 100644 --- a/tests/integration/test_workflow_replay_safety.py +++ b/tests/integration/test_workflow_replay_safety.py @@ -93,8 +93,8 @@ def test_new_guid_is_deterministic_across_replays(sidecar): """GUID sequences remain unchanged when the workflow replays its history. The echo activity returns the GUIDS from the initial execution. - During replay, that result comes from history instead of re-running the - activity, so comparing the generated and echoed values detects if replay + During replay, that result comes from history instead of executing the + activity again, so comparing the generated and echoed values detects if replay produced different GUIDs than the initial execution. """ from dapr.ext.workflow import DaprWorkflowContext, WorkflowActivityContext From 84d358dfc25c2a4ffeb6f0e5e27fc22df95f004e Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Mon, 12 Oct 2026 11:10:04 -0700 Subject: [PATCH 13/15] remove unneeded sidecar param from helper Signed-off-by: Jeffrey Zhang --- tests/integration/test_workflow_replay_safety.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_workflow_replay_safety.py b/tests/integration/test_workflow_replay_safety.py index 20e516594..a29953abe 100644 --- a/tests/integration/test_workflow_replay_safety.py +++ b/tests/integration/test_workflow_replay_safety.py @@ -39,9 +39,7 @@ def sidecar(dapr_env): return dapr_env.start_sidecar(app_id='test-workflow-replay-safety') -def _run_workflow( - sidecar, workflow, *, activities: tuple[Any, ...] = () -) -> dict[str, Any] | list[str]: +def _run_workflow(workflow, *, activities: tuple[Any, ...] = ()) -> dict[str, Any] | list[str]: """Run a workflow to completion through a real Dapr sidecar and decode its JSON output.""" from dapr.ext.workflow import DaprWorkflowClient, WorkflowRuntime @@ -78,7 +76,7 @@ def generate_guids(ctx: DaprWorkflowContext, _input: None): yield ctx.call_activity(echo_guids, input=generated_guids) return generated_guids - generated_guids = _run_workflow(sidecar, generate_guids, activities=(echo_guids,)) + generated_guids = _run_workflow(generate_guids, activities=(echo_guids,)) assert isinstance(generated_guids, list) @@ -116,7 +114,7 @@ def generate_guids(ctx: DaprWorkflowContext, _input: None): 'second_echo': second_echo, } - result = _run_workflow(sidecar, generate_guids, activities=(echo_guids,)) + result = _run_workflow(generate_guids, activities=(echo_guids,)) assert isinstance(result, dict) assert result['first_guids'] == result['first_echo'] From bb0187f99de9fc1e817471ab7df01ae8d6f96930 Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Mon, 12 Oct 2026 11:26:03 -0700 Subject: [PATCH 14/15] make guid count local var Signed-off-by: Jeffrey Zhang --- tests/ext/workflow/test_dapr_workflow_context.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/ext/workflow/test_dapr_workflow_context.py b/tests/ext/workflow/test_dapr_workflow_context.py index d210a91ed..0cf25cede 100644 --- a/tests/ext/workflow/test_dapr_workflow_context.py +++ b/tests/ext/workflow/test_dapr_workflow_context.py @@ -126,9 +126,10 @@ def test_new_guid_is_unique_within_execution_and_deterministic_across_replays(se instance_id = 'abc' ctx1 = DaprWorkflowContext(worker._RuntimeOrchestrationContext(instance_id)) ctx2 = DaprWorkflowContext(worker._RuntimeOrchestrationContext(instance_id)) - g1 = [ctx1.new_guid() for _ in range(3)] - g2 = [ctx2.new_guid() for _ in range(3)] + guid_count = 3 + g1 = [ctx1.new_guid() for _ in range(guid_count)] + g2 = [ctx2.new_guid() for _ in range(guid_count)] assert all(isinstance(g, uuid.UUID) for g in g1) - assert len(set(g1)) == 3 # unique within an execution + assert len(set(g1)) == guid_count # unique within an execution assert g1 == g2 # deterministic across replays From 1efbae325c48fe883cf4f289ed15e955228ca183 Mon Sep 17 00:00:00 2001 From: Jeffrey Zhang Date: Mon, 12 Oct 2026 11:34:44 -0700 Subject: [PATCH 15/15] address comments Signed-off-by: Jeffrey Zhang --- dapr/ext/workflow/AGENTS.md | 1 + tests/integration/test_workflow_replay_safety.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dapr/ext/workflow/AGENTS.md b/dapr/ext/workflow/AGENTS.md index 0a58a89fa..9c32b8dd6 100644 --- a/dapr/ext/workflow/AGENTS.md +++ b/dapr/ext/workflow/AGENTS.md @@ -164,6 +164,7 @@ Passed to workflow functions as the first argument: - `create_timer(fire_at)` → `Task` (accepts `datetime` or `timedelta`) - `wait_for_external_event(name)` → `Task` - `set_custom_status(status)` / `continue_as_new(new_input, *, save_events)` +- `new_guid()` → `uuid.UUID` — deterministic across workflow replays Module-level functions: - `when_all(tasks)` → `WhenAllTask` — wait for all tasks to complete diff --git a/tests/integration/test_workflow_replay_safety.py b/tests/integration/test_workflow_replay_safety.py index a29953abe..ab6e97fa4 100644 --- a/tests/integration/test_workflow_replay_safety.py +++ b/tests/integration/test_workflow_replay_safety.py @@ -25,8 +25,6 @@ import pytest -pytestmark = pytest.mark.dapr_head - HOST = '127.0.0.1' GRPC_PORT = '13501' COMPLETION_TIMEOUT_S = 60