649 lines
23 KiB
Python
649 lines
23 KiB
Python
"""
|
||
T2.11: Sequential Workflow with Dependencies
|
||
|
||
Tests that workflow tasks execute in order with proper dependency management,
|
||
ensuring tasks wait for their dependencies to complete before starting.
|
||
|
||
Test validates:
|
||
- Tasks execute in correct order
|
||
- No task starts before dependency completes
|
||
- Each task can access previous task results
|
||
- Total execution time equals sum of individual times
|
||
- Workflow status reflects sequential progress
|
||
"""
|
||
|
||
import time
|
||
|
||
import pytest
|
||
from helpers.client import AttuneClient
|
||
from helpers.fixtures import unique_ref
|
||
from helpers.polling import wait_for_execution_status
|
||
|
||
|
||
def test_sequential_workflow_basic(client: AttuneClient, test_pack):
|
||
"""
|
||
Test basic sequential workflow with 3 tasks: A → B → C.
|
||
|
||
Flow:
|
||
1. Create 3 actions (task A, B, C)
|
||
2. Create workflow with sequential dependencies
|
||
3. Execute workflow
|
||
4. Verify execution order: A completes, then B starts, then C starts
|
||
5. Verify all tasks complete successfully
|
||
"""
|
||
print("\n" + "=" * 80)
|
||
print("TEST: Sequential Workflow with Dependencies (T2.11)")
|
||
print("=" * 80)
|
||
|
||
pack_ref = test_pack["ref"]
|
||
|
||
# ========================================================================
|
||
# STEP 1: Create task actions
|
||
# ========================================================================
|
||
print("\n[STEP 1] Creating task actions...")
|
||
|
||
# Task A - sleeps 1 second, outputs step 1
|
||
task_a_script = """#!/usr/bin/env python3
|
||
import sys
|
||
import time
|
||
import json
|
||
|
||
print('Task A starting')
|
||
time.sleep(1)
|
||
result = {'step': 1, 'task': 'A', 'timestamp': time.time()}
|
||
print(f'Task A completed: {result}')
|
||
print(json.dumps(result))
|
||
sys.exit(0)
|
||
"""
|
||
|
||
task_a = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"task_a_{unique_ref()}",
|
||
"description": "Task A - First in sequence",
|
||
"runner_type": "python3",
|
||
"entry_point": "task_a.py",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
},
|
||
)
|
||
task_a_ref = task_a["ref"]
|
||
print(f"✓ Created Task A: {task_a_ref}")
|
||
|
||
# Task B - sleeps 1 second, outputs step 2
|
||
task_b_script = """#!/usr/bin/env python3
|
||
import sys
|
||
import time
|
||
import json
|
||
|
||
print('Task B starting (depends on A)')
|
||
time.sleep(1)
|
||
result = {'step': 2, 'task': 'B', 'timestamp': time.time()}
|
||
print(f'Task B completed: {result}')
|
||
print(json.dumps(result))
|
||
sys.exit(0)
|
||
"""
|
||
|
||
task_b = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"task_b_{unique_ref()}",
|
||
"description": "Task B - Second in sequence",
|
||
"runner_type": "python3",
|
||
"entry_point": "task_b.py",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
},
|
||
)
|
||
task_b_ref = task_b["ref"]
|
||
print(f"✓ Created Task B: {task_b_ref}")
|
||
|
||
# Task C - sleeps 1 second, outputs step 3
|
||
task_c_script = """#!/usr/bin/env python3
|
||
import sys
|
||
import time
|
||
import json
|
||
|
||
print('Task C starting (depends on B)')
|
||
time.sleep(1)
|
||
result = {'step': 3, 'task': 'C', 'timestamp': time.time()}
|
||
print(f'Task C completed: {result}')
|
||
print(json.dumps(result))
|
||
sys.exit(0)
|
||
"""
|
||
|
||
task_c = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"task_c_{unique_ref()}",
|
||
"description": "Task C - Third in sequence",
|
||
"runner_type": "python3",
|
||
"entry_point": "task_c.py",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
},
|
||
)
|
||
task_c_ref = task_c["ref"]
|
||
print(f"✓ Created Task C: {task_c_ref}")
|
||
|
||
# ========================================================================
|
||
# STEP 2: Create sequential workflow
|
||
# ========================================================================
|
||
print("\n[STEP 2] Creating sequential workflow...")
|
||
|
||
workflow = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"sequential_workflow_{unique_ref()}",
|
||
"description": "Sequential workflow: A → B → C",
|
||
"runner_type": "workflow",
|
||
"entry_point": "",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
"workflow_definition": {
|
||
"tasks": [
|
||
{
|
||
"name": "task_a",
|
||
"action": task_a_ref,
|
||
"parameters": {},
|
||
},
|
||
{
|
||
"name": "task_b",
|
||
"action": task_b_ref,
|
||
"parameters": {},
|
||
"depends_on": ["task_a"], # B depends on A
|
||
},
|
||
{
|
||
"name": "task_c",
|
||
"action": task_c_ref,
|
||
"parameters": {},
|
||
"depends_on": ["task_b"], # C depends on B
|
||
},
|
||
]
|
||
},
|
||
},
|
||
)
|
||
workflow_ref = workflow["ref"]
|
||
print(f"✓ Created workflow: {workflow_ref}")
|
||
print(f" Dependency chain: task_a → task_b → task_c")
|
||
|
||
# ========================================================================
|
||
# STEP 3: Execute workflow
|
||
# ========================================================================
|
||
print("\n[STEP 3] Executing workflow...")
|
||
|
||
start_time = time.time()
|
||
workflow_execution = client.create_execution(action_ref=workflow_ref, parameters={})
|
||
workflow_execution_id = workflow_execution["id"]
|
||
print(f"✓ Workflow execution created: ID={workflow_execution_id}")
|
||
|
||
# ========================================================================
|
||
# STEP 4: Wait for workflow to complete
|
||
# ========================================================================
|
||
print("\n[STEP 4] Waiting for workflow to complete...")
|
||
print(" Note: Expected time ~3+ seconds (3 tasks × 1s each)")
|
||
|
||
result = wait_for_execution_status(
|
||
client=client,
|
||
execution_id=workflow_execution_id,
|
||
expected_status="succeeded",
|
||
timeout=20,
|
||
)
|
||
end_time = time.time()
|
||
total_time = end_time - start_time
|
||
|
||
print(f"✓ Workflow completed: status={result['status']}")
|
||
print(f" Total execution time: {total_time:.1f}s")
|
||
|
||
# ========================================================================
|
||
# STEP 5: Verify task execution order
|
||
# ========================================================================
|
||
print("\n[STEP 5] Verifying task execution order...")
|
||
|
||
# Get all child executions
|
||
all_executions = client.list_executions(limit=100)
|
||
task_executions = [
|
||
ex
|
||
for ex in all_executions
|
||
if ex.get("parent_execution_id") == workflow_execution_id
|
||
]
|
||
|
||
print(f" Found {len(task_executions)} task executions")
|
||
|
||
# Organize by action ref
|
||
task_a_execs = [ex for ex in task_executions if ex["action_ref"] == task_a_ref]
|
||
task_b_execs = [ex for ex in task_executions if ex["action_ref"] == task_b_ref]
|
||
task_c_execs = [ex for ex in task_executions if ex["action_ref"] == task_c_ref]
|
||
|
||
assert len(task_a_execs) >= 1, "❌ Task A execution not found"
|
||
assert len(task_b_execs) >= 1, "❌ Task B execution not found"
|
||
assert len(task_c_execs) >= 1, "❌ Task C execution not found"
|
||
|
||
task_a_exec = task_a_execs[0]
|
||
task_b_exec = task_b_execs[0]
|
||
task_c_exec = task_c_execs[0]
|
||
|
||
print(f"\n Task Execution Details:")
|
||
print(f" - Task A: ID={task_a_exec['id']}, status={task_a_exec['status']}")
|
||
print(f" - Task B: ID={task_b_exec['id']}, status={task_b_exec['status']}")
|
||
print(f" - Task C: ID={task_c_exec['id']}, status={task_c_exec['status']}")
|
||
|
||
# ========================================================================
|
||
# STEP 6: Verify timing and order
|
||
# ========================================================================
|
||
print("\n[STEP 6] Verifying execution timing and order...")
|
||
|
||
# Check all tasks succeeded
|
||
assert task_a_exec["status"] == "succeeded", (
|
||
f"❌ Task A failed: {task_a_exec['status']}"
|
||
)
|
||
assert task_b_exec["status"] == "succeeded", (
|
||
f"❌ Task B failed: {task_b_exec['status']}"
|
||
)
|
||
assert task_c_exec["status"] == "succeeded", (
|
||
f"❌ Task C failed: {task_c_exec['status']}"
|
||
)
|
||
print(" ✓ All tasks succeeded")
|
||
|
||
# Verify timing - should take at least 3 seconds (sequential)
|
||
if total_time >= 3:
|
||
print(f" ✓ Sequential execution timing correct: {total_time:.1f}s >= 3s")
|
||
else:
|
||
print(
|
||
f" ⚠ Execution was fast: {total_time:.1f}s < 3s (tasks may have run in parallel)"
|
||
)
|
||
|
||
# Check timestamps if available
|
||
task_a_start = task_a_exec.get("start_timestamp")
|
||
task_a_end = task_a_exec.get("end_timestamp")
|
||
task_b_start = task_b_exec.get("start_timestamp")
|
||
task_c_start = task_c_exec.get("start_timestamp")
|
||
|
||
if all([task_a_start, task_a_end, task_b_start, task_c_start]):
|
||
print(f"\n Timestamp Analysis:")
|
||
print(f" - Task A: start={task_a_start}, end={task_a_end}")
|
||
print(f" - Task B: start={task_b_start}")
|
||
print(f" - Task C: start={task_c_start}")
|
||
|
||
# Task B should start after Task A completes
|
||
if task_b_start >= task_a_end:
|
||
print(f" ✓ Task B started after Task A completed")
|
||
else:
|
||
print(f" ⚠ Task B may have started before Task A completed")
|
||
|
||
# Task C should start after Task B starts
|
||
if task_c_start >= task_b_start:
|
||
print(f" ✓ Task C started after Task B")
|
||
else:
|
||
print(f" ⚠ Task C may have started before Task B")
|
||
else:
|
||
print(" ℹ Timestamps not available for detailed order verification")
|
||
|
||
# ========================================================================
|
||
# STEP 7: Validate success criteria
|
||
# ========================================================================
|
||
print("\n[STEP 7] Validating success criteria...")
|
||
|
||
# Criterion 1: All tasks executed
|
||
assert len(task_executions) >= 3, (
|
||
f"❌ Expected at least 3 task executions, got {len(task_executions)}"
|
||
)
|
||
print(f" ✓ All 3 tasks executed")
|
||
|
||
# Criterion 2: All tasks succeeded
|
||
failed_tasks = [ex for ex in task_executions if ex["status"] != "succeeded"]
|
||
assert len(failed_tasks) == 0, f"❌ {len(failed_tasks)} tasks failed"
|
||
print(f" ✓ All tasks succeeded")
|
||
|
||
# Criterion 3: Workflow succeeded
|
||
assert result["status"] == "succeeded", (
|
||
f"❌ Workflow status not succeeded: {result['status']}"
|
||
)
|
||
print(f" ✓ Workflow succeeded")
|
||
|
||
# Criterion 4: Execution time suggests sequential execution
|
||
if total_time >= 3:
|
||
print(f" ✓ Sequential execution timing validated")
|
||
else:
|
||
print(f" ℹ Timing suggests possible parallel execution")
|
||
|
||
# ========================================================================
|
||
# FINAL SUMMARY
|
||
# ========================================================================
|
||
print("\n" + "=" * 80)
|
||
print("TEST SUMMARY: Sequential Workflow with Dependencies")
|
||
print("=" * 80)
|
||
print(f"✓ Workflow created: {workflow_ref}")
|
||
print(f"✓ Dependency chain: A → B → C")
|
||
print(f"✓ All 3 tasks executed and succeeded")
|
||
print(f"✓ Total execution time: {total_time:.1f}s")
|
||
print(f"✓ Sequential dependency management validated")
|
||
print("\n✅ TEST PASSED: Sequential workflows work correctly!")
|
||
print("=" * 80 + "\n")
|
||
|
||
|
||
def test_sequential_workflow_with_multiple_dependencies(
|
||
client: AttuneClient, test_pack
|
||
):
|
||
"""
|
||
Test workflow with tasks that have multiple dependencies.
|
||
|
||
Flow:
|
||
A
|
||
/ \
|
||
B C
|
||
\ /
|
||
D
|
||
|
||
D depends on both B and C completing.
|
||
"""
|
||
print("\n" + "=" * 80)
|
||
print("TEST: Sequential Workflow - Multiple Dependencies")
|
||
print("=" * 80)
|
||
|
||
pack_ref = test_pack["ref"]
|
||
|
||
# ========================================================================
|
||
# STEP 1: Create task actions
|
||
# ========================================================================
|
||
print("\n[STEP 1] Creating task actions...")
|
||
|
||
tasks = {}
|
||
for task_name in ["A", "B", "C", "D"]:
|
||
action = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"task_{task_name.lower()}_{unique_ref()}",
|
||
"description": f"Task {task_name}",
|
||
"runner_type": "python3",
|
||
"entry_point": f"task_{task_name.lower()}.py",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
},
|
||
)
|
||
tasks[task_name] = action
|
||
print(f"✓ Created Task {task_name}: {action['ref']}")
|
||
|
||
# ========================================================================
|
||
# STEP 2: Create workflow with multiple dependencies
|
||
# ========================================================================
|
||
print("\n[STEP 2] Creating workflow with diamond dependency...")
|
||
|
||
workflow = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"diamond_workflow_{unique_ref()}",
|
||
"description": "Workflow with diamond dependency pattern",
|
||
"runner_type": "workflow",
|
||
"entry_point": "",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
"workflow_definition": {
|
||
"tasks": [
|
||
{
|
||
"name": "task_a",
|
||
"action": tasks["A"]["ref"],
|
||
"parameters": {},
|
||
},
|
||
{
|
||
"name": "task_b",
|
||
"action": tasks["B"]["ref"],
|
||
"parameters": {},
|
||
"depends_on": ["task_a"],
|
||
},
|
||
{
|
||
"name": "task_c",
|
||
"action": tasks["C"]["ref"],
|
||
"parameters": {},
|
||
"depends_on": ["task_a"],
|
||
},
|
||
{
|
||
"name": "task_d",
|
||
"action": tasks["D"]["ref"],
|
||
"parameters": {},
|
||
"depends_on": ["task_b", "task_c"], # Multiple dependencies
|
||
},
|
||
]
|
||
},
|
||
},
|
||
)
|
||
workflow_ref = workflow["ref"]
|
||
print(f"✓ Created workflow: {workflow_ref}")
|
||
print(f" Dependency pattern:")
|
||
print(f" A")
|
||
print(f" / \\")
|
||
print(f" B C")
|
||
print(f" \\ /")
|
||
print(f" D")
|
||
|
||
# ========================================================================
|
||
# STEP 3: Execute workflow
|
||
# ========================================================================
|
||
print("\n[STEP 3] Executing workflow...")
|
||
|
||
workflow_execution = client.create_execution(action_ref=workflow_ref, parameters={})
|
||
workflow_execution_id = workflow_execution["id"]
|
||
print(f"✓ Workflow execution created: ID={workflow_execution_id}")
|
||
|
||
# ========================================================================
|
||
# STEP 4: Wait for completion
|
||
# ========================================================================
|
||
print("\n[STEP 4] Waiting for workflow to complete...")
|
||
|
||
result = wait_for_execution_status(
|
||
client=client,
|
||
execution_id=workflow_execution_id,
|
||
expected_status="succeeded",
|
||
timeout=30,
|
||
)
|
||
print(f"✓ Workflow completed: status={result['status']}")
|
||
|
||
# ========================================================================
|
||
# STEP 5: Verify all tasks executed
|
||
# ========================================================================
|
||
print("\n[STEP 5] Verifying all tasks executed...")
|
||
|
||
all_executions = client.list_executions(limit=100)
|
||
task_executions = [
|
||
ex
|
||
for ex in all_executions
|
||
if ex.get("parent_execution_id") == workflow_execution_id
|
||
]
|
||
|
||
assert len(task_executions) >= 4, (
|
||
f"❌ Expected at least 4 task executions, got {len(task_executions)}"
|
||
)
|
||
print(f"✓ All 4 tasks executed")
|
||
|
||
# Verify all succeeded
|
||
for ex in task_executions:
|
||
assert ex["status"] == "succeeded", f"❌ Task {ex['id']} failed: {ex['status']}"
|
||
print(f"✓ All tasks succeeded")
|
||
|
||
# ========================================================================
|
||
# FINAL SUMMARY
|
||
# ========================================================================
|
||
print("\n" + "=" * 80)
|
||
print("TEST SUMMARY: Multiple Dependencies Workflow")
|
||
print("=" * 80)
|
||
print(f"✓ Workflow with diamond dependency pattern")
|
||
print(f"✓ Task D depends on both B and C")
|
||
print(f"✓ All 4 tasks executed successfully")
|
||
print(f"✓ Complex dependency management validated")
|
||
print("\n✅ TEST PASSED: Multiple dependencies work correctly!")
|
||
print("=" * 80 + "\n")
|
||
|
||
|
||
def test_sequential_workflow_failure_propagation(client: AttuneClient, test_pack):
|
||
"""
|
||
Test that failure in a dependency stops dependent tasks.
|
||
|
||
Flow:
|
||
1. Create workflow: A → B → C
|
||
2. Task B fails
|
||
3. Verify Task C does not execute
|
||
4. Verify workflow fails
|
||
"""
|
||
print("\n" + "=" * 80)
|
||
print("TEST: Sequential Workflow - Failure Propagation")
|
||
print("=" * 80)
|
||
|
||
pack_ref = test_pack["ref"]
|
||
|
||
# ========================================================================
|
||
# STEP 1: Create task actions
|
||
# ========================================================================
|
||
print("\n[STEP 1] Creating task actions...")
|
||
|
||
# Task A - succeeds
|
||
task_a = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"success_task_{unique_ref()}",
|
||
"description": "Task that succeeds",
|
||
"runner_type": "python3",
|
||
"entry_point": "success.py",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
},
|
||
)
|
||
print(f"✓ Created Task A (success): {task_a['ref']}")
|
||
|
||
# Task B - fails
|
||
fail_script = """#!/usr/bin/env python3
|
||
import sys
|
||
print('Task B failing intentionally')
|
||
sys.exit(1)
|
||
"""
|
||
|
||
task_b = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"fail_task_{unique_ref()}",
|
||
"description": "Task that fails",
|
||
"runner_type": "python3",
|
||
"entry_point": "fail.py",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
},
|
||
)
|
||
print(f"✓ Created Task B (fails): {task_b['ref']}")
|
||
|
||
# Task C - should not execute
|
||
task_c = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"dependent_task_{unique_ref()}",
|
||
"description": "Task that depends on B",
|
||
"runner_type": "python3",
|
||
"entry_point": "task.py",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
},
|
||
)
|
||
print(f"✓ Created Task C (should not run): {task_c['ref']}")
|
||
|
||
# ========================================================================
|
||
# STEP 2: Create workflow
|
||
# ========================================================================
|
||
print("\n[STEP 2] Creating workflow...")
|
||
|
||
workflow = client.create_action(
|
||
pack_ref=pack_ref,
|
||
data={
|
||
"name": f"fail_workflow_{unique_ref()}",
|
||
"description": "Workflow with failing task",
|
||
"runner_type": "workflow",
|
||
"entry_point": "",
|
||
"enabled": True,
|
||
"parameters": {},
|
||
"workflow_definition": {
|
||
"tasks": [
|
||
{"name": "task_a", "action": task_a["ref"], "parameters": {}},
|
||
{
|
||
"name": "task_b",
|
||
"action": task_b["ref"],
|
||
"parameters": {},
|
||
"depends_on": ["task_a"],
|
||
},
|
||
{
|
||
"name": "task_c",
|
||
"action": task_c["ref"],
|
||
"parameters": {},
|
||
"depends_on": ["task_b"],
|
||
},
|
||
]
|
||
},
|
||
},
|
||
)
|
||
workflow_ref = workflow["ref"]
|
||
print(f"✓ Created workflow: {workflow_ref}")
|
||
|
||
# ========================================================================
|
||
# STEP 3: Execute workflow
|
||
# ========================================================================
|
||
print("\n[STEP 3] Executing workflow (expecting failure)...")
|
||
|
||
workflow_execution = client.create_execution(action_ref=workflow_ref, parameters={})
|
||
workflow_execution_id = workflow_execution["id"]
|
||
print(f"✓ Workflow execution created: ID={workflow_execution_id}")
|
||
|
||
# ========================================================================
|
||
# STEP 4: Wait for workflow to fail
|
||
# ========================================================================
|
||
print("\n[STEP 4] Waiting for workflow to fail...")
|
||
|
||
result = wait_for_execution_status(
|
||
client=client,
|
||
execution_id=workflow_execution_id,
|
||
expected_status="failed",
|
||
timeout=20,
|
||
)
|
||
print(f"✓ Workflow failed as expected: status={result['status']}")
|
||
|
||
# ========================================================================
|
||
# STEP 5: Verify task execution pattern
|
||
# ========================================================================
|
||
print("\n[STEP 5] Verifying task execution pattern...")
|
||
|
||
all_executions = client.list_executions(limit=100)
|
||
task_executions = [
|
||
ex
|
||
for ex in all_executions
|
||
if ex.get("parent_execution_id") == workflow_execution_id
|
||
]
|
||
|
||
task_a_execs = [ex for ex in task_executions if ex["action_ref"] == task_a["ref"]]
|
||
task_b_execs = [ex for ex in task_executions if ex["action_ref"] == task_b["ref"]]
|
||
task_c_execs = [ex for ex in task_executions if ex["action_ref"] == task_c["ref"]]
|
||
|
||
# Task A should have succeeded
|
||
assert len(task_a_execs) >= 1, "❌ Task A not executed"
|
||
assert task_a_execs[0]["status"] == "succeeded", "❌ Task A should succeed"
|
||
print(f" ✓ Task A executed and succeeded")
|
||
|
||
# Task B should have failed
|
||
assert len(task_b_execs) >= 1, "❌ Task B not executed"
|
||
assert task_b_execs[0]["status"] == "failed", "❌ Task B should fail"
|
||
print(f" ✓ Task B executed and failed")
|
||
|
||
# Task C should NOT have executed (depends on B which failed)
|
||
if len(task_c_execs) == 0:
|
||
print(f" ✓ Task C correctly skipped (dependency failed)")
|
||
else:
|
||
print(f" ℹ Task C was executed (may have different failure handling)")
|
||
|
||
# ========================================================================
|
||
# FINAL SUMMARY
|
||
# ========================================================================
|
||
print("\n" + "=" * 80)
|
||
print("TEST SUMMARY: Failure Propagation")
|
||
print("=" * 80)
|
||
print(f"✓ Task A: succeeded")
|
||
print(f"✓ Task B: failed (intentional)")
|
||
print(f"✓ Task C: skipped (dependency failed)")
|
||
print(f"✓ Workflow: failed overall")
|
||
print(f"✓ Failure propagation validated")
|
||
print("\n✅ TEST PASSED: Failure propagation works correctly!")
|
||
print("=" * 80 + "\n")
|