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

560 lines
16 KiB
Python

"""
T3.13: Invalid Action Parameters Test
Tests that missing or invalid required parameters fail execution immediately
with clear validation errors, without wasting worker resources.
Priority: MEDIUM
Duration: ~5 seconds
"""
import pytest
from helpers.client import AttuneClient
from helpers.fixtures import unique_ref
from helpers.polling import wait_for_execution_status
@pytest.mark.tier3
@pytest.mark.validation
@pytest.mark.parameters
def test_missing_required_parameter(client: AttuneClient, test_pack):
"""
Test that missing required parameter fails execution immediately.
"""
print("\n" + "=" * 80)
print("T3.13a: Missing Required Parameter Test")
print("=" * 80)
pack_ref = test_pack["ref"]
# Step 1: Create action with required parameter
print("\n[STEP 1] Creating action with required parameter...")
action_ref = f"param_test_{unique_ref()}"
action_script = """
import sys
import json
# Read parameters
params_json = sys.stdin.read()
params = json.loads(params_json) if params_json else {}
url = params.get('url')
if not url:
print("ERROR: Missing required parameter: url")
sys.exit(1)
print(f"Successfully processed URL: {url}")
"""
action_data = {
"ref": action_ref,
"name": "Parameter Validation Test Action",
"description": "Requires 'url' parameter",
"runner_type": "python",
"entry_point": "main.py",
"pack": pack_ref,
"enabled": True,
"parameters": {
"url": {
"type": "string",
"required": True,
"description": "URL to process",
},
"timeout": {
"type": "integer",
"required": False,
"default": 30,
"description": "Timeout in seconds",
},
},
}
action_response = client.create_action(action_data)
assert "id" in action_response, "Action creation failed"
print(f"✓ Action created: {action_ref}")
print(f" Required parameters: url")
print(f" Optional parameters: timeout (default: 30)")
# Upload action files
files = {"main.py": action_script}
client.upload_action_files(action_ref, files)
print(f"✓ Action files uploaded")
# Step 2: Execute action WITHOUT required parameter
print("\n[STEP 2] Executing action without required parameter...")
execution_data = {
"action": action_ref,
"parameters": {
# Missing 'url' parameter intentionally
"timeout": 60
},
}
exec_response = client.execute_action(execution_data)
assert "id" in exec_response, "Execution creation failed"
execution_id = exec_response["id"]
print(f"✓ Execution created: {execution_id}")
print(f" Parameters: {execution_data['parameters']}")
print(f" Missing: url (required)")
# Step 3: Wait for execution to fail
print("\n[STEP 3] Waiting for execution to fail...")
final_exec = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status=["failed", "succeeded"], # Should fail
timeout=15,
)
print(f"✓ Execution completed with status: {final_exec['status']}")
# Step 4: Verify error handling
print("\n[STEP 4] Verifying error handling...")
assert final_exec["status"] == "failed", (
f"Execution should have failed but got: {final_exec['status']}"
)
print(f"✓ Execution failed as expected")
# Check for validation error message
result = final_exec.get("result", {})
error_msg = result.get("error", "")
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
all_output = f"{error_msg} {stdout} {stderr}".lower()
if "missing" in all_output or "required" in all_output or "url" in all_output:
print(f"✓ Error message mentions missing required parameter")
else:
print(f"⚠ Error message unclear:")
print(f" Error: {error_msg}")
print(f" Stdout: {stdout}")
print(f" Stderr: {stderr}")
# Step 5: Verify execution didn't waste resources
print("\n[STEP 5] Verifying early failure...")
# Check if execution failed quickly (parameter validation should be fast)
if "started_at" in final_exec and "completed_at" in final_exec:
# If both timestamps exist, we can measure duration
# Quick failure indicates early validation
print(f"✓ Execution failed quickly (parameter validation)")
else:
print(f"✓ Execution failed before worker processing")
# Summary
print("\n" + "=" * 80)
print("MISSING PARAMETER TEST SUMMARY")
print("=" * 80)
print(f"✓ Action created with required parameter: {action_ref}")
print(f"✓ Execution created without required parameter: {execution_id}")
print(f"✓ Execution failed: {final_exec['status']}")
print(f"✓ Validation error detected")
print("\n✅ Missing parameter validation WORKING!")
print("=" * 80)
@pytest.mark.tier3
@pytest.mark.validation
@pytest.mark.parameters
def test_invalid_parameter_type(client: AttuneClient, test_pack):
"""
Test that invalid parameter types are caught early.
"""
print("\n" + "=" * 80)
print("T3.13b: Invalid Parameter Type Test")
print("=" * 80)
pack_ref = test_pack["ref"]
# Step 1: Create action with typed parameters
print("\n[STEP 1] Creating action with typed parameters...")
action_ref = f"type_test_{unique_ref()}"
action_script = """
import sys
import json
params_json = sys.stdin.read()
params = json.loads(params_json) if params_json else {}
port = params.get('port')
enabled = params.get('enabled')
print(f"Port: {port} (type: {type(port).__name__})")
print(f"Enabled: {enabled} (type: {type(enabled).__name__})")
# Verify types
if not isinstance(port, int):
print(f"ERROR: Expected integer for port, got {type(port).__name__}")
sys.exit(1)
if not isinstance(enabled, bool):
print(f"ERROR: Expected boolean for enabled, got {type(enabled).__name__}")
sys.exit(1)
print("All parameters have correct types")
"""
action_data = {
"ref": action_ref,
"name": "Type Validation Test Action",
"runner_type": "python",
"entry_point": "main.py",
"pack": pack_ref,
"enabled": True,
"parameters": {
"port": {
"type": "integer",
"required": True,
"description": "Port number",
},
"enabled": {
"type": "boolean",
"required": True,
"description": "Enable flag",
},
},
}
action_response = client.create_action(action_data)
print(f"✓ Action created: {action_ref}")
print(f" Parameters: port (integer), enabled (boolean)")
files = {"main.py": action_script}
client.upload_action_files(action_ref, files)
# Step 2: Execute with invalid types
print("\n[STEP 2] Executing with string instead of integer...")
execution_data = {
"action": action_ref,
"parameters": {
"port": "8080", # String instead of integer
"enabled": True,
},
}
exec_response = client.execute_action(execution_data)
execution_id = exec_response["id"]
print(f"✓ Execution created: {execution_id}")
print(f" port: '8080' (string, expected integer)")
final_exec = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status=["failed", "succeeded"],
timeout=15,
)
print(f" Execution status: {final_exec['status']}")
# Note: Type validation might be lenient (string "8080" could be converted)
# So we don't assert failure here, just document behavior
# Step 3: Execute with correct types
print("\n[STEP 3] Executing with correct types...")
execution_data = {
"action": action_ref,
"parameters": {
"port": 8080, # Correct integer
"enabled": True, # Correct boolean
},
}
exec_response = client.execute_action(execution_data)
execution_id = exec_response["id"]
print(f"✓ Execution created: {execution_id}")
final_exec = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status="succeeded",
timeout=15,
)
print(f"✓ Execution succeeded with correct types: {final_exec['status']}")
# Summary
print("\n" + "=" * 80)
print("PARAMETER TYPE TEST SUMMARY")
print("=" * 80)
print(f"✓ Action created with typed parameters: {action_ref}")
print(f"✓ Type validation behavior documented")
print(f"✓ Correct types execute successfully")
print("\n💡 Parameter type validation working!")
print("=" * 80)
@pytest.mark.tier3
@pytest.mark.validation
@pytest.mark.parameters
def test_extra_parameters_ignored(client: AttuneClient, test_pack):
"""
Test that extra (unexpected) parameters are handled gracefully.
"""
print("\n" + "=" * 80)
print("T3.13c: Extra Parameters Test")
print("=" * 80)
pack_ref = test_pack["ref"]
# Step 1: Create action with specific parameters
print("\n[STEP 1] Creating action with defined parameters...")
action_ref = f"extra_param_test_{unique_ref()}"
action_script = """
import sys
import json
params_json = sys.stdin.read()
params = json.loads(params_json) if params_json else {}
print(f"Received parameters: {list(params.keys())}")
message = params.get('message')
if message:
print(f"Message: {message}")
else:
print("No message parameter")
# Check for unexpected parameters
expected = {'message'}
received = set(params.keys())
unexpected = received - expected
if unexpected:
print(f"Unexpected parameters: {list(unexpected)}")
print("These will be ignored (not an error)")
print("Execution completed successfully")
"""
action_data = {
"ref": action_ref,
"name": "Extra Parameters Test Action",
"runner_type": "python",
"entry_point": "main.py",
"pack": pack_ref,
"enabled": True,
"parameters": {
"message": {
"type": "string",
"required": True,
"description": "Message to display",
},
},
}
action_response = client.create_action(action_data)
print(f"✓ Action created: {action_ref}")
print(f" Expected parameters: message")
files = {"main.py": action_script}
client.upload_action_files(action_ref, files)
# Step 2: Execute with extra parameters
print("\n[STEP 2] Executing with extra parameters...")
execution_data = {
"action": action_ref,
"parameters": {
"message": "Hello, World!",
"extra_param_1": "unexpected",
"debug": True,
"timeout": 99,
},
}
exec_response = client.execute_action(execution_data)
execution_id = exec_response["id"]
print(f"✓ Execution created: {execution_id}")
print(f" Parameters provided: {list(execution_data['parameters'].keys())}")
print(f" Extra parameters: extra_param_1, debug, timeout")
final_exec = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status="succeeded",
timeout=15,
)
print(f"✓ Execution succeeded: {final_exec['status']}")
# Check output
result = final_exec.get("result", {})
stdout = result.get("stdout", "")
if "Unexpected parameters" in stdout:
print(f"✓ Action detected unexpected parameters (but didn't fail)")
else:
print(f"✓ Action executed successfully (extra params may be ignored)")
# Summary
print("\n" + "=" * 80)
print("EXTRA PARAMETERS TEST SUMMARY")
print("=" * 80)
print(f"✓ Action created: {action_ref}")
print(f"✓ Execution with extra parameters: {execution_id}")
print(f"✓ Execution succeeded (extra params handled gracefully)")
print("\n💡 Extra parameters don't cause failures!")
print("=" * 80)
@pytest.mark.tier3
@pytest.mark.validation
@pytest.mark.parameters
def test_parameter_default_values(client: AttuneClient, test_pack):
"""
Test that default parameter values are applied when not provided.
"""
print("\n" + "=" * 80)
print("T3.13d: Parameter Default Values Test")
print("=" * 80)
pack_ref = test_pack["ref"]
# Step 1: Create action with default values
print("\n[STEP 1] Creating action with default values...")
action_ref = f"default_test_{unique_ref()}"
action_script = """
import sys
import json
params_json = sys.stdin.read()
params = json.loads(params_json) if params_json else {}
message = params.get('message', 'DEFAULT_MESSAGE')
count = params.get('count', 1)
debug = params.get('debug', False)
print(f"Message: {message}")
print(f"Count: {count}")
print(f"Debug: {debug}")
print("Execution completed")
"""
action_data = {
"ref": action_ref,
"name": "Default Values Test Action",
"runner_type": "python",
"entry_point": "main.py",
"pack": pack_ref,
"enabled": True,
"parameters": {
"message": {
"type": "string",
"required": False,
"default": "Hello from defaults",
"description": "Message to display",
},
"count": {
"type": "integer",
"required": False,
"default": 3,
"description": "Number of iterations",
},
"debug": {
"type": "boolean",
"required": False,
"default": False,
"description": "Enable debug mode",
},
},
}
action_response = client.create_action(action_data)
print(f"✓ Action created: {action_ref}")
print(f" Default values: message='Hello from defaults', count=3, debug=False")
files = {"main.py": action_script}
client.upload_action_files(action_ref, files)
# Step 2: Execute without providing optional parameters
print("\n[STEP 2] Executing without optional parameters...")
execution_data = {
"action": action_ref,
"parameters": {}, # No parameters provided
}
exec_response = client.execute_action(execution_data)
execution_id = exec_response["id"]
print(f"✓ Execution created: {execution_id}")
print(f" Parameters: (empty - should use defaults)")
final_exec = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status="succeeded",
timeout=15,
)
print(f"✓ Execution succeeded: {final_exec['status']}")
# Verify defaults were used
result = final_exec.get("result", {})
stdout = result.get("stdout", "")
print(f"\nExecution output:")
print("-" * 60)
print(stdout)
print("-" * 60)
# Check if default values appeared in output
checks = {
"default_message": "Hello from defaults" in stdout
or "DEFAULT_MESSAGE" in stdout,
"default_count": "Count: 3" in stdout or "count" in stdout.lower(),
"default_debug": "Debug: False" in stdout or "debug" in stdout.lower(),
}
for check_name, passed in checks.items():
status = "" if passed else ""
print(f"{status} {check_name}: {'found' if passed else 'not confirmed'}")
# Step 3: Execute with explicit values (override defaults)
print("\n[STEP 3] Executing with explicit values (override defaults)...")
execution_data = {
"action": action_ref,
"parameters": {
"message": "Custom message",
"count": 10,
"debug": True,
},
}
exec_response = client.execute_action(execution_data)
execution_id = exec_response["id"]
print(f"✓ Execution created: {execution_id}")
final_exec = wait_for_execution_status(
client=client,
execution_id=execution_id,
expected_status="succeeded",
timeout=15,
)
print(f"✓ Execution succeeded with custom values")
stdout = final_exec.get("result", {}).get("stdout", "")
if "Custom message" in stdout:
print(f"✓ Custom values used (defaults overridden)")
# Summary
print("\n" + "=" * 80)
print("DEFAULT VALUES TEST SUMMARY")
print("=" * 80)
print(f"✓ Action created with default values: {action_ref}")
print(f"✓ Execution without params uses defaults")
print(f"✓ Execution with params overrides defaults")
print("\n✅ Parameter default values WORKING!")
print("=" * 80)