durable-workflow 2.5.5__tar.gz → 2.5.6__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.6}/PKG-INFO +1 -1
  2. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/pyproject.toml +3 -3
  3. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/worker.py +11 -7
  4. {durable_workflow-2.5.5 → durable_workflow-2.5.6/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  5. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_replay_regression_corpus.py +42 -1
  6. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_worker.py +55 -1
  7. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/LICENSE +0 -0
  8. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/README.md +0 -0
  9. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/setup.cfg +0 -0
  10. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/__init__.py +0 -0
  11. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/_activity_process.py +0 -0
  12. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/_avro.py +0 -0
  13. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/_cancellation_scope.py +0 -0
  14. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  15. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  16. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/_prepared_local_activity.py +0 -0
  17. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  18. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/activity.py +0 -0
  19. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/auth_composition.py +0 -0
  20. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/cancellation.py +0 -0
  21. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/client.py +0 -0
  22. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/errors.py +0 -0
  23. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/external_storage.py +0 -0
  24. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/external_task_input.py +0 -0
  25. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/external_task_result.py +0 -0
  26. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/history_bundle_verify.py +0 -0
  27. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/interceptors.py +0 -0
  28. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/invocable.py +0 -0
  29. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/metrics.py +0 -0
  30. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/nexus.py +0 -0
  31. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/py.typed +0 -0
  32. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/python_conformance.py +0 -0
  33. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/replay_conformance.py +0 -0
  34. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/replay_verify.py +0 -0
  35. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/retry_policy.py +0 -0
  36. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/serializer.py +0 -0
  37. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/sync.py +0 -0
  38. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/testing.py +0 -0
  39. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/worker_session.py +0 -0
  40. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/workflow.py +0 -0
  41. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  42. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  43. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  44. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  45. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow.egg-info/requires.txt +0 -0
  46. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/src/durable_workflow.egg-info/top_level.txt +0 -0
  47. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_activity_cancellation_policies.py +0 -0
  48. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_activity_context.py +0 -0
  49. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_activity_process.py +0 -0
  50. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_auth_composition.py +0 -0
  51. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_avro_value_benchmark.py +0 -0
  52. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_avro_value_protocol.py +0 -0
  53. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cancellation_context.py +0 -0
  54. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cancellation_remaining_time.py +0 -0
  55. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cancellation_scope_admission.py +0 -0
  56. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cancellation_scope_authoring.py +0 -0
  57. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cancellation_scope_boundary.py +0 -0
  58. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cancellation_scope_opening.py +0 -0
  59. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_child_workflow_policies.py +0 -0
  60. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_ci_checkout.py +0 -0
  61. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_ci_integration_endpoint.py +0 -0
  62. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_client.py +0 -0
  63. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_client_timeouts.py +0 -0
  64. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_codec_regression_corpus.py +0 -0
  65. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  66. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_committed_cancellation_scope_groups.py +0 -0
  67. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_committed_cancellation_scope_history.py +0 -0
  68. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_committed_cancellation_scope_replay.py +0 -0
  69. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_condition_replay_drift.py +0 -0
  70. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_control_plane_parity_fixtures.py +0 -0
  71. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cooperative_cancellation.py +0 -0
  72. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cooperative_cancellation_client.py +0 -0
  73. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cooperative_cancellation_worker.py +0 -0
  74. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_cooperative_remote_worker.py +0 -0
  75. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_docstring_cross_references.py +0 -0
  76. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_durable_selection.py +0 -0
  77. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_errors.py +0 -0
  78. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_external_storage.py +0 -0
  79. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_external_task_input.py +0 -0
  80. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_external_task_result.py +0 -0
  81. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_golden_history_replay.py +0 -0
  82. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_history_bundle_verify.py +0 -0
  83. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_history_event_contract.py +0 -0
  84. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_invocable.py +0 -0
  85. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_message_streams.py +0 -0
  86. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_metrics.py +0 -0
  87. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_nexus_namespace.py +0 -0
  88. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_nexus_wire.py +0 -0
  89. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_order_processing_example.py +0 -0
  90. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_parallel_completion_order_replay.py +0 -0
  91. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_parallel_saga_authoring.py +0 -0
  92. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_patch_history.py +0 -0
  93. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_pending_operation_replay.py +0 -0
  94. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_prepared_local_activity_client.py +0 -0
  95. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_prepared_local_activity_groups.py +0 -0
  96. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_prepared_local_activity_policies.py +0 -0
  97. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_prepared_local_activity_worker.py +0 -0
  98. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_public_boundary_scanner.py +0 -0
  99. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_pypi_project_surface.py +0 -0
  100. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_python_conformance.py +0 -0
  101. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_queries.py +0 -0
  102. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_readme_quickstart.py +0 -0
  103. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_redrive_replay.py +0 -0
  104. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_registration_ambiguity.py +0 -0
  105. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_release_metadata.py +0 -0
  106. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_replay.py +0 -0
  107. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_replay_conformance.py +0 -0
  108. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_replay_verify.py +0 -0
  109. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_retry_policy.py +0 -0
  110. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_run_cancellation_scope_replay.py +0 -0
  111. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_runtime_external_payload_transport.py +0 -0
  112. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_schedules.py +0 -0
  113. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_scoped_run_cancellation_context.py +0 -0
  114. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_serializer.py +0 -0
  115. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_signals.py +0 -0
  116. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_sleep.py +0 -0
  117. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_standalone_activity_client.py +0 -0
  118. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_sticky_execution.py +0 -0
  119. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_storage_admission.py +0 -0
  120. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_sync.py +0 -0
  121. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_testing_harness.py +0 -0
  122. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_update_signal_condition_replay.py +0 -0
  123. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_updates.py +0 -0
  124. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_wait_condition.py +0 -0
  125. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_worker_session.py +0 -0
  126. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_workflow_entrypoint_validation.py +0 -0
  127. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_workflow_result_timeout.py +0 -0
  128. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/tests/test_workflow_streams.py +0 -0
  129. {durable_workflow-2.5.5 → durable_workflow-2.5.6}/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.6
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
@@ -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.6"
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,8 +71,8 @@ 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.6"
75
+ registry-version = "2.5.6"
76
76
  supported-server-versions = "2.5.16"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -4077,11 +4077,12 @@ class Worker:
4077
4077
  try:
4078
4078
  if self._stop.is_set():
4079
4079
  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:
4080
+ loops = [asyncio.create_task(self._heartbeat_loop())]
4081
+ if self.workflows:
4082
+ loops.append(asyncio.create_task(self._poll_workflow_tasks()))
4083
+ if self.activities:
4084
+ loops.append(asyncio.create_task(self._poll_activity_tasks()))
4085
+ if self.workflows and self._query_tasks_supported:
4085
4086
  if self._can_clone_client_for_query_tasks():
4086
4087
  self._start_query_task_thread()
4087
4088
  else:
@@ -4243,7 +4244,7 @@ class Worker:
4243
4244
  if self._stop.is_set():
4244
4245
  raise asyncio.CancelledError
4245
4246
  background_tasks.append(asyncio.create_task(self._heartbeat_loop()))
4246
- if self._query_tasks_supported:
4247
+ if self.workflows and self._query_tasks_supported:
4247
4248
  if self._can_clone_client_for_query_tasks():
4248
4249
  self._start_query_task_thread()
4249
4250
  else:
@@ -4295,7 +4296,7 @@ class Worker:
4295
4296
  f"workflow {workflow_id} not terminal after {timeout}s (status={desc.status})"
4296
4297
  )
4297
4298
 
4298
- if next_task_kind == "workflow":
4299
+ if self.workflows and (next_task_kind == "workflow" or not self.activities):
4299
4300
  await self._reserve_workflow_capacity()
4300
4301
  try:
4301
4302
  poll_start = time.perf_counter()
@@ -4347,6 +4348,9 @@ class Worker:
4347
4348
  next_task_kind = "workflow"
4348
4349
  continue
4349
4350
 
4351
+ if not self.activities:
4352
+ await asyncio.sleep(poll_interval)
4353
+ continue
4350
4354
  self._validate_cooperative_activity_handlers()
4351
4355
  if len(self._remote_activity_processes) >= self.max_concurrent_activity_tasks:
4352
4356
  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.6
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
@@ -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"))
@@ -4403,6 +4403,8 @@ class TestWorkerShutdown:
4403
4403
  client,
4404
4404
  task_queue="orders",
4405
4405
  worker_id="worker-only",
4406
+ workflows=[TestWorkflow],
4407
+ activities=[echo_activity],
4406
4408
  poll_timeout=0.01,
4407
4409
  heartbeat_interval=0.01,
4408
4410
  )
@@ -4597,7 +4599,7 @@ class TestWorkerShutdown:
4597
4599
  },
4598
4600
  )
4599
4601
  mock_client.deregister_worker_registration.side_effect = cleanup_error
4600
- worker = Worker(mock_client, task_queue="q1")
4602
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow])
4601
4603
  worker._poll_workflow_tasks = AsyncMock( # type: ignore[method-assign]
4602
4604
  side_effect=RuntimeError("worker loop failed")
4603
4605
  )
@@ -4729,6 +4731,31 @@ class TestWorkerShutdown:
4729
4731
 
4730
4732
 
4731
4733
  class TestPollLoops:
4734
+ @pytest.mark.asyncio
4735
+ @pytest.mark.parametrize("has_workflows,has_activities", [(True, False), (False, True), (False, False)])
4736
+ async def test_run_polls_only_registered_task_families(
4737
+ self, mock_client: AsyncMock, has_workflows: bool, has_activities: bool,
4738
+ ) -> None:
4739
+ worker = Worker(
4740
+ mock_client,
4741
+ task_queue="q1",
4742
+ workflows=[TestWorkflow] if has_workflows else [],
4743
+ activities=[echo_activity] if has_activities else [],
4744
+ poll_timeout=0.01,
4745
+ )
4746
+ run_task = asyncio.create_task(worker.run())
4747
+ try:
4748
+ await asyncio.sleep(0.05)
4749
+ finally:
4750
+ await worker.stop()
4751
+ await run_task
4752
+
4753
+ assert bool(mock_client.poll_workflow_task.await_count) is has_workflows
4754
+ assert bool(mock_client.poll_activity_task.await_count) is has_activities
4755
+ assert bool(mock_client.poll_query_task.await_count) is has_workflows
4756
+ mock_client.register_worker.assert_awaited_once()
4757
+ mock_client.deregister_worker_registration.assert_awaited_once_with(worker.worker_id)
4758
+
4732
4759
  @pytest.mark.asyncio
4733
4760
  async def test_run_starts_both_loops(self, mock_client: AsyncMock) -> None:
4734
4761
  worker = Worker(
@@ -4990,6 +5017,33 @@ class TestWorkerHeartbeats:
4990
5017
 
4991
5018
 
4992
5019
  class TestRunUntil:
5020
+ @pytest.mark.asyncio
5021
+ @pytest.mark.parametrize("has_workflows,has_activities", [(True, False), (False, True), (False, False)])
5022
+ async def test_run_until_polls_only_registered_task_families(
5023
+ self, mock_client: AsyncMock, has_workflows: bool, has_activities: bool,
5024
+ ) -> None:
5025
+ mock_client.describe_workflow = AsyncMock(side_effect=[
5026
+ WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="running"),
5027
+ WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="running"),
5028
+ WorkflowExecution(workflow_id="wf-1", run_id="run-1", workflow_type="test-wf", status="completed"),
5029
+ ])
5030
+ worker = Worker(
5031
+ mock_client,
5032
+ task_queue="q1",
5033
+ workflows=[TestWorkflow] if has_workflows else [],
5034
+ activities=[echo_activity] if has_activities else [],
5035
+ poll_timeout=0.01,
5036
+ )
5037
+
5038
+ result = await worker.run_until(workflow_id="wf-1", timeout=1.0, poll_interval=0.01)
5039
+
5040
+ assert result.status == "completed"
5041
+ assert bool(mock_client.poll_workflow_task.await_count) is has_workflows
5042
+ assert bool(mock_client.poll_activity_task.await_count) is has_activities
5043
+ if not has_workflows:
5044
+ mock_client.poll_query_task.assert_not_awaited()
5045
+ mock_client.deregister_worker_registration.assert_awaited_once_with(worker.worker_id)
5046
+
4993
5047
  @pytest.mark.asyncio
4994
5048
  async def test_run_until_returns_terminal_description(self, mock_client: AsyncMock) -> None:
4995
5049
  mock_client.describe_workflow = AsyncMock(