pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

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 (145) hide show
  1. pyworkflow/__init__.py +10 -1
  2. pyworkflow/celery/tasks.py +272 -24
  3. pyworkflow/cli/__init__.py +4 -1
  4. pyworkflow/cli/commands/runs.py +4 -4
  5. pyworkflow/cli/commands/setup.py +203 -4
  6. pyworkflow/cli/utils/config_generator.py +76 -3
  7. pyworkflow/cli/utils/docker_manager.py +232 -0
  8. pyworkflow/context/__init__.py +13 -0
  9. pyworkflow/context/base.py +26 -0
  10. pyworkflow/context/local.py +80 -0
  11. pyworkflow/context/step_context.py +295 -0
  12. pyworkflow/core/registry.py +6 -1
  13. pyworkflow/core/step.py +141 -0
  14. pyworkflow/core/workflow.py +56 -0
  15. pyworkflow/engine/events.py +30 -0
  16. pyworkflow/engine/replay.py +39 -0
  17. pyworkflow/primitives/child_workflow.py +1 -1
  18. pyworkflow/runtime/local.py +1 -1
  19. pyworkflow/storage/__init__.py +14 -0
  20. pyworkflow/storage/base.py +35 -0
  21. pyworkflow/storage/cassandra.py +1747 -0
  22. pyworkflow/storage/config.py +69 -0
  23. pyworkflow/storage/dynamodb.py +31 -2
  24. pyworkflow/storage/file.py +28 -0
  25. pyworkflow/storage/memory.py +18 -0
  26. pyworkflow/storage/mysql.py +1159 -0
  27. pyworkflow/storage/postgres.py +27 -2
  28. pyworkflow/storage/schemas.py +4 -3
  29. pyworkflow/storage/sqlite.py +25 -2
  30. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/METADATA +7 -4
  31. pyworkflow_engine-0.1.9.dist-info/RECORD +91 -0
  32. pyworkflow_engine-0.1.9.dist-info/top_level.txt +1 -0
  33. dashboard/backend/app/__init__.py +0 -1
  34. dashboard/backend/app/config.py +0 -32
  35. dashboard/backend/app/controllers/__init__.py +0 -6
  36. dashboard/backend/app/controllers/run_controller.py +0 -86
  37. dashboard/backend/app/controllers/workflow_controller.py +0 -33
  38. dashboard/backend/app/dependencies/__init__.py +0 -5
  39. dashboard/backend/app/dependencies/storage.py +0 -50
  40. dashboard/backend/app/repositories/__init__.py +0 -6
  41. dashboard/backend/app/repositories/run_repository.py +0 -80
  42. dashboard/backend/app/repositories/workflow_repository.py +0 -27
  43. dashboard/backend/app/rest/__init__.py +0 -8
  44. dashboard/backend/app/rest/v1/__init__.py +0 -12
  45. dashboard/backend/app/rest/v1/health.py +0 -33
  46. dashboard/backend/app/rest/v1/runs.py +0 -133
  47. dashboard/backend/app/rest/v1/workflows.py +0 -41
  48. dashboard/backend/app/schemas/__init__.py +0 -23
  49. dashboard/backend/app/schemas/common.py +0 -16
  50. dashboard/backend/app/schemas/event.py +0 -24
  51. dashboard/backend/app/schemas/hook.py +0 -25
  52. dashboard/backend/app/schemas/run.py +0 -54
  53. dashboard/backend/app/schemas/step.py +0 -28
  54. dashboard/backend/app/schemas/workflow.py +0 -31
  55. dashboard/backend/app/server.py +0 -87
  56. dashboard/backend/app/services/__init__.py +0 -6
  57. dashboard/backend/app/services/run_service.py +0 -240
  58. dashboard/backend/app/services/workflow_service.py +0 -155
  59. dashboard/backend/main.py +0 -18
  60. docs/concepts/cancellation.mdx +0 -362
  61. docs/concepts/continue-as-new.mdx +0 -434
  62. docs/concepts/events.mdx +0 -266
  63. docs/concepts/fault-tolerance.mdx +0 -370
  64. docs/concepts/hooks.mdx +0 -552
  65. docs/concepts/limitations.mdx +0 -167
  66. docs/concepts/schedules.mdx +0 -775
  67. docs/concepts/sleep.mdx +0 -312
  68. docs/concepts/steps.mdx +0 -301
  69. docs/concepts/workflows.mdx +0 -255
  70. docs/guides/cli.mdx +0 -942
  71. docs/guides/configuration.mdx +0 -560
  72. docs/introduction.mdx +0 -155
  73. docs/quickstart.mdx +0 -279
  74. examples/__init__.py +0 -1
  75. examples/celery/__init__.py +0 -1
  76. examples/celery/durable/docker-compose.yml +0 -55
  77. examples/celery/durable/pyworkflow.config.yaml +0 -12
  78. examples/celery/durable/workflows/__init__.py +0 -122
  79. examples/celery/durable/workflows/basic.py +0 -87
  80. examples/celery/durable/workflows/batch_processing.py +0 -102
  81. examples/celery/durable/workflows/cancellation.py +0 -273
  82. examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
  83. examples/celery/durable/workflows/child_workflows.py +0 -202
  84. examples/celery/durable/workflows/continue_as_new.py +0 -260
  85. examples/celery/durable/workflows/fault_tolerance.py +0 -210
  86. examples/celery/durable/workflows/hooks.py +0 -211
  87. examples/celery/durable/workflows/idempotency.py +0 -112
  88. examples/celery/durable/workflows/long_running.py +0 -99
  89. examples/celery/durable/workflows/retries.py +0 -101
  90. examples/celery/durable/workflows/schedules.py +0 -209
  91. examples/celery/transient/01_basic_workflow.py +0 -91
  92. examples/celery/transient/02_fault_tolerance.py +0 -257
  93. examples/celery/transient/__init__.py +0 -20
  94. examples/celery/transient/pyworkflow.config.yaml +0 -25
  95. examples/local/__init__.py +0 -1
  96. examples/local/durable/01_basic_workflow.py +0 -94
  97. examples/local/durable/02_file_storage.py +0 -132
  98. examples/local/durable/03_retries.py +0 -169
  99. examples/local/durable/04_long_running.py +0 -119
  100. examples/local/durable/05_event_log.py +0 -145
  101. examples/local/durable/06_idempotency.py +0 -148
  102. examples/local/durable/07_hooks.py +0 -334
  103. examples/local/durable/08_cancellation.py +0 -233
  104. examples/local/durable/09_child_workflows.py +0 -198
  105. examples/local/durable/10_child_workflow_patterns.py +0 -265
  106. examples/local/durable/11_continue_as_new.py +0 -249
  107. examples/local/durable/12_schedules.py +0 -198
  108. examples/local/durable/__init__.py +0 -1
  109. examples/local/transient/01_quick_tasks.py +0 -87
  110. examples/local/transient/02_retries.py +0 -130
  111. examples/local/transient/03_sleep.py +0 -141
  112. examples/local/transient/__init__.py +0 -1
  113. pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
  114. pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
  115. tests/examples/__init__.py +0 -0
  116. tests/integration/__init__.py +0 -0
  117. tests/integration/test_cancellation.py +0 -330
  118. tests/integration/test_child_workflows.py +0 -439
  119. tests/integration/test_continue_as_new.py +0 -428
  120. tests/integration/test_dynamodb_storage.py +0 -1146
  121. tests/integration/test_fault_tolerance.py +0 -369
  122. tests/integration/test_schedule_storage.py +0 -484
  123. tests/unit/__init__.py +0 -0
  124. tests/unit/backends/__init__.py +0 -1
  125. tests/unit/backends/test_dynamodb_storage.py +0 -1554
  126. tests/unit/backends/test_postgres_storage.py +0 -1281
  127. tests/unit/backends/test_sqlite_storage.py +0 -1460
  128. tests/unit/conftest.py +0 -41
  129. tests/unit/test_cancellation.py +0 -364
  130. tests/unit/test_child_workflows.py +0 -680
  131. tests/unit/test_continue_as_new.py +0 -441
  132. tests/unit/test_event_limits.py +0 -316
  133. tests/unit/test_executor.py +0 -320
  134. tests/unit/test_fault_tolerance.py +0 -334
  135. tests/unit/test_hooks.py +0 -495
  136. tests/unit/test_registry.py +0 -261
  137. tests/unit/test_replay.py +0 -420
  138. tests/unit/test_schedule_schemas.py +0 -285
  139. tests/unit/test_schedule_utils.py +0 -286
  140. tests/unit/test_scheduled_workflow.py +0 -274
  141. tests/unit/test_step.py +0 -353
  142. tests/unit/test_workflow.py +0 -243
  143. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/WHEEL +0 -0
  144. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/entry_points.txt +0 -0
  145. {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,441 +0,0 @@
1
- """
2
- Unit tests for continue-as-new feature.
3
-
4
- Tests cover:
5
- - ContinueAsNewSignal exception
6
- - continue_as_new() primitive function
7
- - CONTINUED_AS_NEW status and event types
8
- - Storage chain tracking methods
9
- """
10
-
11
- import pytest
12
-
13
- from pyworkflow import (
14
- CancellationError,
15
- LocalContext,
16
- MockContext,
17
- RunStatus,
18
- set_context,
19
- )
20
- from pyworkflow.core.exceptions import ContinueAsNewSignal
21
- from pyworkflow.engine.events import (
22
- EventType,
23
- create_workflow_continued_as_new_event,
24
- )
25
- from pyworkflow.primitives.continue_as_new import continue_as_new
26
- from pyworkflow.storage.memory import InMemoryStorageBackend
27
- from pyworkflow.storage.schemas import WorkflowRun
28
-
29
-
30
- class TestContinueAsNewSignal:
31
- """Test ContinueAsNewSignal exception."""
32
-
33
- def test_signal_default_message(self):
34
- """Test ContinueAsNewSignal has correct default message."""
35
- signal = ContinueAsNewSignal()
36
- assert str(signal) == "Workflow continuing as new execution"
37
-
38
- def test_signal_stores_args(self):
39
- """Test ContinueAsNewSignal stores positional args."""
40
- signal = ContinueAsNewSignal(workflow_args=("arg1", "arg2"))
41
- assert signal.workflow_args == ("arg1", "arg2")
42
-
43
- def test_signal_stores_kwargs(self):
44
- """Test ContinueAsNewSignal stores keyword args."""
45
- signal = ContinueAsNewSignal(workflow_kwargs={"key": "value"})
46
- assert signal.workflow_kwargs == {"key": "value"}
47
-
48
- def test_signal_stores_both_args_and_kwargs(self):
49
- """Test ContinueAsNewSignal stores both args and kwargs."""
50
- signal = ContinueAsNewSignal(workflow_args=("a", "b"), workflow_kwargs={"x": 1, "y": 2})
51
- assert signal.workflow_args == ("a", "b")
52
- assert signal.workflow_kwargs == {"x": 1, "y": 2}
53
-
54
- def test_signal_defaults_to_empty(self):
55
- """Test ContinueAsNewSignal defaults to empty args/kwargs."""
56
- signal = ContinueAsNewSignal()
57
- assert signal.workflow_args == ()
58
- assert signal.workflow_kwargs == {}
59
-
60
- def test_signal_none_kwargs_becomes_empty_dict(self):
61
- """Test None kwargs becomes empty dict."""
62
- signal = ContinueAsNewSignal(workflow_kwargs=None)
63
- assert signal.workflow_kwargs == {}
64
-
65
-
66
- class TestContinueAsNewPrimitive:
67
- """Test continue_as_new() primitive function."""
68
-
69
- def test_raises_runtime_error_outside_context(self):
70
- """Test continue_as_new raises RuntimeError outside context."""
71
- set_context(None)
72
-
73
- with pytest.raises(RuntimeError) as exc_info:
74
- continue_as_new("arg1")
75
-
76
- assert "must be called within a workflow context" in str(exc_info.value)
77
-
78
- def test_raises_value_error_without_args(self):
79
- """Test continue_as_new raises ValueError without args."""
80
- ctx = LocalContext(
81
- run_id="test_run",
82
- workflow_name="test_workflow",
83
- storage=None,
84
- durable=False,
85
- )
86
- set_context(ctx)
87
-
88
- try:
89
- with pytest.raises(ValueError) as exc_info:
90
- continue_as_new()
91
-
92
- assert "requires at least one argument" in str(exc_info.value)
93
- finally:
94
- set_context(None)
95
-
96
- def test_raises_signal_with_positional_args(self):
97
- """Test continue_as_new raises signal with positional args."""
98
- ctx = LocalContext(
99
- run_id="test_run",
100
- workflow_name="test_workflow",
101
- storage=None,
102
- durable=False,
103
- )
104
- set_context(ctx)
105
-
106
- try:
107
- with pytest.raises(ContinueAsNewSignal) as exc_info:
108
- continue_as_new("arg1", "arg2")
109
-
110
- assert exc_info.value.workflow_args == ("arg1", "arg2")
111
- assert exc_info.value.workflow_kwargs == {}
112
- finally:
113
- set_context(None)
114
-
115
- def test_raises_signal_with_keyword_args(self):
116
- """Test continue_as_new raises signal with keyword args."""
117
- ctx = LocalContext(
118
- run_id="test_run",
119
- workflow_name="test_workflow",
120
- storage=None,
121
- durable=False,
122
- )
123
- set_context(ctx)
124
-
125
- try:
126
- with pytest.raises(ContinueAsNewSignal) as exc_info:
127
- continue_as_new(cursor="abc123")
128
-
129
- assert exc_info.value.workflow_args == ()
130
- assert exc_info.value.workflow_kwargs == {"cursor": "abc123"}
131
- finally:
132
- set_context(None)
133
-
134
- def test_raises_signal_with_mixed_args(self):
135
- """Test continue_as_new raises signal with mixed args."""
136
- ctx = LocalContext(
137
- run_id="test_run",
138
- workflow_name="test_workflow",
139
- storage=None,
140
- durable=False,
141
- )
142
- set_context(ctx)
143
-
144
- try:
145
- with pytest.raises(ContinueAsNewSignal) as exc_info:
146
- continue_as_new("pos_arg", key1="val1", key2="val2")
147
-
148
- assert exc_info.value.workflow_args == ("pos_arg",)
149
- assert exc_info.value.workflow_kwargs == {"key1": "val1", "key2": "val2"}
150
- finally:
151
- set_context(None)
152
-
153
- def test_checks_cancellation_before_raising_signal(self):
154
- """Test continue_as_new checks cancellation first."""
155
- ctx = LocalContext(
156
- run_id="test_run",
157
- workflow_name="test_workflow",
158
- storage=None,
159
- durable=False,
160
- )
161
- ctx.request_cancellation(reason="User cancelled")
162
- set_context(ctx)
163
-
164
- try:
165
- with pytest.raises(CancellationError):
166
- continue_as_new("arg1")
167
- finally:
168
- set_context(None)
169
-
170
-
171
- class TestContinuedAsNewStatus:
172
- """Test CONTINUED_AS_NEW status."""
173
-
174
- def test_status_exists_in_enum(self):
175
- """Test CONTINUED_AS_NEW exists in RunStatus enum."""
176
- assert hasattr(RunStatus, "CONTINUED_AS_NEW")
177
- assert RunStatus.CONTINUED_AS_NEW.value == "continued_as_new"
178
-
179
- def test_status_is_distinct_from_completed(self):
180
- """Test CONTINUED_AS_NEW is distinct from COMPLETED."""
181
- assert RunStatus.CONTINUED_AS_NEW != RunStatus.COMPLETED
182
-
183
-
184
- class TestContinuedAsNewEvent:
185
- """Test WORKFLOW_CONTINUED_AS_NEW event type."""
186
-
187
- def test_event_type_exists(self):
188
- """Test WORKFLOW_CONTINUED_AS_NEW exists in EventType enum."""
189
- assert hasattr(EventType, "WORKFLOW_CONTINUED_AS_NEW")
190
- assert EventType.WORKFLOW_CONTINUED_AS_NEW.value == "workflow.continued_as_new"
191
-
192
- def test_create_event(self):
193
- """Test create_workflow_continued_as_new_event."""
194
- event = create_workflow_continued_as_new_event(
195
- run_id="run_123",
196
- new_run_id="run_456",
197
- args='["arg1", "arg2"]',
198
- kwargs='{"key": "value"}',
199
- reason="Event limit reached",
200
- )
201
-
202
- assert event.run_id == "run_123"
203
- assert event.type == EventType.WORKFLOW_CONTINUED_AS_NEW
204
- assert event.data["new_run_id"] == "run_456"
205
- assert event.data["args"] == '["arg1", "arg2"]'
206
- assert event.data["kwargs"] == '{"key": "value"}'
207
- assert event.data["reason"] == "Event limit reached"
208
-
209
- def test_create_event_minimal(self):
210
- """Test create_workflow_continued_as_new_event with minimal params."""
211
- event = create_workflow_continued_as_new_event(
212
- run_id="run_123",
213
- new_run_id="run_456",
214
- args="[]",
215
- kwargs="{}",
216
- )
217
-
218
- assert event.run_id == "run_123"
219
- assert event.type == EventType.WORKFLOW_CONTINUED_AS_NEW
220
- assert event.data.get("reason") is None
221
-
222
-
223
- class TestWorkflowRunContinuationFields:
224
- """Test WorkflowRun continuation tracking fields."""
225
-
226
- def test_workflow_run_has_continuation_fields(self):
227
- """Test WorkflowRun has continued_from and continued_to fields."""
228
- run = WorkflowRun(
229
- run_id="run_123",
230
- workflow_name="test_workflow",
231
- status=RunStatus.PENDING,
232
- continued_from_run_id="run_100",
233
- continued_to_run_id="run_200",
234
- )
235
-
236
- assert run.continued_from_run_id == "run_100"
237
- assert run.continued_to_run_id == "run_200"
238
-
239
- def test_workflow_run_defaults_to_none(self):
240
- """Test continuation fields default to None."""
241
- run = WorkflowRun(
242
- run_id="run_123",
243
- workflow_name="test_workflow",
244
- status=RunStatus.PENDING,
245
- )
246
-
247
- assert run.continued_from_run_id is None
248
- assert run.continued_to_run_id is None
249
-
250
- def test_workflow_run_to_dict_includes_continuation_fields(self):
251
- """Test to_dict includes continuation fields."""
252
- run = WorkflowRun(
253
- run_id="run_123",
254
- workflow_name="test_workflow",
255
- status=RunStatus.CONTINUED_AS_NEW,
256
- continued_from_run_id="run_100",
257
- continued_to_run_id="run_200",
258
- )
259
-
260
- data = run.to_dict()
261
-
262
- assert data["continued_from_run_id"] == "run_100"
263
- assert data["continued_to_run_id"] == "run_200"
264
-
265
- def test_workflow_run_from_dict_parses_continuation_fields(self):
266
- """Test from_dict parses continuation fields."""
267
- from datetime import UTC, datetime
268
-
269
- now = datetime.now(UTC)
270
- data = {
271
- "run_id": "run_123",
272
- "workflow_name": "test_workflow",
273
- "status": "continued_as_new",
274
- "created_at": now.isoformat(),
275
- "updated_at": now.isoformat(),
276
- "continued_from_run_id": "run_100",
277
- "continued_to_run_id": "run_200",
278
- }
279
-
280
- run = WorkflowRun.from_dict(data)
281
-
282
- assert run.continued_from_run_id == "run_100"
283
- assert run.continued_to_run_id == "run_200"
284
-
285
-
286
- class TestStorageChainMethods:
287
- """Test storage backend chain tracking methods."""
288
-
289
- @pytest.mark.asyncio
290
- async def test_update_run_continuation(self):
291
- """Test update_run_continuation sets continued_to_run_id."""
292
- storage = InMemoryStorageBackend()
293
-
294
- run = WorkflowRun(
295
- run_id="run_1",
296
- workflow_name="test_workflow",
297
- status=RunStatus.PENDING,
298
- )
299
- await storage.create_run(run)
300
-
301
- await storage.update_run_continuation("run_1", "run_2")
302
-
303
- updated_run = await storage.get_run("run_1")
304
- assert updated_run.continued_to_run_id == "run_2"
305
-
306
- @pytest.mark.asyncio
307
- async def test_update_run_continuation_nonexistent(self):
308
- """Test update_run_continuation for non-existent run does not raise."""
309
- storage = InMemoryStorageBackend()
310
-
311
- # Should not raise
312
- await storage.update_run_continuation("nonexistent_run", "run_2")
313
-
314
- @pytest.mark.asyncio
315
- async def test_get_workflow_chain_single_run(self):
316
- """Test get_workflow_chain returns single run for no chain."""
317
- storage = InMemoryStorageBackend()
318
-
319
- run = WorkflowRun(
320
- run_id="run_1",
321
- workflow_name="test_workflow",
322
- status=RunStatus.PENDING,
323
- )
324
- await storage.create_run(run)
325
-
326
- chain = await storage.get_workflow_chain("run_1")
327
-
328
- assert len(chain) == 1
329
- assert chain[0].run_id == "run_1"
330
-
331
- @pytest.mark.asyncio
332
- async def test_get_workflow_chain_two_runs(self):
333
- """Test get_workflow_chain returns ordered chain of two runs."""
334
- storage = InMemoryStorageBackend()
335
-
336
- # Create first run
337
- run1 = WorkflowRun(
338
- run_id="run_1",
339
- workflow_name="test_workflow",
340
- status=RunStatus.CONTINUED_AS_NEW,
341
- )
342
- await storage.create_run(run1)
343
-
344
- # Create second run with continued_from
345
- run2 = WorkflowRun(
346
- run_id="run_2",
347
- workflow_name="test_workflow",
348
- status=RunStatus.RUNNING,
349
- continued_from_run_id="run_1",
350
- )
351
- await storage.create_run(run2)
352
-
353
- # Link first run to second
354
- await storage.update_run_continuation("run_1", "run_2")
355
-
356
- # Query from either run should give same chain
357
- chain_from_1 = await storage.get_workflow_chain("run_1")
358
- chain_from_2 = await storage.get_workflow_chain("run_2")
359
-
360
- assert len(chain_from_1) == 2
361
- assert len(chain_from_2) == 2
362
- assert chain_from_1[0].run_id == "run_1"
363
- assert chain_from_1[1].run_id == "run_2"
364
- assert chain_from_2[0].run_id == "run_1"
365
- assert chain_from_2[1].run_id == "run_2"
366
-
367
- @pytest.mark.asyncio
368
- async def test_get_workflow_chain_three_runs(self):
369
- """Test get_workflow_chain returns ordered chain of three runs."""
370
- storage = InMemoryStorageBackend()
371
-
372
- # Create chain: run_1 -> run_2 -> run_3
373
- run1 = WorkflowRun(
374
- run_id="run_1",
375
- workflow_name="test_workflow",
376
- status=RunStatus.CONTINUED_AS_NEW,
377
- )
378
- await storage.create_run(run1)
379
-
380
- run2 = WorkflowRun(
381
- run_id="run_2",
382
- workflow_name="test_workflow",
383
- status=RunStatus.CONTINUED_AS_NEW,
384
- continued_from_run_id="run_1",
385
- )
386
- await storage.create_run(run2)
387
-
388
- run3 = WorkflowRun(
389
- run_id="run_3",
390
- workflow_name="test_workflow",
391
- status=RunStatus.RUNNING,
392
- continued_from_run_id="run_2",
393
- )
394
- await storage.create_run(run3)
395
-
396
- await storage.update_run_continuation("run_1", "run_2")
397
- await storage.update_run_continuation("run_2", "run_3")
398
-
399
- # Query from middle run
400
- chain = await storage.get_workflow_chain("run_2")
401
-
402
- assert len(chain) == 3
403
- assert [r.run_id for r in chain] == ["run_1", "run_2", "run_3"]
404
-
405
- @pytest.mark.asyncio
406
- async def test_get_workflow_chain_nonexistent_run(self):
407
- """Test get_workflow_chain returns empty list for nonexistent run."""
408
- storage = InMemoryStorageBackend()
409
-
410
- chain = await storage.get_workflow_chain("nonexistent")
411
-
412
- assert chain == []
413
-
414
-
415
- class TestMockContextContinueAsNew:
416
- """Test MockContext with continue_as_new."""
417
-
418
- def test_mock_context_allows_continue_as_new(self):
419
- """Test continue_as_new works with MockContext."""
420
- ctx = MockContext(run_id="test", workflow_name="test")
421
- set_context(ctx)
422
-
423
- try:
424
- with pytest.raises(ContinueAsNewSignal) as exc_info:
425
- continue_as_new("arg1")
426
-
427
- assert exc_info.value.workflow_args == ("arg1",)
428
- finally:
429
- set_context(None)
430
-
431
- def test_mock_context_cancellation_prevents_continue_as_new(self):
432
- """Test MockContext cancellation prevents continue_as_new."""
433
- ctx = MockContext(run_id="test", workflow_name="test")
434
- ctx.request_cancellation()
435
- set_context(ctx)
436
-
437
- try:
438
- with pytest.raises(CancellationError):
439
- continue_as_new("arg1")
440
- finally:
441
- set_context(None)