durable-workflow 2.1.0__tar.gz → 2.1.2__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 (88) hide show
  1. {durable_workflow-2.1.0/src/durable_workflow.egg-info → durable_workflow-2.1.2}/PKG-INFO +1 -1
  2. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/pyproject.toml +3 -3
  3. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/worker.py +12 -3
  4. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/workflow.py +12 -2
  5. {durable_workflow-2.1.0 → durable_workflow-2.1.2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  6. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_pypi_project_surface.py +5 -0
  7. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_release_metadata.py +2 -2
  8. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_replay.py +51 -0
  9. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_replay_regression_corpus.py +10 -0
  10. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_worker.py +59 -0
  11. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/LICENSE +0 -0
  12. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/README.md +0 -0
  13. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/setup.cfg +0 -0
  14. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/__init__.py +0 -0
  15. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/_avro.py +0 -0
  16. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/activity.py +0 -0
  17. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/auth_composition.py +0 -0
  18. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/client.py +0 -0
  19. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/errors.py +0 -0
  20. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/external_storage.py +0 -0
  21. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/external_task_input.py +0 -0
  22. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/external_task_result.py +0 -0
  23. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/history_bundle_verify.py +0 -0
  24. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/interceptors.py +0 -0
  25. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/invocable.py +0 -0
  26. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/metrics.py +0 -0
  27. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/nexus.py +0 -0
  28. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/py.typed +0 -0
  29. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/python_conformance.py +0 -0
  30. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/replay_conformance.py +0 -0
  31. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/replay_verify.py +0 -0
  32. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/retry_policy.py +0 -0
  33. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/serializer.py +0 -0
  34. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/sync.py +0 -0
  35. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/testing.py +0 -0
  36. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  37. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  38. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  39. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  40. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow.egg-info/requires.txt +0 -0
  41. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
  42. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_activity_context.py +0 -0
  43. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_auth_composition.py +0 -0
  44. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_avro_value_benchmark.py +0 -0
  45. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_avro_value_protocol.py +0 -0
  46. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_ci_checkout.py +0 -0
  47. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_ci_integration_endpoint.py +0 -0
  48. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_client.py +0 -0
  49. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_codec_regression_corpus.py +0 -0
  50. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_control_plane_parity_fixtures.py +0 -0
  51. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_docstring_cross_references.py +0 -0
  52. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_durable_selection.py +0 -0
  53. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_errors.py +0 -0
  54. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_external_storage.py +0 -0
  55. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_external_task_input.py +0 -0
  56. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_external_task_result.py +0 -0
  57. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_golden_history_replay.py +0 -0
  58. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_history_bundle_verify.py +0 -0
  59. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_history_event_contract.py +0 -0
  60. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_invocable.py +0 -0
  61. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_message_streams.py +0 -0
  62. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_metrics.py +0 -0
  63. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_order_processing_example.py +0 -0
  64. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_parallel_completion_order_replay.py +0 -0
  65. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_parallel_saga_authoring.py +0 -0
  66. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_public_boundary_scanner.py +0 -0
  67. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_python_conformance.py +0 -0
  68. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_queries.py +0 -0
  69. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_readme_quickstart.py +0 -0
  70. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_redrive_replay.py +0 -0
  71. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_replay_conformance.py +0 -0
  72. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_replay_verify.py +0 -0
  73. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_retry_policy.py +0 -0
  74. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_runtime_external_payload_transport.py +0 -0
  75. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_schedules.py +0 -0
  76. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_serializer.py +0 -0
  77. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_signals.py +0 -0
  78. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_sleep.py +0 -0
  79. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_standalone_activity_client.py +0 -0
  80. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_storage_admission.py +0 -0
  81. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_sync.py +0 -0
  82. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_testing_harness.py +0 -0
  83. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_update_signal_condition_replay.py +0 -0
  84. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_updates.py +0 -0
  85. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_wait_condition.py +0 -0
  86. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_workflow_result_timeout.py +0 -0
  87. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/tests/test_workflow_streams.py +0 -0
  88. {durable_workflow-2.1.0 → durable_workflow-2.1.2}/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.1.0
3
+ Version: 2.1.2
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.1.0"
7
+ version = "2.1.2"
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.1.0"
75
- registry-version = "2.1.0"
74
+ product-train = "2.1.2"
75
+ registry-version = "2.1.2"
76
76
  supported-server-versions = "2.4.0"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -162,6 +162,15 @@ def _is_storage_admission_error(error: BaseException) -> bool:
162
162
  )
163
163
 
164
164
 
165
+ def _poll_error_summary(error: BaseException) -> str:
166
+ if not isinstance(error, ServerError):
167
+ return str(error)
168
+ reason = error.reason()
169
+ if not isinstance(reason, str) or not reason:
170
+ return f"server returned {error.status}"
171
+ return f"server returned {error.status} reason={reason[:64]!r}"
172
+
173
+
165
174
  def _command_payload_codec(codec: object) -> str:
166
175
  if codec is None:
167
176
  return serializer.AVRO_CODEC
@@ -2184,7 +2193,7 @@ class Worker:
2184
2193
  if _is_storage_admission_error(e):
2185
2194
  raise
2186
2195
  self._record_poll_metrics("workflow", "error", time.perf_counter() - poll_start)
2187
- log.warning("workflow poll error: %s", e)
2196
+ log.warning("workflow poll error: %s", _poll_error_summary(e))
2188
2197
  await asyncio.sleep(1.0)
2189
2198
  continue
2190
2199
  if task is None:
@@ -2282,7 +2291,7 @@ class Worker:
2282
2291
  if _is_storage_admission_error(e):
2283
2292
  raise
2284
2293
  self._record_poll_metrics("activity", "error", time.perf_counter() - poll_start)
2285
- log.warning("activity poll error: %s", e)
2294
+ log.warning("activity poll error: %s", _poll_error_summary(e))
2286
2295
  await asyncio.sleep(1.0)
2287
2296
  continue
2288
2297
  if task is None:
@@ -2333,7 +2342,7 @@ class Worker:
2333
2342
  if _is_storage_admission_error(e):
2334
2343
  raise
2335
2344
  self._record_poll_metrics("query", "error", time.perf_counter() - poll_start)
2336
- log.warning("query poll error: %s", e)
2345
+ log.warning("query poll error: %s", _poll_error_summary(e))
2337
2346
  await asyncio.sleep(1.0)
2338
2347
  continue
2339
2348
  if task is None:
@@ -762,6 +762,7 @@ class RecordSideEffect:
762
762
 
763
763
  result: Any
764
764
  workflow_stream: dict[str, Any] | None = None
765
+ _effect: Callable[[], Any] | None = field(default=None, repr=False, compare=False)
765
766
 
766
767
  def to_server_command(
767
768
  self,
@@ -1799,8 +1800,7 @@ class WorkflowContext:
1799
1800
  return SelectGroup(entries)
1800
1801
 
1801
1802
  def side_effect(self, fn: Callable[[], Any]) -> RecordSideEffect:
1802
- result = fn()
1803
- return RecordSideEffect(result=result)
1803
+ return RecordSideEffect(result=None, _effect=fn)
1804
1804
 
1805
1805
  def append_workflow_stream(
1806
1806
  self,
@@ -5139,6 +5139,16 @@ def _replay_state(
5139
5139
  continue
5140
5140
  ctx.logger._set_replaying(False)
5141
5141
  _assert_pending_step_matches(cmd)
5142
+ if cmd._effect is not None:
5143
+ try:
5144
+ cmd.result = cmd._effect()
5145
+ except Exception as exc:
5146
+ try:
5147
+ advanced_cmd = gen.throw(exc)
5148
+ continue
5149
+ except StopIteration as stop:
5150
+ return _terminal_state(stop.value, include_pending=True)
5151
+ cmd._effect = None
5142
5152
  pending.append(cmd)
5143
5153
  next_value = cmd.result
5144
5154
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.1.0
3
+ Version: 2.1.2
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
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import subprocess
5
+ from dataclasses import replace
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
@@ -91,6 +92,10 @@ def pip_report(version: str) -> dict[str, object]:
91
92
  def test_release_channel_distinguishes_prerelease_and_stable() -> None:
92
93
  assert release_channel(source_metadata()) == "prerelease"
93
94
  assert release_channel(source_metadata(stable=True)) == "stable"
95
+ stable_minor = replace(source_metadata(stable=True), version="2.1.0", registry_version="2.1.0")
96
+ prerelease_minor = replace(source_metadata(), version="2.1.0-rc.1", registry_version="2.1.0rc1")
97
+ assert release_channel(stable_minor) == "stable"
98
+ assert release_channel(prerelease_minor) == "prerelease"
94
99
 
95
100
 
96
101
  def test_prerelease_with_existing_final_skips_project_root_and_bare_install(
@@ -41,9 +41,9 @@ def test_worker_release_identity_matches_supported_server_and_protocol() -> None
41
41
  project = manifest["project"]
42
42
  release = manifest["tool"]["durable-workflow"]
43
43
 
44
- assert project["version"] == "2.1.0"
44
+ assert check_release_metadata.VERSION_PATTERN.fullmatch(project["version"]) is not None
45
45
  assert release["product-train"] == project["version"]
46
- assert release["registry-version"] == "2.1.0"
46
+ assert release["registry-version"] == project["version"].replace("-rc.", "rc")
47
47
  assert release["supported-server-versions"] == "2.4.0"
48
48
  assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.19"
49
49
  assert release["durable-selection"] is True
@@ -1158,6 +1158,57 @@ class TestContinueAsNew:
1158
1158
 
1159
1159
 
1160
1160
  class TestSideEffect:
1161
+ def test_user_callable_runs_only_for_an_unrecorded_side_effect(self) -> None:
1162
+ calls: list[str] = []
1163
+
1164
+ @workflow.defn(name="side-effect-call-count-wf")
1165
+ class CountedSideEffectWorkflow:
1166
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
1167
+ value = yield ctx.side_effect(lambda: calls.append("called") or 42)
1168
+ return value
1169
+
1170
+ first = replay(CountedSideEffectWorkflow, [], [])
1171
+ assert calls == ["called"]
1172
+ assert isinstance(first.commands[0], RecordSideEffect)
1173
+ assert first.commands[0].result == 42
1174
+
1175
+ history = [{"event_type": "SideEffectRecorded", "payload": {"result": _avro(99)}}]
1176
+ replayed = replay(CountedSideEffectWorkflow, history, [])
1177
+ assert calls == ["called"]
1178
+ assert isinstance(replayed.commands[0], CompleteWorkflow)
1179
+ assert replayed.commands[0].result == 99
1180
+
1181
+ def test_user_callable_is_not_run_when_history_shape_mismatches(self) -> None:
1182
+ calls: list[str] = []
1183
+
1184
+ @workflow.defn(name="side-effect-mismatch-wf")
1185
+ class MismatchedSideEffectWorkflow:
1186
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
1187
+ return (yield ctx.side_effect(lambda: calls.append("called") or 42))
1188
+
1189
+ history = [{"event_type": "TimerScheduled", "payload": {"sequence": 1, "timer_kind": "durable_timer"}}]
1190
+ with pytest.raises(NonDeterministicReplayError):
1191
+ replay(MismatchedSideEffectWorkflow, history, [])
1192
+ assert calls == []
1193
+
1194
+ def test_user_callable_error_can_be_handled_by_workflow(self) -> None:
1195
+ @workflow.defn(name="side-effect-error-wf")
1196
+ class HandledSideEffectWorkflow:
1197
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
1198
+ def fail() -> int:
1199
+ raise ValueError("expected failure")
1200
+
1201
+ try:
1202
+ yield ctx.side_effect(fail)
1203
+ except ValueError:
1204
+ return "handled"
1205
+ return "unexpected"
1206
+
1207
+ outcome = replay(HandledSideEffectWorkflow, [], [])
1208
+ assert len(outcome.commands) == 1
1209
+ assert isinstance(outcome.commands[0], CompleteWorkflow)
1210
+ assert outcome.commands[0].result == "handled"
1211
+
1161
1212
  def test_first_replay_records_and_continues(self) -> None:
1162
1213
  outcome = replay(SideEffectWorkflow, [], [])
1163
1214
  assert len(outcome.commands) == 2
@@ -147,6 +147,15 @@ class WorkflowStreamAuthorWorkflow:
147
147
  return "done"
148
148
 
149
149
 
150
+ @workflow.defn(name="tests.replay.recorded-side-effect")
151
+ class RecordedSideEffectWorkflow:
152
+ def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
153
+ def unexpected_execution() -> int:
154
+ raise AssertionError("recorded side effect callable ran during replay")
155
+
156
+ return (yield ctx.side_effect(unexpected_execution))
157
+
158
+
150
159
  @workflow.defn(name="tests.replay.message-stream-consumer")
151
160
  class MessageStreamConsumerWorkflow:
152
161
  def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
@@ -197,6 +206,7 @@ WORKFLOWS = [
197
206
  ParallelMetadataProducerWorkflow,
198
207
  ParallelResultBindingWorkflow,
199
208
  PostConditionReceiversWorkflow,
209
+ RecordedSideEffectWorkflow,
200
210
  SelectionAwaitMarkerWorkflow,
201
211
  SelectionCancellationQueryWorkflow,
202
212
  SelectionMissingNestedOpeningWorkflow,
@@ -820,6 +820,65 @@ class TestWorkerRegistration:
820
820
  assert query_called.is_set()
821
821
  assert query_kwargs["build_id"] == "release-2026.04.22-a1"
822
822
 
823
+ @pytest.mark.parametrize(
824
+ ("poll_method", "loop_method", "task_kind"),
825
+ [
826
+ ("poll_workflow_task", "_poll_workflow_tasks", "workflow"),
827
+ ("poll_activity_task", "_poll_activity_tasks", "activity"),
828
+ ("poll_query_task", "_poll_query_tasks", "query"),
829
+ ],
830
+ )
831
+ @pytest.mark.asyncio
832
+ async def test_poll_warning_omits_large_server_response(
833
+ self,
834
+ mock_client: AsyncMock,
835
+ monkeypatch: pytest.MonkeyPatch,
836
+ caplog: pytest.LogCaptureFixture,
837
+ poll_method: str,
838
+ loop_method: str,
839
+ task_kind: str,
840
+ ) -> None:
841
+ worker = Worker(
842
+ mock_client,
843
+ task_queue="q1",
844
+ workflows=[TestWorkflow],
845
+ activities=[echo_activity],
846
+ )
847
+ getattr(mock_client, poll_method).side_effect = ServerError(
848
+ 429,
849
+ {
850
+ "reason": "long_poll_capacity_exhausted",
851
+ "server_capabilities": {"large": "x" * 10_000},
852
+ },
853
+ )
854
+
855
+ async def stop_after_backoff(seconds: float) -> None:
856
+ assert seconds == 1.0
857
+ worker._stop.set()
858
+
859
+ monkeypatch.setattr(worker_module.asyncio, "sleep", stop_after_backoff)
860
+ with caplog.at_level(logging.WARNING, logger="durable_workflow.worker"):
861
+ await getattr(worker, loop_method)()
862
+
863
+ warnings = [
864
+ record.getMessage()
865
+ for record in caplog.records
866
+ if record.name == "durable_workflow.worker"
867
+ ]
868
+ assert warnings == [
869
+ f"{task_kind} poll error: server returned 429 "
870
+ "reason='long_poll_capacity_exhausted'"
871
+ ]
872
+ assert "server_capabilities" not in caplog.text
873
+
874
+ def test_poll_error_summary_bounds_reason(self) -> None:
875
+ summary = worker_module._poll_error_summary(
876
+ ServerError(429, {"reason": "x" * 10_000})
877
+ )
878
+ assert summary == f"server returned 429 reason={'x' * 64!r}"
879
+ assert worker_module._poll_error_summary(ServerError(503, {})) == "server returned 503"
880
+ assert worker_module._poll_error_summary(ValueError("transport failed")) == "transport failed"
881
+
823
882
  @pytest.mark.asyncio
824
883
  async def test_register_omits_build_id_when_not_configured(
825
884
  self, mock_client: AsyncMock