re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

796
tests/helpers/fixtures.py Normal file
View File

@@ -0,0 +1,796 @@
"""
Fixture Helpers for E2E Tests
Provides helper functions for creating test resources like packs,
triggers, actions, rules, etc.
"""
import time
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, Optional
from .client import AttuneClient
def unique_ref(prefix: str = "test") -> str:
"""
Generate unique reference string
Args:
prefix: Prefix for reference
Returns:
Unique reference string (e.g., "test_abc123")
"""
timestamp = int(time.time() * 1000)
random_part = str(uuid.uuid4())[:8]
return f"{prefix}_{timestamp}_{random_part}"
def timestamp_now() -> str:
"""Get current timestamp in ISO format"""
return datetime.utcnow().isoformat() + "Z"
def timestamp_future(seconds: int) -> str:
"""
Get future timestamp in ISO format
Args:
seconds: Seconds in the future
Returns:
ISO timestamp string
"""
future = datetime.utcnow() + timedelta(seconds=seconds)
return future.isoformat() + "Z"
# ============================================================================
# Pack Creation
# ============================================================================
def create_test_pack(
client: AttuneClient,
pack_ref: Optional[str] = None,
pack_dir: str = "tests/fixtures/packs/test_pack",
) -> Dict[str, Any]:
"""
Create or get test pack
Args:
client: AttuneClient instance
pack_ref: Optional pack reference (generated if not provided)
pack_dir: Path to pack directory
Returns:
Pack data
"""
# Extract pack_ref from pack_dir if not provided
if not pack_ref:
# Default pack ref is "test_pack" for the standard test pack
pack_ref = "test_pack"
# Always try to get existing pack first
existing_pack = client.get_pack_by_ref(pack_ref)
if existing_pack:
return existing_pack
# Register new pack if it doesn't exist
return client.register_pack(pack_dir, force=True)
def ensure_core_pack(client: AttuneClient) -> Dict[str, Any]:
"""
Ensure core pack exists, register it if needed
Args:
client: AttuneClient instance
Returns:
Core pack data
"""
# Try to get existing core pack
try:
core_pack = client.get_pack_by_ref("core")
if core_pack:
return core_pack
except Exception:
pass
# Core pack doesn't exist, register it
try:
return client.register_pack("packs/core", force=True, skip_tests=True)
except Exception as e:
# If registration fails, try without skip_tests
try:
return client.register_pack("packs/core", force=True)
except Exception as inner_e:
raise Exception(f"Failed to register core pack: {inner_e}") from e
# ============================================================================
# Trigger Creation
# ============================================================================
def create_interval_timer(
client: AttuneClient,
interval_seconds: int = 5,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
) -> Dict[str, Any]:
"""
Create interval timer sensor for timer to actually fire
Args:
client: AttuneClient instance
interval_seconds: Interval in seconds
name: Sensor name (generated if not provided)
pack_ref: Pack reference
Returns:
Dict with trigger and sensor info
"""
sensor_name = name or f"interval_{interval_seconds}s_{unique_ref()}"
# Ensure core pack exists
ensure_core_pack(client)
# Get or ensure core.intervaltimer trigger exists
triggers = client.list_triggers()
core_trigger = None
for t in triggers:
if t.get("ref") == "core.intervaltimer":
core_trigger = t
break
if not core_trigger:
# Create core.intervaltimer trigger if it doesn't exist
core_trigger = client.create_trigger(
ref="core.intervaltimer",
label="Interval Timer",
pack_ref="core",
description="Fires at regular intervals",
)
# Create sensor to make timer actually fire events
sensor_ref = f"{pack_ref}.{sensor_name}_sensor"
sensor_config = {"unit": "seconds", "interval": interval_seconds}
sensor = client.create_sensor(
ref=sensor_ref,
trigger_id=core_trigger["id"],
trigger_ref=core_trigger["ref"],
label=f"{sensor_name} Sensor",
description=f"Sensor for interval timer (every {interval_seconds}s)",
entrypoint="internal://timer",
runtime_ref="core.sensor.python3",
pack_ref=pack_ref,
enabled=True,
config=sensor_config,
)
# Restart sensor service to load the new sensor
restart_sensor_service(wait_seconds=2)
# Return dict with both trigger and sensor info
return {
"id": core_trigger["id"],
"ref": core_trigger["ref"],
"label": sensor["label"],
"trigger": core_trigger,
"sensor": sensor,
"sensor_id": sensor["id"],
}
def create_date_timer(
client: AttuneClient,
fire_at: Optional[str] = None,
seconds_from_now: int = 5,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
) -> Dict[str, Any]:
"""
Create date timer sensor for timer to actually fire
Args:
client: AttuneClient instance
fire_at: ISO timestamp when to fire (optional)
seconds_from_now: Seconds from now to fire (used if fire_at not provided)
name: Sensor name (generated if not provided)
pack_ref: Pack reference
Returns:
Dict with trigger and sensor info
"""
if not fire_at:
fire_at = timestamp_future(seconds_from_now)
sensor_name = name or f"date_{unique_ref()}"
# Ensure core pack exists
ensure_core_pack(client)
# Get or ensure core.datetimetimer trigger exists
triggers = client.list_triggers()
core_trigger = None
for t in triggers:
if t.get("ref") == "core.datetimetimer":
core_trigger = t
break
if not core_trigger:
# Create core.datetimetimer trigger if it doesn't exist
core_trigger = client.create_trigger(
ref="core.datetimetimer",
label="Date/Time Timer",
pack_ref="core",
description="Fires at a specific date/time",
)
# Create sensor to make timer actually fire events
sensor_ref = f"{pack_ref}.{sensor_name}_sensor"
sensor_config = {"date": fire_at}
sensor = client.create_sensor(
ref=sensor_ref,
trigger_id=core_trigger["id"],
trigger_ref=core_trigger["ref"],
label=f"{sensor_name} Sensor",
description=f"Sensor for date timer (fires at {fire_at})",
entrypoint="internal://timer",
runtime_ref="core.sensor.python3",
pack_ref=pack_ref,
enabled=True,
config=sensor_config,
)
# Restart sensor service to load the new sensor
restart_sensor_service(wait_seconds=2)
# Return dict with both trigger and sensor info
return {
"id": core_trigger["id"],
"ref": core_trigger["ref"],
"label": sensor["label"],
"trigger": core_trigger,
"sensor": sensor,
"sensor_id": sensor["id"],
"fire_at": fire_at,
}
def create_cron_timer(
client: AttuneClient,
cron_expression: str = "*/5 * * * * *",
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
timezone: str = "UTC",
) -> Dict[str, Any]:
"""
Create cron timer sensor for timer to actually fire
Args:
client: AttuneClient instance
cron_expression: Cron expression (6-field with seconds)
name: Sensor name (generated if not provided)
pack_ref: Pack reference
timezone: Timezone for cron evaluation
Returns:
Dict with trigger and sensor info
"""
sensor_name = name or f"cron_{unique_ref()}"
# Ensure core pack exists
ensure_core_pack(client)
# Get or ensure core.crontimer trigger exists
triggers = client.list_triggers()
core_trigger = None
for t in triggers:
if t.get("ref") == "core.crontimer":
core_trigger = t
break
if not core_trigger:
# Create core.crontimer trigger if it doesn't exist
core_trigger = client.create_trigger(
ref="core.crontimer",
label="Cron Timer",
pack_ref="core",
description="Fires based on cron schedule",
)
# Create sensor to make timer actually fire events
sensor_ref = f"{pack_ref}.{sensor_name}_sensor"
sensor_config = {"cron": cron_expression, "timezone": timezone}
sensor = client.create_sensor(
ref=sensor_ref,
trigger_id=core_trigger["id"],
trigger_ref=core_trigger["ref"],
label=f"{sensor_name} Sensor",
description=f"Sensor for cron timer ({cron_expression})",
entrypoint="internal://timer",
runtime_ref="core.sensor.python3",
pack_ref=pack_ref,
enabled=True,
config=sensor_config,
)
# Restart sensor service to load the new sensor
restart_sensor_service(wait_seconds=2)
# Return dict with both trigger and sensor info
return {
"id": core_trigger["id"],
"ref": core_trigger["ref"],
"label": sensor["label"],
"trigger": core_trigger,
"sensor": sensor,
"sensor_id": sensor["id"],
"cron_expression": cron_expression,
"timezone": timezone,
}
def create_webhook_trigger(
client: AttuneClient,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
) -> Dict[str, Any]:
"""
Create webhook trigger
Args:
client: AttuneClient instance
name: Trigger name (generated if not provided)
pack_ref: Pack reference
Returns:
Created trigger data
"""
trigger_name = name or f"webhook_{unique_ref()}"
return client.create_trigger(
pack_ref=pack_ref,
name=trigger_name,
trigger_type="webhook",
parameters={},
)
# ============================================================================
# Action Creation
# ============================================================================
def create_simple_action(
client: AttuneClient,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
runner_type: str = "python3",
entrypoint: str = "actions/echo.py",
param_schema: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Create simple action
Args:
client: AttuneClient instance
name: Action name (generated if not provided)
pack_ref: Pack reference
runner_type: Runner type
entrypoint: Entry point path
param_schema: JSON Schema for parameters
Returns:
Created action data
"""
action_name = name or f"action_{unique_ref()}"
if param_schema is None:
param_schema = {
"type": "object",
"properties": {"message": {"type": "string", "default": "Hello, World!"}},
}
return client.create_action(
pack_ref=pack_ref,
name=action_name,
runner_type=runner_type,
entrypoint=entrypoint,
param_schema=param_schema,
)
def create_echo_action(
client: AttuneClient,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
) -> Dict[str, Any]:
"""
Create echo action (simple action that echoes input)
Args:
client: AttuneClient instance
name: Action name (generated if not provided)
pack_ref: Pack reference
Returns:
Created action data
"""
return create_simple_action(
client=client,
name=name or f"echo_{unique_ref()}",
pack_ref=pack_ref,
runner_type="python3",
entrypoint="actions/echo.py",
param_schema={
"type": "object",
"properties": {
"message": {"type": "string", "default": "echo"},
"count": {"type": "integer", "default": 1},
},
},
)
def create_failing_action(
client: AttuneClient,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
exit_code: int = 1,
) -> Dict[str, Any]:
"""
Create action that always fails
Args:
client: AttuneClient instance
name: Action name (generated if not provided)
pack_ref: Pack reference
exit_code: Exit code to return
Returns:
Created action data
"""
action_name = name or f"failing_{unique_ref()}"
return client.create_action(
pack_ref=pack_ref,
name=action_name,
runner_type="python3",
entrypoint="actions/fail.py",
param_schema={
"type": "object",
"properties": {"exit_code": {"type": "integer", "default": exit_code}},
},
)
def create_sleep_action(
client: AttuneClient,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
default_duration: int = 5,
) -> Dict[str, Any]:
"""
Create action that sleeps for specified duration
Args:
client: AttuneClient instance
name: Action name (generated if not provided)
pack_ref: Pack reference
default_duration: Default sleep duration in seconds
Returns:
Created action data
"""
action_name = name or f"sleep_{unique_ref()}"
return client.create_action(
pack_ref=pack_ref,
name=action_name,
runner_type="python3",
entrypoint="actions/sleep.py",
param_schema={
"type": "object",
"properties": {
"duration": {"type": "integer", "default": default_duration}
},
},
)
# ============================================================================
# Rule Creation
# ============================================================================
def create_rule(
client: AttuneClient,
trigger_id: int,
action_ref: str,
name: Optional[str] = None,
pack_ref: str = "test.test_pack",
enabled: bool = True,
criteria: Optional[str] = None,
action_parameters: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Create rule
Args:
client: AttuneClient instance
trigger_id: Trigger ID to monitor
action_ref: Action reference to execute
name: Rule name (generated if not provided)
pack_ref: Pack reference
enabled: Whether rule is enabled
criteria: Optional Jinja2 criteria expression
action_parameters: Parameters to pass to action
Returns:
Created rule data
"""
rule_name = name or f"rule_{unique_ref()}"
return client.create_rule(
name=rule_name,
pack_ref=pack_ref,
trigger_id=trigger_id,
action_ref=action_ref,
enabled=enabled,
criteria=criteria,
action_parameters=action_parameters or {},
)
def create_timer_automation(
client: AttuneClient,
interval_seconds: int = 5,
pack_ref: str = "test.test_pack",
action_parameters: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Create complete timer automation (trigger + action + rule)
Args:
client: AttuneClient instance
interval_seconds: Timer interval in seconds
pack_ref: Pack reference
action_parameters: Parameters to pass to action
Returns:
Dictionary with trigger, action, and rule data
"""
# Create timer trigger
trigger = create_interval_timer(
client=client, interval_seconds=interval_seconds, pack_ref=pack_ref
)
# Create echo action
action = create_echo_action(client=client, pack_ref=pack_ref)
# Create rule linking them
rule = create_rule(
client=client,
trigger_id=trigger["id"],
action_ref=action["ref"],
pack_ref=pack_ref,
action_parameters=action_parameters,
)
return {"trigger": trigger, "action": action, "rule": rule}
def create_webhook_automation(
client: AttuneClient,
pack_ref: str = "test.test_pack",
action_parameters: Optional[Dict[str, Any]] = None,
criteria: Optional[str] = None,
) -> Dict[str, Any]:
"""
Create complete webhook automation (trigger + action + rule)
Args:
client: AttuneClient instance
pack_ref: Pack reference
action_parameters: Parameters to pass to action
criteria: Optional rule criteria
Returns:
Dictionary with trigger, action, and rule data
"""
# Create webhook trigger
trigger = create_webhook_trigger(client=client, pack_ref=pack_ref)
# Create echo action
action = create_echo_action(client=client, pack_ref=pack_ref)
# Create rule linking them
rule = create_rule(
client=client,
trigger_id=trigger["id"],
action_ref=action["ref"],
pack_ref=pack_ref,
action_parameters=action_parameters,
criteria=criteria,
)
return {"trigger": trigger, "action": action, "rule": rule}
# ============================================================================
# Service Management
# ============================================================================
def restart_sensor_service(wait_seconds: int = 3) -> bool:
"""
Restart sensor service to reload sensors
This is needed after creating new sensors so they are loaded
and can start generating events.
Works with E2E services managed by start-e2e-services.sh script.
Args:
wait_seconds: Seconds to wait after restart for service to be ready
Returns:
True if restart successful, False otherwise
"""
import os
import signal
import subprocess
try:
# Check if we're running in docker-compose environment
if os.path.exists("/.dockerenv") or os.getenv("DOCKER_ENV"):
# Try to restart via docker-compose
result = subprocess.run(
["docker-compose", "restart", "sensor"],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
time.sleep(wait_seconds)
return True
# For E2E services, use PID file to restart the sensor service
# Calculate paths relative to tests directory
helpers_dir = os.path.dirname(os.path.abspath(__file__)) # tests/helpers
tests_dir = os.path.dirname(helpers_dir) # tests
pid_dir = os.path.join(tests_dir, "pids")
pid_file = os.path.join(pid_dir, "sensor.pid")
log_dir = os.path.join(tests_dir, "logs")
log_file = os.path.join(log_dir, "sensor.log")
if os.path.exists(pid_file):
# Read PID and stop the service
with open(pid_file, "r") as f:
pid_str = f.read().strip()
if not pid_str:
print(f"Warning: PID file {pid_file} is empty")
os.remove(pid_file)
return False
pid = int(pid_str)
# Stop the existing process
stopped = False
try:
# Send SIGTERM for graceful shutdown
os.kill(pid, signal.SIGTERM)
# Wait up to 5 seconds for graceful shutdown
for _ in range(10):
try:
os.kill(pid, 0) # Check if process exists
time.sleep(0.5)
except ProcessLookupError:
stopped = True
break
# Force kill if still running
if not stopped:
try:
os.kill(pid, signal.SIGKILL)
time.sleep(1)
stopped = True
except ProcessLookupError:
stopped = True
except ProcessLookupError:
stopped = True # Process doesn't exist
# Remove PID file
if os.path.exists(pid_file):
os.remove(pid_file)
if not stopped:
print(f"Warning: Failed to stop sensor process {pid}")
# Restart the sensor service
# Get project root
project_root = os.path.dirname(tests_dir) # project root
binary_path = os.path.join(project_root, "target", "debug", "attune-sensor")
config_file = os.path.join(project_root, "config.e2e.yaml")
# Verify binary exists
if not os.path.exists(binary_path):
print(f"Error: Sensor binary not found at {binary_path}")
return False
env = os.environ.copy()
env["ATTUNE__ENVIRONMENT"] = "e2e"
env["ATTUNE_CONFIG"] = config_file
with open(log_file, "a") as log:
log.write(f"\n\n=== Sensor service restarted at {time.time()} ===\n\n")
log.flush()
process = subprocess.Popen(
[binary_path],
stdout=log,
stderr=subprocess.STDOUT,
env=env,
start_new_session=True,
cwd=project_root, # Run from project root
)
# Write new PID file
with open(pid_file, "w") as f:
f.write(str(process.pid))
f.flush()
# Wait for service to initialize
time.sleep(wait_seconds)
# Verify process is still running
try:
os.kill(process.pid, 0)
# Additional verification: check if log file is being written to
if os.path.exists(log_file):
# Get file size before and after a short wait
size_before = os.path.getsize(log_file)
time.sleep(1)
size_after = os.path.getsize(log_file)
if size_after > size_before:
print(
f"✓ Sensor service restarted successfully (PID: {process.pid})"
)
return True
else:
print(
f"Warning: Sensor service started but not logging (PID: {process.pid})"
)
return True # Still return True as process is running
else:
print(f"✓ Sensor service restarted (PID: {process.pid})")
return True
except ProcessLookupError:
print(f"✗ Sensor service failed to start (process died immediately)")
if os.path.exists(pid_file):
os.remove(pid_file)
return False
else:
print(
f"Warning: No PID file found at {pid_file}. Sensor service may not be running."
)
time.sleep(wait_seconds)
return False
except Exception as e:
print(f"Warning: Error restarting sensor service: {e}")
import traceback
traceback.print_exc()
time.sleep(wait_seconds)
return False