Files
attune/tests/e2e/tier2/test_t2_11_sequential_workflow.py
2026-02-04 17:46:30 -06:00

649 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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")