durable-workflow 2.5.5__tar.gz → 2.5.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. {durable_workflow-2.5.5/src/durable_workflow.egg-info → durable_workflow-2.5.7}/PKG-INFO +11 -9
  2. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/README.md +10 -8
  3. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/pyproject.toml +4 -4
  4. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/client.py +2 -1
  5. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/worker.py +25 -10
  6. {durable_workflow-2.5.5 → durable_workflow-2.5.7/src/durable_workflow.egg-info}/PKG-INFO +11 -9
  7. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_opening.py +2 -2
  8. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_client.py +6 -1
  9. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation_client.py +4 -7
  10. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation_worker.py +1 -1
  11. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_remote_worker.py +3 -3
  12. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_client.py +2 -2
  13. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_release_metadata.py +2 -2
  14. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay_regression_corpus.py +42 -1
  15. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_worker.py +88 -1
  16. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/LICENSE +0 -0
  17. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/setup.cfg +0 -0
  18. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/__init__.py +0 -0
  19. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_activity_process.py +0 -0
  20. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_avro.py +0 -0
  21. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cancellation_scope.py +0 -0
  22. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  23. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  24. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_prepared_local_activity.py +0 -0
  25. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  26. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/activity.py +0 -0
  27. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/auth_composition.py +0 -0
  28. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/cancellation.py +0 -0
  29. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/errors.py +0 -0
  30. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_storage.py +0 -0
  31. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_task_input.py +0 -0
  32. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/external_task_result.py +0 -0
  33. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/history_bundle_verify.py +0 -0
  34. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/interceptors.py +0 -0
  35. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/invocable.py +0 -0
  36. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/metrics.py +0 -0
  37. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/nexus.py +0 -0
  38. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/py.typed +0 -0
  39. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/python_conformance.py +0 -0
  40. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/replay_conformance.py +0 -0
  41. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/replay_verify.py +0 -0
  42. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/retry_policy.py +0 -0
  43. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/serializer.py +0 -0
  44. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/sync.py +0 -0
  45. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/testing.py +0 -0
  46. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/worker_session.py +0 -0
  47. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/workflow.py +0 -0
  48. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  49. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  50. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  51. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  52. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/requires.txt +0 -0
  53. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/src/durable_workflow.egg-info/top_level.txt +0 -0
  54. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_activity_cancellation_policies.py +0 -0
  55. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_activity_context.py +0 -0
  56. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_activity_process.py +0 -0
  57. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_auth_composition.py +0 -0
  58. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_avro_value_benchmark.py +0 -0
  59. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_avro_value_protocol.py +0 -0
  60. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_context.py +0 -0
  61. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_remaining_time.py +0 -0
  62. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_admission.py +0 -0
  63. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_authoring.py +0 -0
  64. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cancellation_scope_boundary.py +0 -0
  65. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_child_workflow_policies.py +0 -0
  66. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_ci_checkout.py +0 -0
  67. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_ci_integration_endpoint.py +0 -0
  68. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_client_timeouts.py +0 -0
  69. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_codec_regression_corpus.py +0 -0
  70. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  71. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_groups.py +0 -0
  72. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_history.py +0 -0
  73. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_committed_cancellation_scope_replay.py +0 -0
  74. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_condition_replay_drift.py +0 -0
  75. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_control_plane_parity_fixtures.py +0 -0
  76. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_cooperative_cancellation.py +0 -0
  77. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_docstring_cross_references.py +0 -0
  78. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_durable_selection.py +0 -0
  79. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_errors.py +0 -0
  80. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_external_storage.py +0 -0
  81. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_external_task_input.py +0 -0
  82. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_external_task_result.py +0 -0
  83. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_golden_history_replay.py +0 -0
  84. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_history_bundle_verify.py +0 -0
  85. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_history_event_contract.py +0 -0
  86. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_invocable.py +0 -0
  87. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_message_streams.py +0 -0
  88. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_metrics.py +0 -0
  89. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_nexus_namespace.py +0 -0
  90. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_nexus_wire.py +0 -0
  91. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_order_processing_example.py +0 -0
  92. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_parallel_completion_order_replay.py +0 -0
  93. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_parallel_saga_authoring.py +0 -0
  94. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_patch_history.py +0 -0
  95. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_pending_operation_replay.py +0 -0
  96. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_groups.py +0 -0
  97. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_policies.py +0 -0
  98. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_prepared_local_activity_worker.py +0 -0
  99. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_public_boundary_scanner.py +0 -0
  100. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_pypi_project_surface.py +0 -0
  101. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_python_conformance.py +0 -0
  102. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_queries.py +0 -0
  103. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_readme_quickstart.py +0 -0
  104. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_redrive_replay.py +0 -0
  105. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_registration_ambiguity.py +0 -0
  106. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay.py +0 -0
  107. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay_conformance.py +0 -0
  108. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_replay_verify.py +0 -0
  109. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_retry_policy.py +0 -0
  110. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_run_cancellation_scope_replay.py +0 -0
  111. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_runtime_external_payload_transport.py +0 -0
  112. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_schedules.py +0 -0
  113. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_scoped_run_cancellation_context.py +0 -0
  114. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_serializer.py +0 -0
  115. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_signals.py +0 -0
  116. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_sleep.py +0 -0
  117. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_standalone_activity_client.py +0 -0
  118. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_sticky_execution.py +0 -0
  119. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_storage_admission.py +0 -0
  120. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_sync.py +0 -0
  121. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_testing_harness.py +0 -0
  122. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_update_signal_condition_replay.py +0 -0
  123. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_updates.py +0 -0
  124. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_wait_condition.py +0 -0
  125. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_worker_session.py +0 -0
  126. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_entrypoint_validation.py +0 -0
  127. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_result_timeout.py +0 -0
  128. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_streams.py +0 -0
  129. {durable_workflow-2.5.5 → durable_workflow-2.5.7}/tests/test_workflow_updates_conformance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.5.5
3
+ Version: 2.5.7
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -155,12 +155,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
155
155
  for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
156
156
  documents protocol and upgrade guarantees.
157
157
 
158
- ## Cooperative cancellation release candidate
158
+ ## Worker protocol and cooperative cancellation
159
+
160
+ Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
161
+ `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
162
+ without enabling optional worker features.
159
163
 
160
164
  Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
161
165
  against a Server that advertises protocol 1.20 and the required capabilities:
162
- set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` and include
163
- `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
166
+ include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
164
167
  admission also needs `prepared_local_activities`, with
165
168
  `prepared_local_activity_cancellation_policies` for explicit local policies.
166
169
  Independently cancellable scopes remain disabled.
@@ -196,11 +199,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
196
199
  authority and exact source provenance. An optional exact `native_commit` mounts
197
200
  that public Native checkout read-only into the test stack. The image's published
198
201
  Composer authority stays intact and the evidence identifies the source overlay.
199
- These are source qualification runs. For a local
200
- candidate, set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` before starting
201
- Compose and `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases
202
- fail if the runtime does not discover the required capability. Ordinary CI
203
- keeps protocol 1.19 and skips this unpublished feature's connected cases.
202
+ These are source qualification runs. For a local candidate, set
203
+ `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
204
+ runtime does not discover the required capability. Ordinary CI uses the SDK's
205
+ default protocol and skips the optional connected cooperative cases.
204
206
 
205
207
  ## License
206
208
 
@@ -115,12 +115,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
115
115
  for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
116
116
  documents protocol and upgrade guarantees.
117
117
 
118
- ## Cooperative cancellation release candidate
118
+ ## Worker protocol and cooperative cancellation
119
+
120
+ Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
121
+ `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
122
+ without enabling optional worker features.
119
123
 
120
124
  Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
121
125
  against a Server that advertises protocol 1.20 and the required capabilities:
122
- set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` and include
123
- `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
126
+ include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
124
127
  admission also needs `prepared_local_activities`, with
125
128
  `prepared_local_activity_cancellation_policies` for explicit local policies.
126
129
  Independently cancellable scopes remain disabled.
@@ -156,11 +159,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
156
159
  authority and exact source provenance. An optional exact `native_commit` mounts
157
160
  that public Native checkout read-only into the test stack. The image's published
158
161
  Composer authority stays intact and the evidence identifies the source overlay.
159
- These are source qualification runs. For a local
160
- candidate, set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` before starting
161
- Compose and `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases
162
- fail if the runtime does not discover the required capability. Ordinary CI
163
- keeps protocol 1.19 and skips this unpublished feature's connected cases.
162
+ These are source qualification runs. For a local candidate, set
163
+ `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
164
+ runtime does not discover the required capability. Ordinary CI uses the SDK's
165
+ default protocol and skips the optional connected cooperative cases.
164
166
 
165
167
  ## License
166
168
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.5.5"
7
+ version = "2.5.7"
8
8
  description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -71,10 +71,10 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
71
71
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
72
72
 
73
73
  [tool.durable-workflow]
74
- product-train = "2.5.5"
75
- registry-version = "2.5.5"
74
+ product-train = "2.5.7"
75
+ registry-version = "2.5.7"
76
76
  supported-server-versions = "2.5.16"
77
- worker-protocol-version = "1.19"
77
+ worker-protocol-version = "1.20"
78
78
  control-plane-version = "2"
79
79
  durable-selection = true
80
80
  durable-selection-minimum-worker-protocol-version = "1.19"
@@ -72,7 +72,8 @@ from .nexus import NexusOperationResult, nexus_request_payload
72
72
  from .retry_policy import TransportRetryPolicy
73
73
  from .worker_session import WorkerSessionOptions
74
74
 
75
- PROTOCOL_VERSION = "1.19"
75
+ PROTOCOL_VERSION = "1.20"
76
+ MINIMUM_WORKER_PROTOCOL_VERSION = "1.19"
76
77
  CONTROL_PLANE_VERSION = "2"
77
78
  PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST: dict[str, dict[str, str | bool]] = {
78
79
  "local_activities": {
@@ -55,6 +55,7 @@ from .client import (
55
55
  CONTROL_PLANE_REQUEST_CONTRACT_VERSION,
56
56
  CONTROL_PLANE_VERSION,
57
57
  DEFAULT_SDK_VERSION,
58
+ MINIMUM_WORKER_PROTOCOL_VERSION,
58
59
  PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST,
59
60
  PROTOCOL_VERSION,
60
61
  Client,
@@ -877,6 +878,16 @@ def _server_protocol_compatible(server_version: str, sdk_version: str) -> bool:
877
878
 
878
879
 
879
880
  def _validate_server_compatibility(info: dict[str, Any]) -> None:
881
+ requested_protocol = _protocol_version_from_env(
882
+ "DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", PROTOCOL_VERSION,
883
+ )
884
+ if requested_protocol not in (MINIMUM_WORKER_PROTOCOL_VERSION, PROTOCOL_VERSION):
885
+ raise RuntimeError(
886
+ "Server compatibility error: sdk-python supports worker protocol "
887
+ f"{MINIMUM_WORKER_PROTOCOL_VERSION} or protocol {PROTOCOL_VERSION}; "
888
+ f"requested {requested_protocol!r}."
889
+ )
890
+
880
891
  control_plane = info.get("control_plane")
881
892
  if not isinstance(control_plane, dict):
882
893
  raise RuntimeError(
@@ -914,15 +925,15 @@ def _validate_server_compatibility(info: dict[str, Any]) -> None:
914
925
  if not isinstance(worker_protocol, dict):
915
926
  raise RuntimeError(
916
927
  "Server compatibility error: missing worker_protocol manifest; "
917
- f"sdk-python requires worker_protocol.version {PROTOCOL_VERSION}."
928
+ f"sdk-python requires worker_protocol.version {requested_protocol}."
918
929
  )
919
930
 
920
931
  worker_protocol_version = _manifest_version(worker_protocol)
921
- if not _server_protocol_compatible(worker_protocol_version, PROTOCOL_VERSION):
932
+ if not _server_protocol_compatible(worker_protocol_version, requested_protocol):
922
933
  raise RuntimeError(
923
934
  "Server compatibility error: incompatible worker_protocol.version "
924
935
  f"{worker_protocol_version!r}; sdk-python requires major-equal and "
925
- f"minor>={PROTOCOL_VERSION!r}."
936
+ f"minor>={requested_protocol!r}."
926
937
  )
927
938
 
928
939
  auth_composition = info.get("auth_composition_contract")
@@ -4077,11 +4088,12 @@ class Worker:
4077
4088
  try:
4078
4089
  if self._stop.is_set():
4079
4090
  return
4080
- wf_loop = asyncio.create_task(self._poll_workflow_tasks())
4081
- act_loop = asyncio.create_task(self._poll_activity_tasks())
4082
- hb_loop = asyncio.create_task(self._heartbeat_loop())
4083
- loops = [wf_loop, act_loop, hb_loop]
4084
- if self._query_tasks_supported:
4091
+ loops = [asyncio.create_task(self._heartbeat_loop())]
4092
+ if self.workflows:
4093
+ loops.append(asyncio.create_task(self._poll_workflow_tasks()))
4094
+ if self.activities:
4095
+ loops.append(asyncio.create_task(self._poll_activity_tasks()))
4096
+ if self.workflows and self._query_tasks_supported:
4085
4097
  if self._can_clone_client_for_query_tasks():
4086
4098
  self._start_query_task_thread()
4087
4099
  else:
@@ -4243,7 +4255,7 @@ class Worker:
4243
4255
  if self._stop.is_set():
4244
4256
  raise asyncio.CancelledError
4245
4257
  background_tasks.append(asyncio.create_task(self._heartbeat_loop()))
4246
- if self._query_tasks_supported:
4258
+ if self.workflows and self._query_tasks_supported:
4247
4259
  if self._can_clone_client_for_query_tasks():
4248
4260
  self._start_query_task_thread()
4249
4261
  else:
@@ -4295,7 +4307,7 @@ class Worker:
4295
4307
  f"workflow {workflow_id} not terminal after {timeout}s (status={desc.status})"
4296
4308
  )
4297
4309
 
4298
- if next_task_kind == "workflow":
4310
+ if self.workflows and (next_task_kind == "workflow" or not self.activities):
4299
4311
  await self._reserve_workflow_capacity()
4300
4312
  try:
4301
4313
  poll_start = time.perf_counter()
@@ -4347,6 +4359,9 @@ class Worker:
4347
4359
  next_task_kind = "workflow"
4348
4360
  continue
4349
4361
 
4362
+ if not self.activities:
4363
+ await asyncio.sleep(poll_interval)
4364
+ continue
4350
4365
  self._validate_cooperative_activity_handlers()
4351
4366
  if len(self._remote_activity_processes) >= self.max_concurrent_activity_tasks:
4352
4367
  next_task_kind = "workflow"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.5.5
3
+ Version: 2.5.7
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -155,12 +155,15 @@ capabilities with Server at startup. Use stable `2.x` SDK and Server channels
155
155
  for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
156
156
  documents protocol and upgrade guarantees.
157
157
 
158
- ## Cooperative cancellation release candidate
158
+ ## Worker protocol and cooperative cancellation
159
+
160
+ Workers use protocol 1.20 by default. For a Server that supports only 1.19, set
161
+ `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.19`. This changes the request protocol
162
+ without enabling optional worker features.
159
163
 
160
164
  Use `request_cancellation()` for bounded, replayable workflow cleanup. Opt in
161
165
  against a Server that advertises protocol 1.20 and the required capabilities:
162
- set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` and include
163
- `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
166
+ include `cooperative_cancellation` in the Worker's `capabilities`. Durable local callback
164
167
  admission also needs `prepared_local_activities`, with
165
168
  `prepared_local_activity_cancellation_policies` for explicit local policies.
166
169
  Independently cancellable scopes remain disabled.
@@ -196,11 +199,10 @@ to true. CI verifies that checkout, builds the candidate Server, enables protoco
196
199
  authority and exact source provenance. An optional exact `native_commit` mounts
197
200
  that public Native checkout read-only into the test stack. The image's published
198
201
  Composer authority stays intact and the evidence identifies the source overlay.
199
- These are source qualification runs. For a local
200
- candidate, set `DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION=1.20` before starting
201
- Compose and `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases
202
- fail if the runtime does not discover the required capability. Ordinary CI
203
- keeps protocol 1.19 and skips this unpublished feature's connected cases.
202
+ These are source qualification runs. For a local candidate, set
203
+ `DURABLE_WORKFLOW_COOPERATIVE_QUALIFICATION=1` for pytest. These cases fail if the
204
+ runtime does not discover the required capability. Ordinary CI uses the SDK's
205
+ default protocol and skips the optional connected cooperative cases.
204
206
 
205
207
  ## License
206
208
 
@@ -152,8 +152,8 @@ async def test_invalid_authority_fails_before_io(change: dict[str, Any]) -> None
152
152
  send.assert_not_awaited()
153
153
 
154
154
 
155
- async def test_published_protocol_refuses_opening_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
156
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION")
155
+ async def test_previous_protocol_refuses_opening_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
156
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
157
157
  async with Client("http://server", namespace="tenant") as client:
158
158
  with patch.object(client, "_request", new_callable=AsyncMock) as send:
159
159
  with pytest.raises(ValueError, match="1.20"):
@@ -164,10 +164,15 @@ class TestHeaders:
164
164
 
165
165
  def test_worker_headers(self, client: Client) -> None:
166
166
  h = client._headers(worker=True)
167
- assert PROTOCOL_VERSION == "1.19"
167
+ assert PROTOCOL_VERSION == "1.20"
168
168
  assert h["X-Durable-Workflow-Protocol-Version"] == PROTOCOL_VERSION
169
169
  assert "X-Durable-Workflow-Control-Plane-Version" not in h
170
170
 
171
+ def test_explicit_previous_worker_protocol(self, client: Client, monkeypatch: pytest.MonkeyPatch) -> None:
172
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
173
+ assert client._headers(worker=True)["X-Durable-Workflow-Protocol-Version"] == "1.19"
174
+ assert client._headers(worker=False)["X-Durable-Workflow-Control-Plane-Version"] == "2"
175
+
171
176
  def test_conformance_protocol_header_overrides(self, client: Client, monkeypatch: pytest.MonkeyPatch) -> None:
172
177
  monkeypatch.setenv("DURABLE_WORKFLOW_CONTROL_PLANE_VERSION", "3")
173
178
  monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "2.0")
@@ -406,14 +406,11 @@ async def test_activity_stop_receipt_rejects_invalid_identity_before_io(
406
406
 
407
407
 
408
408
  @pytest.mark.asyncio
409
- @pytest.mark.parametrize("version", [None, "1.19", "2.20", "invalid"])
410
- async def test_activity_stop_receipt_requires_explicit_worker_opt_in(
411
- client: Client, monkeypatch: pytest.MonkeyPatch, version: str | None,
409
+ @pytest.mark.parametrize("version", ["1.19", "2.20", "invalid"])
410
+ async def test_activity_stop_receipt_requires_compatible_protocol(
411
+ client: Client, monkeypatch: pytest.MonkeyPatch, version: str,
412
412
  ) -> None:
413
- if version is None:
414
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
415
- else:
416
- monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
413
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
417
414
  with patch.object(client._http, "request", new_callable=AsyncMock) as send, pytest.raises(ValueError):
418
415
  await client.acknowledge_activity_cancellation(
419
416
  task_id="task/1", activity_attempt_id="attempt-1", lease_owner="worker-1", request_id="original-request",
@@ -134,7 +134,7 @@ class ClaimServer:
134
134
  return {"outcome": "completed"}
135
135
 
136
136
  async def worker(self, monkeypatch: pytest.MonkeyPatch, **kwargs: Any) -> Worker:
137
- monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.20")
137
+ monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
138
138
  # These transport/replay fixtures use local callbacks that share mock
139
139
  # state with this process. They never poll or advertise remote handlers.
140
140
  local_callbacks = kwargs.pop("activities", [])
@@ -94,7 +94,7 @@ async def application_failure(kind: str) -> None:
94
94
 
95
95
  @pytest_asyncio.fixture
96
96
  async def owner(monkeypatch: pytest.MonkeyPatch): # type: ignore[no-untyped-def]
97
- monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.20")
97
+ monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
98
98
  client = AsyncMock(spec=Client)
99
99
  client.get_cluster_info.return_value = compatible_cluster_info(worker_protocol={
100
100
  "version": "1.20", "server_capabilities": {"cooperative_cancellation": True},
@@ -317,8 +317,8 @@ async def test_client_observation_keeps_worker_credentials_namespace_and_fence(m
317
317
  assert send.await_args.kwargs["json"] == {"activity_attempt_id": "attempt", "lease_owner": "owner"}
318
318
 
319
319
 
320
- async def test_client_observation_requires_explicit_protocol_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
321
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", raising=False)
320
+ async def test_client_observation_refuses_previous_protocol_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
321
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
322
322
  async with Client("http://runtime.test") as client:
323
323
  with (patch.object(client._http, "request", new_callable=AsyncMock) as send,
324
324
  pytest.raises(ValueError, match="1.20")):
@@ -66,8 +66,8 @@ async def test_invalid_claim_or_authority_never_starts_transport(change: dict[st
66
66
  send.assert_not_awaited()
67
67
 
68
68
 
69
- async def test_default_protocol_refuses_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
70
- monkeypatch.delenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION")
69
+ async def test_previous_protocol_refuses_before_io(monkeypatch: pytest.MonkeyPatch) -> None:
70
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
71
71
  async with Client("http://server") as client:
72
72
  with patch.object(client, "_request", new_callable=AsyncMock) as send:
73
73
  with pytest.raises(ValueError, match="1.20"):
@@ -45,13 +45,13 @@ def test_worker_release_identity_matches_supported_server_and_protocol() -> None
45
45
  assert release["product-train"] == project["version"]
46
46
  assert release["registry-version"] == project["version"].replace("-rc.", "rc")
47
47
  assert check_release_metadata.VERSION_PATTERN.fullmatch(release["supported-server-versions"]) is not None
48
- assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.19"
48
+ assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.20"
49
49
  assert release["durable-selection"] is True
50
50
  assert release["durable-selection-minimum-worker-protocol-version"] == "1.19"
51
51
 
52
52
  assert declared_runtime_protocol_version(
53
53
  (REPO_ROOT / "src" / "durable_workflow" / "client.py").read_text(encoding="utf-8")
54
- ) == "1.19"
54
+ ) == "1.20"
55
55
 
56
56
 
57
57
  def pypi_json(source: SourceMetadata, **overrides: object) -> bytes:
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock
9
9
  import pytest
10
10
 
11
11
  from durable_workflow import Replayer, Worker, serializer, workflow
12
- from durable_workflow.client import Client, WorkflowStreamAppendItem
12
+ from durable_workflow.client import PROTOCOL_VERSION, Client, WorkflowExecution, WorkflowStreamAppendItem
13
13
  from durable_workflow.errors import NonDeterministicReplayError, WorkflowCancelled, WorkflowPayloadDecodeError
14
14
  from durable_workflow.workflow import (
15
15
  LocalActivityExecutionAborted,
@@ -31,6 +31,7 @@ from tests.test_update_signal_condition_replay import (
31
31
  PostConditionReceiversWorkflow,
32
32
  UpdateSignalConditionTimerWorkflow,
33
33
  )
34
+ from tests.test_worker import compatible_cluster_info
34
35
 
35
36
  FIXTURE_SCHEMA = "durable-workflow.replay-regression/v1"
36
37
  FIXTURE_DIR = Path(__file__).parent / "fixtures" / "replay_regressions"
@@ -489,6 +490,46 @@ def test_checked_in_replay_regression_corpus_uses_official_replayer(
489
490
  _execute_fixture(fixture)
490
491
 
491
492
 
493
+ @pytest.mark.asyncio
494
+ async def test_workflow_only_driver_resumes_external_activity_without_activity_polling() -> None:
495
+ fixture = json.loads(
496
+ (FIXTURE_DIR / "workflow-only-worker-resumes-external-activity.json").read_text(encoding="utf-8")
497
+ )
498
+ client = AsyncMock(spec=Client)
499
+ client.get_cluster_info.return_value = compatible_cluster_info(worker_protocol={"version": PROTOCOL_VERSION})
500
+ client.register_worker.return_value = {"registered": True}
501
+ client.deregister_worker_registration.return_value = {"outcome": "deregistered"}
502
+ client.poll_activity_task.side_effect = AssertionError("workflow-only worker must not poll activities")
503
+ client.poll_workflow_task.side_effect = [None, {
504
+ "task_id": "external-activity-resume-task", "workflow_id": "external-activity-caller",
505
+ "run_id": "original-run", "workflow_type": fixture["workflow"]["type"],
506
+ "workflow_task_attempt": 1, "history_events": fixture["history"],
507
+ "arguments": serializer.encode(fixture["workflow"]["input"], codec="avro"),
508
+ "payload_codec": "avro",
509
+ }]
510
+ client.complete_workflow_task.return_value = {"outcome": "completed"}
511
+
512
+ async def describe(workflow_id: str) -> WorkflowExecution:
513
+ return WorkflowExecution(
514
+ workflow_id=workflow_id, run_id="original-run", workflow_type=fixture["workflow"]["type"],
515
+ status="completed" if client.complete_workflow_task.await_count else "running",
516
+ )
517
+
518
+ client.describe_workflow.side_effect = describe
519
+ worker = Worker(client, task_queue="external-activity", workflows=[GoldenSingleActivityWorkflow], activities=[])
520
+
521
+ result = await worker.run_until(workflow_id="external-activity-caller", timeout=1, poll_interval=0.01)
522
+
523
+ assert result.status == "completed"
524
+ assert result.run_id == "original-run"
525
+ assert client.poll_workflow_task.await_count == 2
526
+ client.poll_activity_task.assert_not_awaited()
527
+ client.complete_workflow_task.assert_awaited_once()
528
+ commands = client.complete_workflow_task.call_args.kwargs["commands"]
529
+ assert [command["type"] for command in commands] == [fixture["expected"]["type"]]
530
+ assert serializer.decode_envelope(commands[0]["result"]) == fixture["expected"]["result"]
531
+
532
+
492
533
  @pytest.mark.asyncio
493
534
  async def test_worker_replays_recorded_side_effects_across_history_pages() -> None:
494
535
  fixture = json.loads((FIXTURE_DIR / "paged-recorded-side-effects.json").read_text(encoding="utf-8"))
@@ -1114,6 +1114,39 @@ class TestWorkerRegistration:
1114
1114
  await worker._register()
1115
1115
  mock_client.register_worker.assert_not_called()
1116
1116
 
1117
+ @pytest.mark.asyncio
1118
+ async def test_register_accepts_explicit_previous_protocol(
1119
+ self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch,
1120
+ ) -> None:
1121
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", "1.19")
1122
+ mock_client.get_cluster_info = AsyncMock(
1123
+ return_value=compatible_cluster_info(worker_protocol={"version": "1.19"})
1124
+ )
1125
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
1126
+ await worker._register()
1127
+ mock_client.register_worker.assert_called_once()
1128
+
1129
+ @pytest.mark.asyncio
1130
+ async def test_default_protocol_refuses_previous_server_before_registration(self, mock_client: AsyncMock) -> None:
1131
+ mock_client.get_cluster_info = AsyncMock(
1132
+ return_value=compatible_cluster_info(worker_protocol={"version": "1.19"})
1133
+ )
1134
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
1135
+ with pytest.raises(RuntimeError, match="minor>='1.20'"):
1136
+ await worker._register()
1137
+ mock_client.register_worker.assert_not_called()
1138
+
1139
+ @pytest.mark.asyncio
1140
+ @pytest.mark.parametrize("version", ["1.18", "1.21", "2.0"])
1141
+ async def test_register_rejects_unsupported_protocol_override(
1142
+ self, mock_client: AsyncMock, monkeypatch: pytest.MonkeyPatch, version: str,
1143
+ ) -> None:
1144
+ monkeypatch.setenv("DURABLE_WORKFLOW_WORKER_PROTOCOL_VERSION", version)
1145
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow], activities=[])
1146
+ with pytest.raises(RuntimeError, match="supports worker protocol 1.19 or protocol 1.20"):
1147
+ await worker._register()
1148
+ mock_client.register_worker.assert_not_called()
1149
+
1117
1150
  @pytest.mark.asyncio
1118
1151
  async def test_register_accepts_higher_compatible_minor_protocol(self, mock_client: AsyncMock) -> None:
1119
1152
  # Server is one minor ahead of the SDK. MINOR bumps in workflow:v2's
@@ -4403,6 +4436,8 @@ class TestWorkerShutdown:
4403
4436
  client,
4404
4437
  task_queue="orders",
4405
4438
  worker_id="worker-only",
4439
+ workflows=[TestWorkflow],
4440
+ activities=[echo_activity],
4406
4441
  poll_timeout=0.01,
4407
4442
  heartbeat_interval=0.01,
4408
4443
  )
@@ -4597,7 +4632,7 @@ class TestWorkerShutdown:
4597
4632
  },
4598
4633
  )
4599
4634
  mock_client.deregister_worker_registration.side_effect = cleanup_error
4600
- worker = Worker(mock_client, task_queue="q1")
4635
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow])
4601
4636
  worker._poll_workflow_tasks = AsyncMock( # type: ignore[method-assign]
4602
4637
  side_effect=RuntimeError("worker loop failed")
4603
4638
  )
@@ -4729,6 +4764,31 @@ class TestWorkerShutdown:
4729
4764
 
4730
4765
 
4731
4766
  class TestPollLoops:
4767
+ @pytest.mark.asyncio
4768
+ @pytest.mark.parametrize("has_workflows,has_activities", [(True, False), (False, True), (False, False)])
4769
+ async def test_run_polls_only_registered_task_families(
4770
+ self, mock_client: AsyncMock, has_workflows: bool, has_activities: bool,
4771
+ ) -> None:
4772
+ worker = Worker(
4773
+ mock_client,
4774
+ task_queue="q1",
4775
+ workflows=[TestWorkflow] if has_workflows else [],
4776
+ activities=[echo_activity] if has_activities else [],
4777
+ poll_timeout=0.01,
4778
+ )
4779
+ run_task = asyncio.create_task(worker.run())
4780
+ try:
4781
+ await asyncio.sleep(0.05)
4782
+ finally:
4783
+ await worker.stop()
4784
+ await run_task
4785
+
4786
+ assert bool(mock_client.poll_workflow_task.await_count) is has_workflows
4787
+ assert bool(mock_client.poll_activity_task.await_count) is has_activities
4788
+ assert bool(mock_client.poll_query_task.await_count) is has_workflows
4789
+ mock_client.register_worker.assert_awaited_once()
4790
+ mock_client.deregister_worker_registration.assert_awaited_once_with(worker.worker_id)
4791
+
4732
4792
  @pytest.mark.asyncio
4733
4793
  async def test_run_starts_both_loops(self, mock_client: AsyncMock) -> None:
4734
4794
  worker = Worker(
@@ -4990,6 +5050,33 @@ class TestWorkerHeartbeats:
4990
5050
 
4991
5051
 
4992
5052
  class TestRunUntil:
5053
+ @pytest.mark.asyncio
5054
+ @pytest.mark.parametrize("has_workflows,has_activities", [(True, False), (False, True), (False, False)])
5055
+ async def test_run_until_polls_only_registered_task_families(
5056
+ self, mock_client: AsyncMock, has_workflows: bool, has_activities: bool,
5057
+ ) -> None:
5058
+ mock_client.describe_workflow = AsyncMock(side_effect=[
5059
+ WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="running"),
5060
+ WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="running"),
5061
+ WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="completed"),
5062
+ ])
5063
+ worker = Worker(
5064
+ mock_client,
5065
+ task_queue="q1",
5066
+ workflows=[TestWorkflow] if has_workflows else [],
5067
+ activities=[echo_activity] if has_activities else [],
5068
+ poll_timeout=0.01,
5069
+ )
5070
+
5071
+ result = await worker.run_until(workflow_id="wf-1", timeout=1.0, poll_interval=0.01)
5072
+
5073
+ assert result.status == "completed"
5074
+ assert bool(mock_client.poll_workflow_task.await_count) is has_workflows
5075
+ assert bool(mock_client.poll_activity_task.await_count) is has_activities
5076
+ if not has_workflows:
5077
+ mock_client.poll_query_task.assert_not_awaited()
5078
+ mock_client.deregister_worker_registration.assert_awaited_once_with(worker.worker_id)
5079
+
4993
5080
  @pytest.mark.asyncio
4994
5081
  async def test_run_until_returns_terminal_description(self, mock_client: AsyncMock) -> None:
4995
5082
  mock_client.describe_workflow = AsyncMock(