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

375 lines
13 KiB
Python

"""
T3.14: Execution Completion Notifications Test
Tests that the notifier service sends real-time notifications when executions complete.
Validates WebSocket delivery of execution status updates.
Priority: MEDIUM
Duration: ~20 seconds
"""
import json
import time
from typing import Any, Dict
import pytest
from helpers.client import AttuneClient
from helpers.fixtures import create_echo_action, create_webhook_trigger, unique_ref
from helpers.polling import (
wait_for_execution_completion,
wait_for_execution_count,
)
@pytest.mark.tier3
@pytest.mark.notifications
@pytest.mark.websocket
def test_execution_success_notification(client: AttuneClient, test_pack):
"""
Test that successful execution completion triggers notification.
Flow:
1. Create webhook trigger and echo action
2. Create rule linking webhook to action
3. Subscribe to WebSocket notifications
4. Trigger webhook
5. Verify notification received for execution completion
6. Validate notification payload structure
"""
print("\n" + "=" * 80)
print("T3.14.1: Execution Success Notification")
print("=" * 80)
pack_ref = test_pack["ref"]
# Step 1: Create webhook trigger
print("\n[STEP 1] Creating webhook trigger...")
trigger_ref = f"notify_webhook_{unique_ref()}"
trigger = create_webhook_trigger(
client=client,
pack_ref=pack_ref,
trigger_ref=trigger_ref,
description="Webhook for notification test",
)
print(f"✓ Created trigger: {trigger['ref']}")
# Step 2: Create echo action
print("\n[STEP 2] Creating echo action...")
action_ref = f"notify_action_{unique_ref()}"
action = create_echo_action(
client=client,
pack_ref=pack_ref,
action_ref=action_ref,
description="Action for notification test",
)
print(f"✓ Created action: {action['ref']}")
# Step 3: Create rule
print("\n[STEP 3] Creating rule...")
rule_ref = f"notify_rule_{unique_ref()}"
rule_payload = {
"ref": rule_ref,
"pack": pack_ref,
"trigger": trigger["ref"],
"action": action["ref"],
"enabled": True,
}
rule_response = client.post("/rules", json=rule_payload)
assert rule_response.status_code == 201, (
f"Failed to create rule: {rule_response.text}"
)
rule = rule_response.json()["data"]
print(f"✓ Created rule: {rule['ref']}")
# Note: WebSocket notifications require the notifier service to be running.
# For now, we'll validate the execution completes and check that notification
# metadata is properly stored in the database.
# Step 4: Trigger webhook
print("\n[STEP 4] Triggering webhook...")
webhook_url = f"/webhooks/{trigger['ref']}"
test_payload = {"message": "test notification", "timestamp": time.time()}
webhook_response = client.post(webhook_url, json=test_payload)
assert webhook_response.status_code == 200, (
f"Webhook trigger failed: {webhook_response.text}"
)
print(f"✓ Webhook triggered successfully")
# Step 5: Wait for execution completion
print("\n[STEP 5] Waiting for execution to complete...")
wait_for_execution_count(client, expected_count=1, timeout=10)
executions = client.get("/executions").json()["data"]
execution_id = executions[0]["id"]
execution = wait_for_execution_completion(client, execution_id, timeout=10)
print(f"✓ Execution completed with status: {execution['status']}")
assert execution["status"] == "succeeded", (
f"Expected succeeded, got {execution['status']}"
)
# Step 6: Validate notification metadata
print("\n[STEP 6] Validating notification metadata...")
# Check that the execution has notification fields set
assert "created" in execution, "Execution missing created timestamp"
assert "updated" in execution, "Execution missing updated timestamp"
# The notifier service would have sent a notification at this point
# In a full integration test with WebSocket, we would verify the message here
print(f"✓ Execution metadata validated for notifications")
print(f" - Execution ID: {execution_id}")
print(f" - Status: {execution['status']}")
print(f" - Created: {execution['created']}")
print(f" - Updated: {execution['updated']}")
print("\n✅ Test passed: Execution completion notification flow validated")
@pytest.mark.tier3
@pytest.mark.notifications
@pytest.mark.websocket
def test_execution_failure_notification(client: AttuneClient, test_pack):
"""
Test that failed execution triggers notification.
Flow:
1. Create webhook trigger and failing action
2. Create rule
3. Trigger webhook
4. Verify notification for failed execution
"""
print("\n" + "=" * 80)
print("T3.14.2: Execution Failure Notification")
print("=" * 80)
pack_ref = test_pack["ref"]
# Step 1: Create webhook trigger
print("\n[STEP 1] Creating webhook trigger...")
trigger_ref = f"fail_notify_webhook_{unique_ref()}"
trigger = create_webhook_trigger(
client=client,
pack_ref=pack_ref,
trigger_ref=trigger_ref,
description="Webhook for failure notification test",
)
print(f"✓ Created trigger: {trigger['ref']}")
# Step 2: Create failing action (Python runner with error)
print("\n[STEP 2] Creating failing action...")
action_ref = f"fail_notify_action_{unique_ref()}"
action_payload = {
"ref": action_ref,
"pack": pack_ref,
"name": "Failing Action for Notification",
"description": "Action that fails to test notifications",
"runner_type": "python",
"entry_point": "raise Exception('Intentional failure for notification test')",
"enabled": True,
}
action_response = client.post("/actions", json=action_payload)
assert action_response.status_code == 201, (
f"Failed to create action: {action_response.text}"
)
action = action_response.json()["data"]
print(f"✓ Created action: {action['ref']}")
# Step 3: Create rule
print("\n[STEP 3] Creating rule...")
rule_ref = f"fail_notify_rule_{unique_ref()}"
rule_payload = {
"ref": rule_ref,
"pack": pack_ref,
"trigger": trigger["ref"],
"action": action["ref"],
"enabled": True,
}
rule_response = client.post("/rules", json=rule_payload)
assert rule_response.status_code == 201, (
f"Failed to create rule: {rule_response.text}"
)
rule = rule_response.json()["data"]
print(f"✓ Created rule: {rule['ref']}")
# Step 4: Trigger webhook
print("\n[STEP 4] Triggering webhook...")
webhook_url = f"/webhooks/{trigger['ref']}"
test_payload = {"message": "trigger failure", "timestamp": time.time()}
webhook_response = client.post(webhook_url, json=test_payload)
assert webhook_response.status_code == 200, (
f"Webhook trigger failed: {webhook_response.text}"
)
print(f"✓ Webhook triggered successfully")
# Step 5: Wait for execution to fail
print("\n[STEP 5] Waiting for execution to fail...")
wait_for_execution_count(client, expected_count=1, timeout=10)
executions = client.get("/executions").json()["data"]
execution_id = executions[0]["id"]
execution = wait_for_execution_completion(client, execution_id, timeout=10)
print(f"✓ Execution completed with status: {execution['status']}")
assert execution["status"] == "failed", (
f"Expected failed, got {execution['status']}"
)
# Step 6: Validate notification metadata for failure
print("\n[STEP 6] Validating failure notification metadata...")
assert "created" in execution, "Execution missing created timestamp"
assert "updated" in execution, "Execution missing updated timestamp"
assert execution["result"] is not None, (
"Failed execution should have result with error"
)
print(f"✓ Failure notification metadata validated")
print(f" - Execution ID: {execution_id}")
print(f" - Status: {execution['status']}")
print(f" - Result available: {execution['result'] is not None}")
print("\n✅ Test passed: Execution failure notification flow validated")
@pytest.mark.tier3
@pytest.mark.notifications
@pytest.mark.websocket
def test_execution_timeout_notification(client: AttuneClient, test_pack):
"""
Test that execution timeout triggers notification.
Flow:
1. Create webhook trigger and long-running action with short timeout
2. Create rule
3. Trigger webhook
4. Verify notification for timed-out execution
"""
print("\n" + "=" * 80)
print("T3.14.3: Execution Timeout Notification")
print("=" * 80)
pack_ref = test_pack["ref"]
# Step 1: Create webhook trigger
print("\n[STEP 1] Creating webhook trigger...")
trigger_ref = f"timeout_notify_webhook_{unique_ref()}"
trigger = create_webhook_trigger(
client=client,
pack_ref=pack_ref,
trigger_ref=trigger_ref,
description="Webhook for timeout notification test",
)
print(f"✓ Created trigger: {trigger['ref']}")
# Step 2: Create long-running action with short timeout
print("\n[STEP 2] Creating long-running action with timeout...")
action_ref = f"timeout_notify_action_{unique_ref()}"
action_payload = {
"ref": action_ref,
"pack": pack_ref,
"name": "Timeout Action for Notification",
"description": "Action that times out",
"runner_type": "python",
"entry_point": "import time; time.sleep(30)", # Sleep longer than timeout
"timeout": 2, # 2 second timeout
"enabled": True,
}
action_response = client.post("/actions", json=action_payload)
assert action_response.status_code == 201, (
f"Failed to create action: {action_response.text}"
)
action = action_response.json()["data"]
print(f"✓ Created action with 2s timeout: {action['ref']}")
# Step 3: Create rule
print("\n[STEP 3] Creating rule...")
rule_ref = f"timeout_notify_rule_{unique_ref()}"
rule_payload = {
"ref": rule_ref,
"pack": pack_ref,
"trigger": trigger["ref"],
"action": action["ref"],
"enabled": True,
}
rule_response = client.post("/rules", json=rule_payload)
assert rule_response.status_code == 201, (
f"Failed to create rule: {rule_response.text}"
)
rule = rule_response.json()["data"]
print(f"✓ Created rule: {rule['ref']}")
# Step 4: Trigger webhook
print("\n[STEP 4] Triggering webhook...")
webhook_url = f"/webhooks/{trigger['ref']}"
test_payload = {"message": "trigger timeout", "timestamp": time.time()}
webhook_response = client.post(webhook_url, json=test_payload)
assert webhook_response.status_code == 200, (
f"Webhook trigger failed: {webhook_response.text}"
)
print(f"✓ Webhook triggered successfully")
# Step 5: Wait for execution to timeout
print("\n[STEP 5] Waiting for execution to timeout...")
wait_for_execution_count(client, expected_count=1, timeout=10)
executions = client.get("/executions").json()["data"]
execution_id = executions[0]["id"]
# Wait a bit longer for timeout to occur
time.sleep(5)
execution = client.get(f"/executions/{execution_id}").json()["data"]
print(f"✓ Execution status: {execution['status']}")
# Timeout might result in 'failed' or 'timeout' status depending on implementation
assert execution["status"] in ["failed", "timeout", "cancelled"], (
f"Expected timeout-related status, got {execution['status']}"
)
# Step 6: Validate timeout notification metadata
print("\n[STEP 6] Validating timeout notification metadata...")
assert "created" in execution, "Execution missing created timestamp"
assert "updated" in execution, "Execution missing updated timestamp"
print(f"✓ Timeout notification metadata validated")
print(f" - Execution ID: {execution_id}")
print(f" - Status: {execution['status']}")
print(f" - Action timeout: {action['timeout']}s")
print("\n✅ Test passed: Execution timeout notification flow validated")
@pytest.mark.tier3
@pytest.mark.notifications
@pytest.mark.websocket
@pytest.mark.skip(
reason="Requires WebSocket infrastructure not yet implemented in test suite"
)
def test_websocket_notification_delivery(client: AttuneClient, test_pack):
"""
Test actual WebSocket notification delivery (requires WebSocket client).
This test is skipped until WebSocket test infrastructure is implemented.
Flow:
1. Connect to WebSocket endpoint with auth token
2. Subscribe to execution notifications
3. Trigger workflow
4. Receive real-time notifications via WebSocket
5. Validate message format and timing
"""
print("\n" + "=" * 80)
print("T3.14.4: WebSocket Notification Delivery")
print("=" * 80)
# This would require:
# - WebSocket client library (websockets or similar)
# - Connection to notifier service WebSocket endpoint
# - Message subscription and parsing
# - Real-time notification validation
# Example pseudo-code:
# async with websockets.connect(f"ws://{host}/ws/notifications") as ws:
# await ws.send(json.dumps({"auth": token, "subscribe": ["executions"]}))
# # Trigger execution
# message = await ws.recv()
# notification = json.loads(message)
# assert notification["type"] == "execution.completed"
pytest.skip("WebSocket client infrastructure not yet implemented")