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,1146 +0,0 @@
1
- """
2
- Integration tests for DynamoDB storage backend.
3
-
4
- These tests require DynamoDB Local to be running on localhost:8000.
5
- To start DynamoDB Local:
6
- docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -sharedDb -inMemory
7
-
8
- Tests will be skipped if DynamoDB Local is not available.
9
- """
10
-
11
- import socket
12
- from datetime import UTC, datetime, timedelta
13
-
14
- import pytest
15
-
16
- # Skip all tests if dependencies are not installed
17
- pytest.importorskip("aiobotocore")
18
-
19
- from pyworkflow.engine.events import Event, EventType
20
- from pyworkflow.storage.dynamodb import DynamoDBStorageBackend
21
- from pyworkflow.storage.schemas import (
22
- Hook,
23
- HookStatus,
24
- RunStatus,
25
- Schedule,
26
- ScheduleSpec,
27
- ScheduleStatus,
28
- StepExecution,
29
- StepStatus,
30
- WorkflowRun,
31
- )
32
-
33
-
34
- def is_dynamodb_local_available(host: str = "localhost", port: int = 8000) -> bool:
35
- """Check if DynamoDB Local is available."""
36
- try:
37
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
38
- sock.settimeout(1)
39
- result = sock.connect_ex((host, port))
40
- sock.close()
41
- return result == 0
42
- except Exception:
43
- return False
44
-
45
-
46
- # Skip all tests if DynamoDB Local is not running
47
- pytestmark = pytest.mark.skipif(
48
- not is_dynamodb_local_available(),
49
- reason="DynamoDB Local is not available at localhost:8000. "
50
- "Start with: docker run -p 8000:8000 amazon/dynamodb-local "
51
- "-jar DynamoDBLocal.jar -sharedDb -inMemory",
52
- )
53
-
54
-
55
- @pytest.fixture
56
- def aws_credentials():
57
- """Set mock AWS credentials for DynamoDB Local."""
58
- import os
59
-
60
- os.environ["AWS_ACCESS_KEY_ID"] = "testing"
61
- os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
62
- os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
63
-
64
-
65
- @pytest.fixture
66
- async def dynamodb_storage(aws_credentials):
67
- """Create a DynamoDB storage backend connected to DynamoDB Local."""
68
- import uuid
69
-
70
- # Use unique table name per test to avoid conflicts
71
- table_name = f"test_pyworkflow_{uuid.uuid4().hex[:8]}"
72
-
73
- backend = DynamoDBStorageBackend(
74
- table_name=table_name,
75
- region="us-east-1",
76
- endpoint_url="http://localhost:8000",
77
- )
78
- await backend.connect()
79
- yield backend
80
-
81
- # Cleanup: delete the test table
82
- try:
83
- async with backend._get_client() as client:
84
- await client.delete_table(TableName=table_name)
85
- except Exception:
86
- pass # Table may already be deleted or not exist
87
-
88
- await backend.disconnect()
89
-
90
-
91
- class TestWorkflowRunCRUD:
92
- """Test workflow run CRUD operations."""
93
-
94
- @pytest.mark.asyncio
95
- async def test_create_and_get_run(self, dynamodb_storage):
96
- """Test creating and retrieving a workflow run."""
97
- now = datetime.now(UTC)
98
- run = WorkflowRun(
99
- run_id="test_run_001",
100
- workflow_name="test_workflow",
101
- status=RunStatus.PENDING,
102
- created_at=now,
103
- updated_at=now,
104
- input_args="[]",
105
- input_kwargs='{"key": "value"}',
106
- )
107
-
108
- await dynamodb_storage.create_run(run)
109
-
110
- retrieved = await dynamodb_storage.get_run("test_run_001")
111
- assert retrieved is not None
112
- assert retrieved.run_id == "test_run_001"
113
- assert retrieved.workflow_name == "test_workflow"
114
- assert retrieved.status == RunStatus.PENDING
115
-
116
- @pytest.mark.asyncio
117
- async def test_get_run_not_found(self, dynamodb_storage):
118
- """Test getting a non-existent run."""
119
- retrieved = await dynamodb_storage.get_run("nonexistent")
120
- assert retrieved is None
121
-
122
- @pytest.mark.asyncio
123
- async def test_update_run_status(self, dynamodb_storage):
124
- """Test updating run status."""
125
- now = datetime.now(UTC)
126
- run = WorkflowRun(
127
- run_id="status_update_test",
128
- workflow_name="test_workflow",
129
- status=RunStatus.PENDING,
130
- created_at=now,
131
- updated_at=now,
132
- input_args="[]",
133
- input_kwargs="{}",
134
- )
135
- await dynamodb_storage.create_run(run)
136
-
137
- await dynamodb_storage.update_run_status(
138
- run_id="status_update_test",
139
- status=RunStatus.RUNNING,
140
- )
141
-
142
- retrieved = await dynamodb_storage.get_run("status_update_test")
143
- assert retrieved.status == RunStatus.RUNNING
144
-
145
- @pytest.mark.asyncio
146
- async def test_update_run_status_with_result(self, dynamodb_storage):
147
- """Test updating run status with result."""
148
- now = datetime.now(UTC)
149
- run = WorkflowRun(
150
- run_id="result_test",
151
- workflow_name="test_workflow",
152
- status=RunStatus.RUNNING,
153
- created_at=now,
154
- updated_at=now,
155
- input_args="[]",
156
- input_kwargs="{}",
157
- )
158
- await dynamodb_storage.create_run(run)
159
-
160
- await dynamodb_storage.update_run_status(
161
- run_id="result_test",
162
- status=RunStatus.COMPLETED,
163
- result='{"output": "success"}',
164
- )
165
-
166
- retrieved = await dynamodb_storage.get_run("result_test")
167
- assert retrieved.status == RunStatus.COMPLETED
168
- assert retrieved.result == '{"output": "success"}'
169
-
170
- @pytest.mark.asyncio
171
- async def test_update_run_status_with_error(self, dynamodb_storage):
172
- """Test updating run status with error."""
173
- now = datetime.now(UTC)
174
- run = WorkflowRun(
175
- run_id="error_test",
176
- workflow_name="test_workflow",
177
- status=RunStatus.RUNNING,
178
- created_at=now,
179
- updated_at=now,
180
- input_args="[]",
181
- input_kwargs="{}",
182
- )
183
- await dynamodb_storage.create_run(run)
184
-
185
- await dynamodb_storage.update_run_status(
186
- run_id="error_test",
187
- status=RunStatus.FAILED,
188
- error="Test error message",
189
- )
190
-
191
- retrieved = await dynamodb_storage.get_run("error_test")
192
- assert retrieved.status == RunStatus.FAILED
193
- assert retrieved.error == "Test error message"
194
-
195
- @pytest.mark.asyncio
196
- async def test_get_run_by_idempotency_key(self, dynamodb_storage):
197
- """Test retrieving run by idempotency key."""
198
- now = datetime.now(UTC)
199
- run = WorkflowRun(
200
- run_id="idempotent_run",
201
- workflow_name="test_workflow",
202
- status=RunStatus.PENDING,
203
- created_at=now,
204
- updated_at=now,
205
- input_args="[]",
206
- input_kwargs="{}",
207
- idempotency_key="unique_key_123",
208
- )
209
- await dynamodb_storage.create_run(run)
210
-
211
- retrieved = await dynamodb_storage.get_run_by_idempotency_key("unique_key_123")
212
- assert retrieved is not None
213
- assert retrieved.run_id == "idempotent_run"
214
-
215
- @pytest.mark.asyncio
216
- async def test_get_run_by_idempotency_key_not_found(self, dynamodb_storage):
217
- """Test idempotency key lookup when not found."""
218
- retrieved = await dynamodb_storage.get_run_by_idempotency_key("nonexistent_key")
219
- assert retrieved is None
220
-
221
- @pytest.mark.asyncio
222
- async def test_update_recovery_attempts(self, dynamodb_storage):
223
- """Test updating recovery attempts counter."""
224
- now = datetime.now(UTC)
225
- run = WorkflowRun(
226
- run_id="recovery_test",
227
- workflow_name="test_workflow",
228
- status=RunStatus.RUNNING,
229
- created_at=now,
230
- updated_at=now,
231
- input_args="[]",
232
- input_kwargs="{}",
233
- recovery_attempts=0,
234
- )
235
- await dynamodb_storage.create_run(run)
236
-
237
- await dynamodb_storage.update_run_recovery_attempts("recovery_test", 2)
238
-
239
- retrieved = await dynamodb_storage.get_run("recovery_test")
240
- assert retrieved.recovery_attempts == 2
241
-
242
- @pytest.mark.asyncio
243
- async def test_list_runs(self, dynamodb_storage):
244
- """Test listing workflow runs."""
245
- now = datetime.now(UTC)
246
-
247
- # Create multiple runs
248
- for i in range(5):
249
- run = WorkflowRun(
250
- run_id=f"list_test_{i}",
251
- workflow_name="test_workflow",
252
- status=RunStatus.PENDING,
253
- created_at=now + timedelta(seconds=i),
254
- updated_at=now + timedelta(seconds=i),
255
- input_args="[]",
256
- input_kwargs="{}",
257
- )
258
- await dynamodb_storage.create_run(run)
259
-
260
- runs, next_cursor = await dynamodb_storage.list_runs()
261
-
262
- assert len(runs) == 5
263
-
264
- @pytest.mark.asyncio
265
- async def test_list_runs_with_status_filter(self, dynamodb_storage):
266
- """Test listing runs filtered by status."""
267
- now = datetime.now(UTC)
268
-
269
- # Create runs with different statuses
270
- for i, status in enumerate([RunStatus.PENDING, RunStatus.RUNNING, RunStatus.COMPLETED]):
271
- run = WorkflowRun(
272
- run_id=f"status_filter_{i}",
273
- workflow_name="test_workflow",
274
- status=status,
275
- created_at=now + timedelta(seconds=i),
276
- updated_at=now + timedelta(seconds=i),
277
- input_args="[]",
278
- input_kwargs="{}",
279
- )
280
- await dynamodb_storage.create_run(run)
281
-
282
- runs, _ = await dynamodb_storage.list_runs(status=RunStatus.PENDING)
283
-
284
- assert len(runs) == 1
285
- assert runs[0].status == RunStatus.PENDING
286
-
287
-
288
- class TestEventOperations:
289
- """Test event log operations."""
290
-
291
- @pytest.mark.asyncio
292
- async def test_record_and_get_events(self, dynamodb_storage):
293
- """Test recording and retrieving events."""
294
- now = datetime.now(UTC)
295
-
296
- # First create a run
297
- run = WorkflowRun(
298
- run_id="event_test_run",
299
- workflow_name="test_workflow",
300
- status=RunStatus.RUNNING,
301
- created_at=now,
302
- updated_at=now,
303
- input_args="[]",
304
- input_kwargs="{}",
305
- )
306
- await dynamodb_storage.create_run(run)
307
-
308
- # Record events
309
- for i in range(3):
310
- event = Event(
311
- event_id=f"evt_{i}",
312
- run_id="event_test_run",
313
- type=EventType.STEP_COMPLETED,
314
- timestamp=now + timedelta(seconds=i),
315
- data={"step_id": f"step_{i}", "result": f"result_{i}"},
316
- )
317
- await dynamodb_storage.record_event(event)
318
-
319
- # Retrieve events
320
- events = await dynamodb_storage.get_events("event_test_run")
321
-
322
- assert len(events) == 3
323
- # Events should be ordered by sequence
324
- for i, event in enumerate(events):
325
- assert event.sequence == i
326
-
327
- @pytest.mark.asyncio
328
- async def test_get_events_with_type_filter(self, dynamodb_storage):
329
- """Test retrieving events filtered by type."""
330
- now = datetime.now(UTC)
331
-
332
- run = WorkflowRun(
333
- run_id="event_filter_run",
334
- workflow_name="test_workflow",
335
- status=RunStatus.RUNNING,
336
- created_at=now,
337
- updated_at=now,
338
- input_args="[]",
339
- input_kwargs="{}",
340
- )
341
- await dynamodb_storage.create_run(run)
342
-
343
- # Record different event types
344
- events_to_create = [
345
- (EventType.WORKFLOW_STARTED, {}),
346
- (EventType.STEP_STARTED, {"step_id": "step_1"}),
347
- (EventType.STEP_COMPLETED, {"step_id": "step_1", "result": "ok"}),
348
- (EventType.WORKFLOW_COMPLETED, {"result": "done"}),
349
- ]
350
-
351
- for i, (event_type, data) in enumerate(events_to_create):
352
- event = Event(
353
- event_id=f"evt_{i}",
354
- run_id="event_filter_run",
355
- type=event_type,
356
- timestamp=now + timedelta(seconds=i),
357
- data=data,
358
- )
359
- await dynamodb_storage.record_event(event)
360
-
361
- # Filter by type
362
- step_events = await dynamodb_storage.get_events(
363
- "event_filter_run",
364
- event_types=["step.completed"],
365
- )
366
-
367
- assert len(step_events) == 1
368
- assert step_events[0].type == EventType.STEP_COMPLETED
369
-
370
- @pytest.mark.asyncio
371
- async def test_get_latest_event(self, dynamodb_storage):
372
- """Test getting the latest event."""
373
- now = datetime.now(UTC)
374
-
375
- run = WorkflowRun(
376
- run_id="latest_event_run",
377
- workflow_name="test_workflow",
378
- status=RunStatus.RUNNING,
379
- created_at=now,
380
- updated_at=now,
381
- input_args="[]",
382
- input_kwargs="{}",
383
- )
384
- await dynamodb_storage.create_run(run)
385
-
386
- # Record multiple events
387
- for i in range(5):
388
- event = Event(
389
- event_id=f"evt_{i}",
390
- run_id="latest_event_run",
391
- type=EventType.STEP_COMPLETED,
392
- timestamp=now + timedelta(seconds=i),
393
- data={"index": i},
394
- )
395
- await dynamodb_storage.record_event(event)
396
-
397
- latest = await dynamodb_storage.get_latest_event("latest_event_run")
398
-
399
- assert latest is not None
400
- assert latest.sequence == 4 # 0-indexed, so 5th event is sequence 4
401
-
402
-
403
- class TestStepOperations:
404
- """Test step execution operations."""
405
-
406
- @pytest.mark.asyncio
407
- async def test_create_and_get_step(self, dynamodb_storage):
408
- """Test creating and retrieving a step."""
409
- now = datetime.now(UTC)
410
-
411
- # First create a run
412
- run = WorkflowRun(
413
- run_id="step_test_run",
414
- workflow_name="test_workflow",
415
- status=RunStatus.RUNNING,
416
- created_at=now,
417
- updated_at=now,
418
- input_args="[]",
419
- input_kwargs="{}",
420
- )
421
- await dynamodb_storage.create_run(run)
422
-
423
- step = StepExecution(
424
- step_id="step_001",
425
- run_id="step_test_run",
426
- step_name="process_data",
427
- status=StepStatus.RUNNING,
428
- created_at=now,
429
- started_at=now,
430
- input_args="[]",
431
- input_kwargs='{"data": "test"}',
432
- attempt=1,
433
- )
434
- await dynamodb_storage.create_step(step)
435
-
436
- retrieved = await dynamodb_storage.get_step("step_001")
437
- assert retrieved is not None
438
- assert retrieved.step_id == "step_001"
439
- assert retrieved.step_name == "process_data"
440
- assert retrieved.status == StepStatus.RUNNING
441
-
442
- @pytest.mark.asyncio
443
- async def test_update_step_status(self, dynamodb_storage):
444
- """Test updating step status."""
445
- now = datetime.now(UTC)
446
-
447
- run = WorkflowRun(
448
- run_id="step_update_run",
449
- workflow_name="test_workflow",
450
- status=RunStatus.RUNNING,
451
- created_at=now,
452
- updated_at=now,
453
- input_args="[]",
454
- input_kwargs="{}",
455
- )
456
- await dynamodb_storage.create_run(run)
457
-
458
- step = StepExecution(
459
- step_id="step_update_001",
460
- run_id="step_update_run",
461
- step_name="test_step",
462
- status=StepStatus.RUNNING,
463
- created_at=now,
464
- input_args="[]",
465
- input_kwargs="{}",
466
- attempt=1,
467
- )
468
- await dynamodb_storage.create_step(step)
469
-
470
- await dynamodb_storage.update_step_status(
471
- step_id="step_update_001",
472
- status="completed",
473
- result='{"output": "success"}',
474
- )
475
-
476
- retrieved = await dynamodb_storage.get_step("step_update_001")
477
- assert retrieved.status == StepStatus.COMPLETED
478
- assert retrieved.result == '{"output": "success"}'
479
-
480
- @pytest.mark.asyncio
481
- async def test_list_steps(self, dynamodb_storage):
482
- """Test listing steps for a run."""
483
- now = datetime.now(UTC)
484
-
485
- run = WorkflowRun(
486
- run_id="list_steps_run",
487
- workflow_name="test_workflow",
488
- status=RunStatus.RUNNING,
489
- created_at=now,
490
- updated_at=now,
491
- input_args="[]",
492
- input_kwargs="{}",
493
- )
494
- await dynamodb_storage.create_run(run)
495
-
496
- # Create multiple steps
497
- for i in range(3):
498
- step = StepExecution(
499
- step_id=f"list_step_{i}",
500
- run_id="list_steps_run",
501
- step_name=f"step_{i}",
502
- status=StepStatus.COMPLETED,
503
- created_at=now + timedelta(seconds=i),
504
- input_args="[]",
505
- input_kwargs="{}",
506
- attempt=1,
507
- )
508
- await dynamodb_storage.create_step(step)
509
-
510
- steps = await dynamodb_storage.list_steps("list_steps_run")
511
-
512
- assert len(steps) == 3
513
-
514
-
515
- class TestHookOperations:
516
- """Test webhook/hook operations."""
517
-
518
- @pytest.mark.asyncio
519
- async def test_create_and_get_hook(self, dynamodb_storage):
520
- """Test creating and retrieving a hook."""
521
- now = datetime.now(UTC)
522
-
523
- run = WorkflowRun(
524
- run_id="hook_test_run",
525
- workflow_name="test_workflow",
526
- status=RunStatus.RUNNING,
527
- created_at=now,
528
- updated_at=now,
529
- input_args="[]",
530
- input_kwargs="{}",
531
- )
532
- await dynamodb_storage.create_run(run)
533
-
534
- hook = Hook(
535
- hook_id="hook_001",
536
- run_id="hook_test_run",
537
- token="token_abc123",
538
- status=HookStatus.PENDING,
539
- created_at=now,
540
- expires_at=now + timedelta(hours=1),
541
- )
542
- await dynamodb_storage.create_hook(hook)
543
-
544
- retrieved = await dynamodb_storage.get_hook("hook_001")
545
- assert retrieved is not None
546
- assert retrieved.hook_id == "hook_001"
547
- assert retrieved.token == "token_abc123"
548
- assert retrieved.status == HookStatus.PENDING
549
-
550
- @pytest.mark.asyncio
551
- async def test_get_hook_by_token(self, dynamodb_storage):
552
- """Test retrieving hook by token."""
553
- now = datetime.now(UTC)
554
-
555
- run = WorkflowRun(
556
- run_id="hook_token_run",
557
- workflow_name="test_workflow",
558
- status=RunStatus.RUNNING,
559
- created_at=now,
560
- updated_at=now,
561
- input_args="[]",
562
- input_kwargs="{}",
563
- )
564
- await dynamodb_storage.create_run(run)
565
-
566
- hook = Hook(
567
- hook_id="hook_token_001",
568
- run_id="hook_token_run",
569
- token="unique_token_xyz",
570
- status=HookStatus.PENDING,
571
- created_at=now,
572
- )
573
- await dynamodb_storage.create_hook(hook)
574
-
575
- retrieved = await dynamodb_storage.get_hook_by_token("unique_token_xyz")
576
- assert retrieved is not None
577
- assert retrieved.hook_id == "hook_token_001"
578
-
579
- @pytest.mark.asyncio
580
- async def test_update_hook_status(self, dynamodb_storage):
581
- """Test updating hook status with payload."""
582
- now = datetime.now(UTC)
583
-
584
- run = WorkflowRun(
585
- run_id="hook_update_run",
586
- workflow_name="test_workflow",
587
- status=RunStatus.RUNNING,
588
- created_at=now,
589
- updated_at=now,
590
- input_args="[]",
591
- input_kwargs="{}",
592
- )
593
- await dynamodb_storage.create_run(run)
594
-
595
- hook = Hook(
596
- hook_id="hook_update_001",
597
- run_id="hook_update_run",
598
- token="update_token",
599
- status=HookStatus.PENDING,
600
- created_at=now,
601
- )
602
- await dynamodb_storage.create_hook(hook)
603
-
604
- await dynamodb_storage.update_hook_status(
605
- hook_id="hook_update_001",
606
- status=HookStatus.RECEIVED,
607
- payload='{"data": "webhook_payload"}',
608
- )
609
-
610
- retrieved = await dynamodb_storage.get_hook("hook_update_001")
611
- assert retrieved.status == HookStatus.RECEIVED
612
- assert retrieved.payload == '{"data": "webhook_payload"}'
613
- assert retrieved.received_at is not None
614
-
615
- @pytest.mark.asyncio
616
- async def test_list_hooks(self, dynamodb_storage):
617
- """Test listing hooks for a run."""
618
- now = datetime.now(UTC)
619
-
620
- run = WorkflowRun(
621
- run_id="list_hooks_run",
622
- workflow_name="test_workflow",
623
- status=RunStatus.RUNNING,
624
- created_at=now,
625
- updated_at=now,
626
- input_args="[]",
627
- input_kwargs="{}",
628
- )
629
- await dynamodb_storage.create_run(run)
630
-
631
- # Create multiple hooks
632
- for i in range(3):
633
- hook = Hook(
634
- hook_id=f"list_hook_{i}",
635
- run_id="list_hooks_run",
636
- token=f"token_{i}",
637
- status=HookStatus.PENDING,
638
- created_at=now + timedelta(seconds=i),
639
- )
640
- await dynamodb_storage.create_hook(hook)
641
-
642
- hooks = await dynamodb_storage.list_hooks(run_id="list_hooks_run")
643
-
644
- assert len(hooks) == 3
645
-
646
-
647
- class TestCancellationOperations:
648
- """Test cancellation flag operations."""
649
-
650
- @pytest.mark.asyncio
651
- async def test_set_and_check_cancellation_flag(self, dynamodb_storage):
652
- """Test setting and checking cancellation flag."""
653
- now = datetime.now(UTC)
654
-
655
- run = WorkflowRun(
656
- run_id="cancel_test_run",
657
- workflow_name="test_workflow",
658
- status=RunStatus.RUNNING,
659
- created_at=now,
660
- updated_at=now,
661
- input_args="[]",
662
- input_kwargs="{}",
663
- )
664
- await dynamodb_storage.create_run(run)
665
-
666
- # Initially no cancellation
667
- assert await dynamodb_storage.check_cancellation_flag("cancel_test_run") is False
668
-
669
- # Set cancellation
670
- await dynamodb_storage.set_cancellation_flag("cancel_test_run")
671
-
672
- # Now should be cancelled
673
- assert await dynamodb_storage.check_cancellation_flag("cancel_test_run") is True
674
-
675
- @pytest.mark.asyncio
676
- async def test_clear_cancellation_flag(self, dynamodb_storage):
677
- """Test clearing cancellation flag."""
678
- now = datetime.now(UTC)
679
-
680
- run = WorkflowRun(
681
- run_id="clear_cancel_run",
682
- workflow_name="test_workflow",
683
- status=RunStatus.RUNNING,
684
- created_at=now,
685
- updated_at=now,
686
- input_args="[]",
687
- input_kwargs="{}",
688
- )
689
- await dynamodb_storage.create_run(run)
690
-
691
- await dynamodb_storage.set_cancellation_flag("clear_cancel_run")
692
- assert await dynamodb_storage.check_cancellation_flag("clear_cancel_run") is True
693
-
694
- await dynamodb_storage.clear_cancellation_flag("clear_cancel_run")
695
- assert await dynamodb_storage.check_cancellation_flag("clear_cancel_run") is False
696
-
697
-
698
- class TestContinueAsNewOperations:
699
- """Test continue-as-new chain operations."""
700
-
701
- @pytest.mark.asyncio
702
- async def test_update_run_continuation(self, dynamodb_storage):
703
- """Test updating run continuation link."""
704
- now = datetime.now(UTC)
705
-
706
- # Create original run
707
- run1 = WorkflowRun(
708
- run_id="chain_run_1",
709
- workflow_name="test_workflow",
710
- status=RunStatus.COMPLETED,
711
- created_at=now,
712
- updated_at=now,
713
- input_args="[]",
714
- input_kwargs="{}",
715
- )
716
- await dynamodb_storage.create_run(run1)
717
-
718
- # Create continuation run
719
- run2 = WorkflowRun(
720
- run_id="chain_run_2",
721
- workflow_name="test_workflow",
722
- status=RunStatus.RUNNING,
723
- created_at=now + timedelta(seconds=1),
724
- updated_at=now + timedelta(seconds=1),
725
- input_args="[]",
726
- input_kwargs="{}",
727
- continued_from_run_id="chain_run_1",
728
- )
729
- await dynamodb_storage.create_run(run2)
730
-
731
- # Link the runs
732
- await dynamodb_storage.update_run_continuation("chain_run_1", "chain_run_2")
733
-
734
- retrieved = await dynamodb_storage.get_run("chain_run_1")
735
- assert retrieved.continued_to_run_id == "chain_run_2"
736
-
737
- @pytest.mark.asyncio
738
- async def test_get_workflow_chain(self, dynamodb_storage):
739
- """Test getting workflow chain."""
740
- now = datetime.now(UTC)
741
-
742
- # Create a chain of 3 runs
743
- run1 = WorkflowRun(
744
- run_id="chain_1",
745
- workflow_name="test_workflow",
746
- status=RunStatus.COMPLETED,
747
- created_at=now,
748
- updated_at=now,
749
- input_args="[]",
750
- input_kwargs="{}",
751
- continued_to_run_id="chain_2",
752
- )
753
- await dynamodb_storage.create_run(run1)
754
-
755
- run2 = WorkflowRun(
756
- run_id="chain_2",
757
- workflow_name="test_workflow",
758
- status=RunStatus.COMPLETED,
759
- created_at=now + timedelta(seconds=1),
760
- updated_at=now + timedelta(seconds=1),
761
- input_args="[]",
762
- input_kwargs="{}",
763
- continued_from_run_id="chain_1",
764
- continued_to_run_id="chain_3",
765
- )
766
- await dynamodb_storage.create_run(run2)
767
-
768
- run3 = WorkflowRun(
769
- run_id="chain_3",
770
- workflow_name="test_workflow",
771
- status=RunStatus.RUNNING,
772
- created_at=now + timedelta(seconds=2),
773
- updated_at=now + timedelta(seconds=2),
774
- input_args="[]",
775
- input_kwargs="{}",
776
- continued_from_run_id="chain_2",
777
- )
778
- await dynamodb_storage.create_run(run3)
779
-
780
- # Get chain from middle run
781
- chain = await dynamodb_storage.get_workflow_chain("chain_2")
782
-
783
- assert len(chain) == 3
784
- assert chain[0].run_id == "chain_1"
785
- assert chain[1].run_id == "chain_2"
786
- assert chain[2].run_id == "chain_3"
787
-
788
-
789
- class TestChildWorkflowOperations:
790
- """Test child workflow operations."""
791
-
792
- @pytest.mark.asyncio
793
- async def test_get_children(self, dynamodb_storage):
794
- """Test getting child workflows."""
795
- now = datetime.now(UTC)
796
-
797
- # Create parent run
798
- parent = WorkflowRun(
799
- run_id="parent_run",
800
- workflow_name="parent_workflow",
801
- status=RunStatus.RUNNING,
802
- created_at=now,
803
- updated_at=now,
804
- input_args="[]",
805
- input_kwargs="{}",
806
- nesting_depth=0,
807
- )
808
- await dynamodb_storage.create_run(parent)
809
-
810
- # Create child runs
811
- for i in range(3):
812
- child = WorkflowRun(
813
- run_id=f"child_run_{i}",
814
- workflow_name="child_workflow",
815
- status=RunStatus.COMPLETED if i < 2 else RunStatus.RUNNING,
816
- created_at=now + timedelta(seconds=i),
817
- updated_at=now + timedelta(seconds=i),
818
- input_args="[]",
819
- input_kwargs="{}",
820
- parent_run_id="parent_run",
821
- nesting_depth=1,
822
- )
823
- await dynamodb_storage.create_run(child)
824
-
825
- children = await dynamodb_storage.get_children("parent_run")
826
-
827
- assert len(children) == 3
828
-
829
- @pytest.mark.asyncio
830
- async def test_get_children_with_status_filter(self, dynamodb_storage):
831
- """Test getting children filtered by status."""
832
- now = datetime.now(UTC)
833
-
834
- parent = WorkflowRun(
835
- run_id="filter_parent",
836
- workflow_name="parent_workflow",
837
- status=RunStatus.RUNNING,
838
- created_at=now,
839
- updated_at=now,
840
- input_args="[]",
841
- input_kwargs="{}",
842
- )
843
- await dynamodb_storage.create_run(parent)
844
-
845
- # Create child runs with different statuses
846
- for i, status in enumerate([RunStatus.COMPLETED, RunStatus.COMPLETED, RunStatus.RUNNING]):
847
- child = WorkflowRun(
848
- run_id=f"filter_child_{i}",
849
- workflow_name="child_workflow",
850
- status=status,
851
- created_at=now + timedelta(seconds=i),
852
- updated_at=now + timedelta(seconds=i),
853
- input_args="[]",
854
- input_kwargs="{}",
855
- parent_run_id="filter_parent",
856
- nesting_depth=1,
857
- )
858
- await dynamodb_storage.create_run(child)
859
-
860
- completed = await dynamodb_storage.get_children("filter_parent", status=RunStatus.COMPLETED)
861
-
862
- assert len(completed) == 2
863
-
864
- @pytest.mark.asyncio
865
- async def test_get_parent(self, dynamodb_storage):
866
- """Test getting parent workflow."""
867
- now = datetime.now(UTC)
868
-
869
- parent = WorkflowRun(
870
- run_id="the_parent",
871
- workflow_name="parent_workflow",
872
- status=RunStatus.RUNNING,
873
- created_at=now,
874
- updated_at=now,
875
- input_args="[]",
876
- input_kwargs="{}",
877
- )
878
- await dynamodb_storage.create_run(parent)
879
-
880
- child = WorkflowRun(
881
- run_id="the_child",
882
- workflow_name="child_workflow",
883
- status=RunStatus.RUNNING,
884
- created_at=now,
885
- updated_at=now,
886
- input_args="[]",
887
- input_kwargs="{}",
888
- parent_run_id="the_parent",
889
- nesting_depth=1,
890
- )
891
- await dynamodb_storage.create_run(child)
892
-
893
- retrieved_parent = await dynamodb_storage.get_parent("the_child")
894
-
895
- assert retrieved_parent is not None
896
- assert retrieved_parent.run_id == "the_parent"
897
-
898
- @pytest.mark.asyncio
899
- async def test_get_nesting_depth(self, dynamodb_storage):
900
- """Test getting nesting depth."""
901
- now = datetime.now(UTC)
902
-
903
- run = WorkflowRun(
904
- run_id="depth_test",
905
- workflow_name="test_workflow",
906
- status=RunStatus.RUNNING,
907
- created_at=now,
908
- updated_at=now,
909
- input_args="[]",
910
- input_kwargs="{}",
911
- nesting_depth=2,
912
- )
913
- await dynamodb_storage.create_run(run)
914
-
915
- depth = await dynamodb_storage.get_nesting_depth("depth_test")
916
-
917
- assert depth == 2
918
-
919
-
920
- class TestScheduleOperations:
921
- """Test schedule CRUD operations."""
922
-
923
- @pytest.mark.asyncio
924
- async def test_create_and_get_schedule(self, dynamodb_storage):
925
- """Test creating and retrieving a schedule."""
926
- now = datetime.now(UTC)
927
- spec = ScheduleSpec(cron="0 9 * * *")
928
- schedule = Schedule(
929
- schedule_id="schedule_001",
930
- workflow_name="scheduled_workflow",
931
- spec=spec,
932
- status=ScheduleStatus.ACTIVE,
933
- created_at=now,
934
- )
935
-
936
- await dynamodb_storage.create_schedule(schedule)
937
-
938
- retrieved = await dynamodb_storage.get_schedule("schedule_001")
939
- assert retrieved is not None
940
- assert retrieved.schedule_id == "schedule_001"
941
- assert retrieved.workflow_name == "scheduled_workflow"
942
- assert retrieved.spec.cron == "0 9 * * *"
943
- assert retrieved.status == ScheduleStatus.ACTIVE
944
-
945
- @pytest.mark.asyncio
946
- async def test_update_schedule(self, dynamodb_storage):
947
- """Test updating a schedule."""
948
- now = datetime.now(UTC)
949
- spec = ScheduleSpec(cron="0 9 * * *")
950
- schedule = Schedule(
951
- schedule_id="update_schedule",
952
- workflow_name="test_workflow",
953
- spec=spec,
954
- status=ScheduleStatus.ACTIVE,
955
- created_at=now,
956
- )
957
- await dynamodb_storage.create_schedule(schedule)
958
-
959
- # Update the schedule
960
- schedule.status = ScheduleStatus.PAUSED
961
- schedule.spec = ScheduleSpec(cron="0 10 * * *")
962
- schedule.updated_at = datetime.now(UTC)
963
- await dynamodb_storage.update_schedule(schedule)
964
-
965
- retrieved = await dynamodb_storage.get_schedule("update_schedule")
966
- assert retrieved.status == ScheduleStatus.PAUSED
967
- assert retrieved.spec.cron == "0 10 * * *"
968
-
969
- @pytest.mark.asyncio
970
- async def test_delete_schedule(self, dynamodb_storage):
971
- """Test deleting (soft delete) a schedule."""
972
- now = datetime.now(UTC)
973
- spec = ScheduleSpec(interval="5m")
974
- schedule = Schedule(
975
- schedule_id="delete_schedule",
976
- workflow_name="test_workflow",
977
- spec=spec,
978
- status=ScheduleStatus.ACTIVE,
979
- created_at=now,
980
- )
981
- await dynamodb_storage.create_schedule(schedule)
982
-
983
- await dynamodb_storage.delete_schedule("delete_schedule")
984
-
985
- retrieved = await dynamodb_storage.get_schedule("delete_schedule")
986
- assert retrieved.status == ScheduleStatus.DELETED
987
-
988
- @pytest.mark.asyncio
989
- async def test_list_schedules(self, dynamodb_storage):
990
- """Test listing schedules."""
991
- now = datetime.now(UTC)
992
-
993
- for i in range(5):
994
- schedule = Schedule(
995
- schedule_id=f"list_sched_{i}",
996
- workflow_name=f"workflow_{i % 2}",
997
- spec=ScheduleSpec(cron="0 9 * * *"),
998
- status=ScheduleStatus.ACTIVE if i % 2 == 0 else ScheduleStatus.PAUSED,
999
- created_at=now + timedelta(seconds=i),
1000
- )
1001
- await dynamodb_storage.create_schedule(schedule)
1002
-
1003
- schedules = await dynamodb_storage.list_schedules()
1004
-
1005
- assert len(schedules) == 5
1006
-
1007
- @pytest.mark.asyncio
1008
- async def test_list_schedules_by_status(self, dynamodb_storage):
1009
- """Test listing schedules filtered by status."""
1010
- now = datetime.now(UTC)
1011
-
1012
- for i, status in enumerate(
1013
- [ScheduleStatus.ACTIVE, ScheduleStatus.ACTIVE, ScheduleStatus.PAUSED]
1014
- ):
1015
- schedule = Schedule(
1016
- schedule_id=f"status_sched_{i}",
1017
- workflow_name="test_workflow",
1018
- spec=ScheduleSpec(cron="0 9 * * *"),
1019
- status=status,
1020
- created_at=now + timedelta(seconds=i),
1021
- )
1022
- await dynamodb_storage.create_schedule(schedule)
1023
-
1024
- active = await dynamodb_storage.list_schedules(status=ScheduleStatus.ACTIVE)
1025
-
1026
- assert len(active) == 2
1027
-
1028
- @pytest.mark.asyncio
1029
- async def test_get_due_schedules(self, dynamodb_storage):
1030
- """Test getting due schedules."""
1031
- now = datetime.now(UTC)
1032
- past = now - timedelta(minutes=5)
1033
- future = now + timedelta(minutes=5)
1034
-
1035
- # Create schedules with different next_run_times
1036
- for i, (next_run, status) in enumerate(
1037
- [
1038
- (past, ScheduleStatus.ACTIVE),
1039
- (past, ScheduleStatus.ACTIVE),
1040
- (future, ScheduleStatus.ACTIVE),
1041
- (past, ScheduleStatus.PAUSED), # Paused should not be returned
1042
- ]
1043
- ):
1044
- schedule = Schedule(
1045
- schedule_id=f"due_sched_{i}",
1046
- workflow_name="test_workflow",
1047
- spec=ScheduleSpec(cron="0 9 * * *"),
1048
- status=status,
1049
- next_run_time=next_run,
1050
- created_at=now,
1051
- )
1052
- await dynamodb_storage.create_schedule(schedule)
1053
-
1054
- due = await dynamodb_storage.get_due_schedules(now)
1055
-
1056
- # Only 2 active schedules with past next_run_time
1057
- assert len(due) == 2
1058
-
1059
- @pytest.mark.asyncio
1060
- async def test_add_and_remove_running_run(self, dynamodb_storage):
1061
- """Test adding and removing running run IDs."""
1062
- now = datetime.now(UTC)
1063
- schedule = Schedule(
1064
- schedule_id="running_sched",
1065
- workflow_name="test_workflow",
1066
- spec=ScheduleSpec(cron="0 9 * * *"),
1067
- status=ScheduleStatus.ACTIVE,
1068
- created_at=now,
1069
- )
1070
- await dynamodb_storage.create_schedule(schedule)
1071
-
1072
- # Add running runs
1073
- await dynamodb_storage.add_running_run("running_sched", "run_1")
1074
- await dynamodb_storage.add_running_run("running_sched", "run_2")
1075
-
1076
- retrieved = await dynamodb_storage.get_schedule("running_sched")
1077
- assert "run_1" in retrieved.running_run_ids
1078
- assert "run_2" in retrieved.running_run_ids
1079
- assert len(retrieved.running_run_ids) == 2
1080
-
1081
- # Remove a run
1082
- await dynamodb_storage.remove_running_run("running_sched", "run_1")
1083
-
1084
- retrieved = await dynamodb_storage.get_schedule("running_sched")
1085
- assert "run_1" not in retrieved.running_run_ids
1086
- assert "run_2" in retrieved.running_run_ids
1087
- assert len(retrieved.running_run_ids) == 1
1088
-
1089
-
1090
- class TestLifecycleMethods:
1091
- """Test lifecycle methods (connect, disconnect, health_check)."""
1092
-
1093
- @pytest.mark.asyncio
1094
- async def test_disconnect_and_reconnect(self, dynamodb_storage):
1095
- """Test that disconnect works and reconnect is possible."""
1096
- # First verify we can create a run
1097
- now = datetime.now(UTC)
1098
- run = WorkflowRun(
1099
- run_id="lifecycle_test_run",
1100
- workflow_name="test_workflow",
1101
- status=RunStatus.PENDING,
1102
- created_at=now,
1103
- updated_at=now,
1104
- input_args="[]",
1105
- input_kwargs="{}",
1106
- )
1107
- await dynamodb_storage.create_run(run)
1108
-
1109
- # Verify it was created
1110
- retrieved = await dynamodb_storage.get_run("lifecycle_test_run")
1111
- assert retrieved is not None
1112
-
1113
- # Disconnect
1114
- await dynamodb_storage.disconnect()
1115
- assert dynamodb_storage._initialized is False
1116
-
1117
- # Reconnect
1118
- await dynamodb_storage.connect()
1119
- assert dynamodb_storage._initialized is True
1120
-
1121
- # Verify we can still perform operations after reconnecting
1122
- retrieved = await dynamodb_storage.get_run("lifecycle_test_run")
1123
- assert retrieved is not None
1124
- assert retrieved.run_id == "lifecycle_test_run"
1125
-
1126
- @pytest.mark.asyncio
1127
- async def test_health_check_returns_true(self, dynamodb_storage):
1128
- """Test health check returns True for healthy backend."""
1129
- result = await dynamodb_storage.health_check()
1130
- assert result is True
1131
-
1132
- @pytest.mark.asyncio
1133
- async def test_health_check_after_disconnect_returns_false(self, aws_credentials):
1134
- """Test health check returns False when disconnected."""
1135
- import uuid
1136
-
1137
- table_name = f"test_health_{uuid.uuid4().hex[:8]}"
1138
- backend = DynamoDBStorageBackend(
1139
- table_name=table_name,
1140
- region="us-east-1",
1141
- endpoint_url="http://localhost:8000",
1142
- )
1143
-
1144
- # Don't connect - should return False
1145
- result = await backend.health_check()
1146
- assert result is False