durable-workflow 2.3.6__tar.gz → 2.3.8__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 (90) hide show
  1. {durable_workflow-2.3.6/src/durable_workflow.egg-info → durable_workflow-2.3.8}/PKG-INFO +1 -1
  2. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/pyproject.toml +3 -3
  3. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/worker.py +15 -41
  4. {durable_workflow-2.3.6 → durable_workflow-2.3.8/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  5. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_replay_regression_corpus.py +50 -2
  6. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_worker.py +38 -4
  7. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/LICENSE +0 -0
  8. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/README.md +0 -0
  9. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/setup.cfg +0 -0
  10. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/__init__.py +0 -0
  11. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/_avro.py +0 -0
  12. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/activity.py +0 -0
  13. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/auth_composition.py +0 -0
  14. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/client.py +0 -0
  15. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/errors.py +0 -0
  16. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/external_storage.py +0 -0
  17. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/external_task_input.py +0 -0
  18. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/external_task_result.py +0 -0
  19. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/history_bundle_verify.py +0 -0
  20. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/interceptors.py +0 -0
  21. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/invocable.py +0 -0
  22. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/metrics.py +0 -0
  23. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/nexus.py +0 -0
  24. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/py.typed +0 -0
  25. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/python_conformance.py +0 -0
  26. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/replay_conformance.py +0 -0
  27. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/replay_verify.py +0 -0
  28. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/retry_policy.py +0 -0
  29. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/serializer.py +0 -0
  30. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/sync.py +0 -0
  31. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/testing.py +0 -0
  32. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/worker_session.py +0 -0
  33. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/workflow.py +0 -0
  34. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  35. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  36. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  37. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  38. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow.egg-info/requires.txt +0 -0
  39. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/src/durable_workflow.egg-info/top_level.txt +0 -0
  40. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_activity_context.py +0 -0
  41. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_auth_composition.py +0 -0
  42. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_avro_value_benchmark.py +0 -0
  43. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_avro_value_protocol.py +0 -0
  44. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_ci_checkout.py +0 -0
  45. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_ci_integration_endpoint.py +0 -0
  46. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_client.py +0 -0
  47. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_codec_regression_corpus.py +0 -0
  48. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_control_plane_parity_fixtures.py +0 -0
  49. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_docstring_cross_references.py +0 -0
  50. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_durable_selection.py +0 -0
  51. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_errors.py +0 -0
  52. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_external_storage.py +0 -0
  53. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_external_task_input.py +0 -0
  54. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_external_task_result.py +0 -0
  55. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_golden_history_replay.py +0 -0
  56. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_history_bundle_verify.py +0 -0
  57. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_history_event_contract.py +0 -0
  58. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_invocable.py +0 -0
  59. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_message_streams.py +0 -0
  60. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_metrics.py +0 -0
  61. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_order_processing_example.py +0 -0
  62. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_parallel_completion_order_replay.py +0 -0
  63. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_parallel_saga_authoring.py +0 -0
  64. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_public_boundary_scanner.py +0 -0
  65. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_pypi_project_surface.py +0 -0
  66. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_python_conformance.py +0 -0
  67. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_queries.py +0 -0
  68. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_readme_quickstart.py +0 -0
  69. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_redrive_replay.py +0 -0
  70. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_release_metadata.py +0 -0
  71. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_replay.py +0 -0
  72. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_replay_conformance.py +0 -0
  73. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_replay_verify.py +0 -0
  74. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_retry_policy.py +0 -0
  75. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_runtime_external_payload_transport.py +0 -0
  76. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_schedules.py +0 -0
  77. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_serializer.py +0 -0
  78. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_signals.py +0 -0
  79. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_sleep.py +0 -0
  80. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_standalone_activity_client.py +0 -0
  81. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_storage_admission.py +0 -0
  82. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_sync.py +0 -0
  83. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_testing_harness.py +0 -0
  84. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_update_signal_condition_replay.py +0 -0
  85. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_updates.py +0 -0
  86. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_wait_condition.py +0 -0
  87. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_worker_session.py +0 -0
  88. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_workflow_result_timeout.py +0 -0
  89. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/tests/test_workflow_streams.py +0 -0
  90. {durable_workflow-2.3.6 → durable_workflow-2.3.8}/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.3.6
3
+ Version: 2.3.8
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.3.6"
7
+ version = "2.3.8"
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.3.6"
75
- registry-version = "2.3.6"
74
+ product-train = "2.3.8"
75
+ registry-version = "2.3.8"
76
76
  supported-server-versions = "2.4.8"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -98,6 +98,7 @@ from .workflow import (
98
98
  log = logging.getLogger("durable_workflow.worker")
99
99
 
100
100
  QUERY_TASKS_CAPABILITY = "query_tasks"
101
+ WORKFLOW_HISTORY_PAGE_SIZE = 500
101
102
  UPDATE_VALIDATION_TASKS_CAPABILITY = "update_validation_tasks"
102
103
  WORKFLOW_UPDATES_CAPABILITY = "workflow_updates"
103
104
  MESSAGE_STREAMS_CAPABILITY = "message_streams"
@@ -950,30 +951,6 @@ def _server_supports_update_validation_tasks(info: dict[str, Any]) -> bool:
950
951
  )
951
952
 
952
953
 
953
- def _server_long_poll_timeout(info: dict[str, Any]) -> float | None:
954
- worker_protocol = info.get("worker_protocol")
955
- if not isinstance(worker_protocol, dict):
956
- return None
957
-
958
- capabilities = worker_protocol.get("server_capabilities")
959
- if not isinstance(capabilities, dict):
960
- return None
961
-
962
- timeout = capabilities.get("long_poll_timeout")
963
- if isinstance(timeout, bool):
964
- return None
965
- if isinstance(timeout, int | float):
966
- return float(timeout) if timeout > 0 else None
967
- if isinstance(timeout, str):
968
- try:
969
- parsed = float(timeout)
970
- except ValueError:
971
- return None
972
- return parsed if parsed > 0 else None
973
-
974
- return None
975
-
976
-
977
954
  def _contract_version_matches(value: Any, expected: int) -> bool:
978
955
  if isinstance(value, int):
979
956
  return value == expected
@@ -1040,6 +1017,7 @@ class Worker:
1040
1017
  raise ValueError("heartbeat_interval must be positive")
1041
1018
 
1042
1019
  self._poll_timeout = poll_timeout
1020
+ # Client supplies HTTP grace separately from this requested poll window.
1043
1021
  self._poll_http_timeout = poll_timeout
1044
1022
  self.max_concurrent_workflow_tasks = max_concurrent_workflow_tasks
1045
1023
  self.max_concurrent_activity_tasks = max_concurrent_activity_tasks
@@ -1190,9 +1168,6 @@ class Worker:
1190
1168
  "multiplexed workflow/update-validation polling. Refusing registration so validated "
1191
1169
  "updates cannot be accepted without validator approval or exceed worker capacity."
1192
1170
  )
1193
- server_long_poll_timeout = _server_long_poll_timeout(info)
1194
- if server_long_poll_timeout is not None:
1195
- self._poll_http_timeout = max(self._poll_http_timeout, server_long_poll_timeout + 5.0)
1196
1171
  log.debug(
1197
1172
  "server compatibility accepted: app_version=%s control_plane=%s worker_protocol=%s",
1198
1173
  info.get("version", "unknown"),
@@ -1637,22 +1612,19 @@ class Worker:
1637
1612
  wf_type: str = task.get("workflow_type", "")
1638
1613
  history = task.get("history_events", [])
1639
1614
 
1640
- # Paginate history if needed
1615
+ # The worker requests bounded history pages when polling. Do not replay
1616
+ # an incomplete history if fetching a later page fails.
1641
1617
  next_page_token = task.get("next_history_page_token")
1642
1618
  while next_page_token:
1643
- try:
1644
- page_data = await self.client.workflow_task_history(
1645
- task_id=task_id,
1646
- next_history_page_token=next_page_token,
1647
- lease_owner=self.worker_id,
1648
- workflow_task_attempt=attempt,
1649
- )
1650
- if page_data and page_data.get("history_events"):
1651
- history.extend(page_data["history_events"])
1652
- next_page_token = page_data.get("next_history_page_token") if page_data else None
1653
- except Exception as e:
1654
- log.warning("failed to fetch history page for task %s: %s", task_id, e)
1655
- break
1619
+ page_data = await self.client.workflow_task_history(
1620
+ task_id=task_id,
1621
+ next_history_page_token=next_page_token,
1622
+ lease_owner=self.worker_id,
1623
+ workflow_task_attempt=attempt,
1624
+ )
1625
+ if page_data and page_data.get("history_events"):
1626
+ history.extend(page_data["history_events"])
1627
+ next_page_token = page_data.get("next_history_page_token") if page_data else None
1656
1628
 
1657
1629
  start_input: list[Any] = []
1658
1630
  codec = task.get("payload_codec")
@@ -2535,6 +2507,7 @@ class Worker:
2535
2507
  timeout=self._poll_http_timeout,
2536
2508
  build_id=self.build_id,
2537
2509
  task_kinds=self._workflow_poll_task_kinds(),
2510
+ history_page_size=WORKFLOW_HISTORY_PAGE_SIZE,
2538
2511
  )
2539
2512
  except asyncio.CancelledError:
2540
2513
  self._release_workflow_capacity()
@@ -3224,6 +3197,7 @@ class Worker:
3224
3197
  timeout=self._poll_http_timeout,
3225
3198
  build_id=self.build_id,
3226
3199
  task_kinds=self._workflow_poll_task_kinds(),
3200
+ history_page_size=WORKFLOW_HISTORY_PAGE_SIZE,
3227
3201
  )
3228
3202
  except BaseException:
3229
3203
  self._release_workflow_capacity()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.3.6
3
+ Version: 2.3.8
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,11 +4,12 @@ import json
4
4
  from collections.abc import Mapping, Sequence
5
5
  from pathlib import Path
6
6
  from typing import Any
7
+ from unittest.mock import AsyncMock
7
8
 
8
9
  import pytest
9
10
 
10
- from durable_workflow import Replayer, serializer, workflow
11
- from durable_workflow.client import WorkflowStreamAppendItem
11
+ from durable_workflow import Replayer, Worker, serializer, workflow
12
+ from durable_workflow.client import Client, WorkflowStreamAppendItem
12
13
  from durable_workflow.errors import NonDeterministicReplayError, WorkflowPayloadDecodeError
13
14
  from durable_workflow.workflow import WorkflowContext, commands_to_server_commands, query_state
14
15
  from tests.test_golden_history_replay import (
@@ -156,6 +157,17 @@ class RecordedSideEffectWorkflow:
156
157
  return (yield ctx.side_effect(unexpected_execution))
157
158
 
158
159
 
160
+ @workflow.defn(name="tests.replay.paged-recorded-side-effects")
161
+ class PagedRecordedSideEffectsWorkflow:
162
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
163
+ def unexpected_execution() -> int:
164
+ raise AssertionError("recorded side effect callable ran during replay")
165
+
166
+ first = yield ctx.side_effect(unexpected_execution)
167
+ second = yield ctx.side_effect(unexpected_execution)
168
+ return first + second
169
+
170
+
159
171
  @workflow.defn(name="tests.replay.message-stream-consumer")
160
172
  class MessageStreamConsumerWorkflow:
161
173
  def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
@@ -212,6 +224,7 @@ WORKFLOWS = [
212
224
  NestedParallelPathWorkflow,
213
225
  ParallelMetadataProducerWorkflow,
214
226
  ParallelResultBindingWorkflow,
227
+ PagedRecordedSideEffectsWorkflow,
215
228
  PostConditionReceiversWorkflow,
216
229
  RecordedSideEffectWorkflow,
217
230
  SelectionAwaitMarkerWorkflow,
@@ -394,6 +407,41 @@ def test_checked_in_replay_regression_corpus_uses_official_replayer(
394
407
  _execute_fixture(fixture)
395
408
 
396
409
 
410
+ @pytest.mark.asyncio
411
+ async def test_worker_replays_recorded_side_effects_across_history_pages() -> None:
412
+ fixture = json.loads((FIXTURE_DIR / "paged-recorded-side-effects.json").read_text(encoding="utf-8"))
413
+ history = fixture["history"]
414
+ client = AsyncMock(spec=Client)
415
+ client.workflow_task_history.return_value = {
416
+ "history_events": history[1:],
417
+ "next_history_page_token": None,
418
+ }
419
+ worker = Worker(client, task_queue="paged-history", workflows=[PagedRecordedSideEffectsWorkflow])
420
+
421
+ commands = await worker._run_workflow_task_core({
422
+ "task_id": "task-paged-side-effects",
423
+ "workflow_id": "workflow-paged-side-effects",
424
+ "run_id": "run-paged-side-effects",
425
+ "workflow_type": fixture["workflow"]["type"],
426
+ "workflow_task_attempt": 1,
427
+ "history_events": history[:1],
428
+ "next_history_page_token": "second-page",
429
+ "arguments": serializer.encode([], codec="avro"),
430
+ "payload_codec": "avro",
431
+ })
432
+
433
+ client.workflow_task_history.assert_awaited_once_with(
434
+ task_id="task-paged-side-effects",
435
+ next_history_page_token="second-page",
436
+ lease_owner=worker.worker_id,
437
+ workflow_task_attempt=1,
438
+ )
439
+ client.complete_workflow_task.assert_awaited_once()
440
+ assert commands is not None
441
+ assert [command["type"] for command in commands] == ["complete_workflow"]
442
+ assert serializer.decode_envelope(commands[0]["result"]) == 198
443
+
444
+
397
445
  @pytest.mark.parametrize(
398
446
  "fixture",
399
447
  [
@@ -711,7 +711,14 @@ class TestWorkerRegistration:
711
711
  )
712
712
 
713
713
  @pytest.mark.asyncio
714
- async def test_register_keeps_http_timeout_above_server_long_poll(self, mock_client: AsyncMock) -> None:
714
+ @pytest.mark.parametrize(("poll_loop", "poll_method"), [
715
+ ("_poll_workflow_tasks", "poll_workflow_task"),
716
+ ("_poll_activity_tasks", "poll_activity_task"),
717
+ ("_poll_query_tasks", "poll_query_task"),
718
+ ])
719
+ async def test_registration_preserves_configured_poll_window(
720
+ self, mock_client: AsyncMock, poll_loop: str, poll_method: str,
721
+ ) -> None:
715
722
  mock_client.get_cluster_info = AsyncMock(
716
723
  return_value=compatible_cluster_info(
717
724
  worker_protocol={
@@ -736,12 +743,12 @@ class TestWorkerRegistration:
736
743
  worker._stop.set()
737
744
  return None
738
745
 
739
- mock_client.poll_workflow_task.side_effect = poll_once
746
+ getattr(mock_client, poll_method).side_effect = poll_once
740
747
 
741
748
  await worker._register()
742
- await worker._poll_workflow_tasks()
749
+ await getattr(worker, poll_loop)()
743
750
 
744
- assert mock_client.poll_workflow_task.call_args.kwargs["timeout"] == 17.0
751
+ assert getattr(mock_client, poll_method).call_args.kwargs["timeout"] == 0.01
745
752
 
746
753
  @pytest.mark.asyncio
747
754
  async def test_register_keeps_baseline_capabilities_when_server_does_not_support_query_tasks(
@@ -836,6 +843,7 @@ class TestWorkerRegistration:
836
843
  workflow_kwargs = mock_client.poll_workflow_task.call_args.kwargs
837
844
  assert workflow_called.is_set()
838
845
  assert workflow_kwargs["build_id"] == "release-2026.04.22-a1"
846
+ assert workflow_kwargs["history_page_size"] == 500
839
847
 
840
848
  worker._stop.clear()
841
849
  mock_client.poll_activity_task.side_effect = activity_poll_once
@@ -1189,6 +1197,31 @@ class TestWorkerRegistration:
1189
1197
 
1190
1198
 
1191
1199
  class TestWorkflowTaskExecution:
1200
+ @pytest.mark.asyncio
1201
+ async def test_history_page_failure_does_not_replay_partial_history(
1202
+ self, mock_client: AsyncMock
1203
+ ) -> None:
1204
+ mock_client.workflow_task_history.side_effect = RuntimeError("history page unavailable")
1205
+ worker = Worker(mock_client, task_queue="q1", workflows=[TestWorkflow])
1206
+ task = {
1207
+ "task_id": "t-paged-history",
1208
+ "workflow_type": "test-wf",
1209
+ "workflow_task_attempt": 1,
1210
+ "history_events": [{"event_id": "first-page"}],
1211
+ "next_history_page_token": "next-page",
1212
+ }
1213
+
1214
+ with pytest.raises(RuntimeError, match="history page unavailable"):
1215
+ await worker._run_workflow_task_core(task)
1216
+
1217
+ mock_client.workflow_task_history.assert_awaited_once_with(
1218
+ task_id="t-paged-history",
1219
+ next_history_page_token="next-page",
1220
+ lease_owner=worker.worker_id,
1221
+ workflow_task_attempt=1,
1222
+ )
1223
+ mock_client.complete_workflow_task.assert_not_awaited()
1224
+
1192
1225
  @pytest.mark.asyncio
1193
1226
  async def test_local_activity_runs_in_workflow_worker_and_records_heartbeats(
1194
1227
  self, mock_client: AsyncMock
@@ -4948,6 +4981,7 @@ class TestRunUntil:
4948
4981
  assert worker._stop.is_set()
4949
4982
  mock_client.register_worker.assert_awaited_once()
4950
4983
  assert mock_client.describe_workflow.await_count == 2
4984
+ assert mock_client.poll_workflow_task.call_args.kwargs["history_page_size"] == 500
4951
4985
 
4952
4986
  @pytest.mark.asyncio
4953
4987
  async def test_run_until_times_out_and_stops_worker(self, mock_client: AsyncMock) -> None: