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

View File

@@ -0,0 +1,377 @@
//! Integration tests for Python virtual environment dependency isolation
//!
//! Tests the end-to-end flow of creating isolated Python environments
//! for packs with dependencies.
use attune_worker::runtime::{
DependencyManager, DependencyManagerRegistry, DependencySpec, PythonVenvManager,
};
use tempfile::TempDir;
#[tokio::test]
async fn test_python_venv_creation() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python").with_dependency("requests==2.28.0");
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
assert_eq!(env_info.runtime, "python");
assert!(env_info.is_valid);
assert!(env_info.path.exists());
assert!(env_info.executable_path.exists());
}
#[tokio::test]
async fn test_venv_idempotency() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python").with_dependency("requests==2.28.0");
// Create environment first time
let env_info1 = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
let created_at1 = env_info1.created_at;
// Call ensure_environment again with same dependencies
let env_info2 = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to ensure environment");
// Should return existing environment (same created_at)
assert_eq!(env_info1.created_at, env_info2.created_at);
assert_eq!(created_at1, env_info2.created_at);
}
#[tokio::test]
async fn test_venv_update_on_dependency_change() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec1 = DependencySpec::new("python").with_dependency("requests==2.28.0");
// Create environment with first set of dependencies
let env_info1 = manager
.ensure_environment("test_pack", &spec1)
.await
.expect("Failed to create environment");
let created_at1 = env_info1.created_at;
// Give it a moment to ensure timestamp difference
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Change dependencies
let spec2 = DependencySpec::new("python").with_dependency("requests==2.29.0");
// Should recreate environment
let env_info2 = manager
.ensure_environment("test_pack", &spec2)
.await
.expect("Failed to update environment");
// Updated timestamp should be newer
assert!(env_info2.updated_at >= created_at1);
}
#[tokio::test]
async fn test_multiple_pack_isolation() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec1 = DependencySpec::new("python").with_dependency("requests==2.28.0");
let spec2 = DependencySpec::new("python").with_dependency("flask==2.3.0");
// Create environments for two different packs
let env1 = manager
.ensure_environment("pack_a", &spec1)
.await
.expect("Failed to create environment for pack_a");
let env2 = manager
.ensure_environment("pack_b", &spec2)
.await
.expect("Failed to create environment for pack_b");
// Should have different paths
assert_ne!(env1.path, env2.path);
assert_ne!(env1.executable_path, env2.executable_path);
// Both should be valid
assert!(env1.is_valid);
assert!(env2.is_valid);
}
#[tokio::test]
async fn test_get_executable_path() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python");
manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
let python_path = manager
.get_executable_path("test_pack")
.await
.expect("Failed to get executable path");
assert!(python_path.exists());
assert!(python_path.to_string_lossy().contains("test_pack"));
}
#[tokio::test]
async fn test_validate_environment() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
// Non-existent environment should not be valid
let is_valid = manager
.validate_environment("nonexistent")
.await
.expect("Validation check failed");
assert!(!is_valid);
// Create environment
let spec = DependencySpec::new("python");
manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
// Should now be valid
let is_valid = manager
.validate_environment("test_pack")
.await
.expect("Validation check failed");
assert!(is_valid);
}
#[tokio::test]
async fn test_remove_environment() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python");
// Create environment
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
let path = env_info.path.clone();
assert!(path.exists());
// Remove environment
manager
.remove_environment("test_pack")
.await
.expect("Failed to remove environment");
assert!(!path.exists());
// Get environment should return None
let env = manager
.get_environment("test_pack")
.await
.expect("Failed to get environment");
assert!(env.is_none());
}
#[tokio::test]
async fn test_list_environments() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python");
// Create multiple environments
manager
.ensure_environment("pack_a", &spec)
.await
.expect("Failed to create pack_a");
manager
.ensure_environment("pack_b", &spec)
.await
.expect("Failed to create pack_b");
manager
.ensure_environment("pack_c", &spec)
.await
.expect("Failed to create pack_c");
// List should return all three
let environments = manager
.list_environments()
.await
.expect("Failed to list environments");
assert_eq!(environments.len(), 3);
}
#[tokio::test]
async fn test_dependency_manager_registry() {
let temp_dir = TempDir::new().unwrap();
let mut registry = DependencyManagerRegistry::new();
let python_manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
registry.register(Box::new(python_manager));
// Should support python
assert!(registry.supports("python"));
assert!(!registry.supports("nodejs"));
// Should be able to get manager
let manager = registry.get("python");
assert!(manager.is_some());
assert_eq!(manager.unwrap().runtime_type(), "python");
}
#[tokio::test]
async fn test_dependency_spec_builder() {
let spec = DependencySpec::new("python")
.with_dependency("requests==2.28.0")
.with_dependency("flask>=2.0.0")
.with_version_range(Some("3.8".to_string()), Some("3.11".to_string()));
assert_eq!(spec.runtime, "python");
assert_eq!(spec.dependencies.len(), 2);
assert!(spec.has_dependencies());
assert_eq!(spec.min_version, Some("3.8".to_string()));
assert_eq!(spec.max_version, Some("3.11".to_string()));
}
#[tokio::test]
async fn test_requirements_file_content() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let requirements = "requests==2.28.0\nflask==2.3.0\npydantic>=2.0.0";
let spec = DependencySpec::new("python").with_requirements_file(requirements.to_string());
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment with requirements file");
assert!(env_info.is_valid);
assert!(env_info.installed_dependencies.len() > 0);
}
#[tokio::test]
async fn test_pack_ref_sanitization() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python");
// Pack refs with special characters should be sanitized
let env_info = manager
.ensure_environment("core.http", &spec)
.await
.expect("Failed to create environment");
// Path should not contain dots
let path_str = env_info.path.to_string_lossy();
assert!(path_str.contains("core_http"));
assert!(!path_str.contains("core.http"));
}
#[tokio::test]
async fn test_needs_update_detection() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec1 = DependencySpec::new("python").with_dependency("requests==2.28.0");
// Non-existent environment needs update
let needs_update = manager
.needs_update("test_pack", &spec1)
.await
.expect("Failed to check update status");
assert!(needs_update);
// Create environment
manager
.ensure_environment("test_pack", &spec1)
.await
.expect("Failed to create environment");
// Same spec should not need update
let needs_update = manager
.needs_update("test_pack", &spec1)
.await
.expect("Failed to check update status");
assert!(!needs_update);
// Different spec should need update
let spec2 = DependencySpec::new("python").with_dependency("requests==2.29.0");
let needs_update = manager
.needs_update("test_pack", &spec2)
.await
.expect("Failed to check update status");
assert!(needs_update);
}
#[tokio::test]
async fn test_empty_dependencies() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
// Pack with no dependencies should still create venv
let spec = DependencySpec::new("python");
assert!(!spec.has_dependencies());
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment without dependencies");
assert!(env_info.is_valid);
assert!(env_info.path.exists());
}
#[tokio::test]
async fn test_get_environment_caching() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python");
// Create environment
manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
// First get_environment should read from disk
let env1 = manager
.get_environment("test_pack")
.await
.expect("Failed to get environment")
.expect("Environment not found");
// Second get_environment should use cache
let env2 = manager
.get_environment("test_pack")
.await
.expect("Failed to get environment")
.expect("Environment not found");
assert_eq!(env1.id, env2.id);
assert_eq!(env1.path, env2.path);
}

View File

@@ -0,0 +1,277 @@
//! Integration tests for log size truncation
//!
//! Tests that verify stdout/stderr are properly truncated when they exceed
//! configured size limits, preventing OOM issues with large output.
use attune_worker::runtime::{ExecutionContext, PythonRuntime, Runtime, ShellRuntime};
use std::collections::HashMap;
#[tokio::test]
async fn test_python_stdout_truncation() {
let runtime = PythonRuntime::new();
// Create a Python script that outputs more than the limit
let code = r#"
import sys
# Output 1KB of data (will exceed 500 byte limit)
for i in range(100):
print("x" * 10)
"#;
let context = ExecutionContext {
execution_id: 1,
action_ref: "test.large_output".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(10),
working_dir: None,
entry_point: "test_script".to_string(),
code: Some(code.to_string()),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 500, // Small limit to trigger truncation
max_stderr_bytes: 1024,
};
let result = runtime.execute(context).await.unwrap();
// Should succeed but with truncated output
assert!(result.is_success());
assert!(result.stdout_truncated);
assert!(result.stdout.contains("[OUTPUT TRUNCATED"));
assert!(result.stdout_bytes_truncated > 0);
assert!(result.stdout.len() <= 500);
}
#[tokio::test]
async fn test_python_stderr_truncation() {
let runtime = PythonRuntime::new();
// Create a Python script that outputs to stderr
let code = r#"
import sys
# Output 1KB of data to stderr
for i in range(100):
sys.stderr.write("error message line\n")
"#;
let context = ExecutionContext {
execution_id: 2,
action_ref: "test.large_stderr".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(10),
working_dir: None,
entry_point: "test_script".to_string(),
code: Some(code.to_string()),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 300, // Small limit for stderr
};
let result = runtime.execute(context).await.unwrap();
// Should succeed but with truncated stderr
assert!(result.is_success());
assert!(!result.stdout_truncated);
assert!(result.stderr_truncated);
assert!(result.stderr.contains("[OUTPUT TRUNCATED"));
assert!(result.stderr.contains("stderr exceeded size limit"));
assert!(result.stderr_bytes_truncated > 0);
assert!(result.stderr.len() <= 300);
}
#[tokio::test]
async fn test_shell_stdout_truncation() {
let runtime = ShellRuntime::new();
// Shell script that outputs more than the limit
let code = r#"
for i in {1..100}; do
echo "This is a long line of text that will add up quickly"
done
"#;
let context = ExecutionContext {
execution_id: 3,
action_ref: "test.shell_large_output".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(10),
working_dir: None,
entry_point: "shell".to_string(),
code: Some(code.to_string()),
code_path: None,
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 400, // Small limit
max_stderr_bytes: 1024,
};
let result = runtime.execute(context).await.unwrap();
// Should succeed but with truncated output
assert!(result.is_success());
assert!(result.stdout_truncated);
assert!(result.stdout.contains("[OUTPUT TRUNCATED"));
assert!(result.stdout_bytes_truncated > 0);
assert!(result.stdout.len() <= 400);
}
#[tokio::test]
async fn test_no_truncation_under_limit() {
let runtime = PythonRuntime::new();
// Small output that won't trigger truncation
let code = r#"
print("Hello, World!")
"#;
let context = ExecutionContext {
execution_id: 4,
action_ref: "test.small_output".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(10),
working_dir: None,
entry_point: "test_script".to_string(),
code: Some(code.to_string()),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024, // Large limit
max_stderr_bytes: 10 * 1024 * 1024,
};
let result = runtime.execute(context).await.unwrap();
// Should succeed without truncation
assert!(result.is_success());
assert!(!result.stdout_truncated);
assert!(!result.stderr_truncated);
assert_eq!(result.stdout_bytes_truncated, 0);
assert_eq!(result.stderr_bytes_truncated, 0);
assert!(result.stdout.contains("Hello, World!"));
}
#[tokio::test]
async fn test_both_streams_truncated() {
let runtime = PythonRuntime::new();
// Script that outputs to both stdout and stderr
let code = r#"
import sys
# Output to both streams
for i in range(50):
print("stdout line " + str(i))
sys.stderr.write("stderr line " + str(i) + "\n")
"#;
let context = ExecutionContext {
execution_id: 5,
action_ref: "test.dual_truncation".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(10),
working_dir: None,
entry_point: "test_script".to_string(),
code: Some(code.to_string()),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 300, // Both limits are small
max_stderr_bytes: 300,
};
let result = runtime.execute(context).await.unwrap();
// Should succeed but with both streams truncated
assert!(result.is_success());
assert!(result.stdout_truncated);
assert!(result.stderr_truncated);
assert!(result.stdout.contains("[OUTPUT TRUNCATED"));
assert!(result.stderr.contains("[OUTPUT TRUNCATED"));
assert!(result.stdout_bytes_truncated > 0);
assert!(result.stderr_bytes_truncated > 0);
assert!(result.stdout.len() <= 300);
assert!(result.stderr.len() <= 300);
}
#[tokio::test]
async fn test_truncation_with_timeout() {
let runtime = PythonRuntime::new();
// Script that times out but should still capture truncated logs
let code = r#"
import time
for i in range(1000):
print(f"Line {i}")
time.sleep(30) # Will timeout before this
"#;
let context = ExecutionContext {
execution_id: 6,
action_ref: "test.timeout_truncation".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(2), // Short timeout
working_dir: None,
entry_point: "test_script".to_string(),
code: Some(code.to_string()),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 500,
max_stderr_bytes: 1024,
};
let result = runtime.execute(context).await.unwrap();
// Should timeout with truncated logs
assert!(!result.is_success());
assert!(result.error.is_some());
assert!(result.error.as_ref().unwrap().contains("timed out"));
// Logs may or may not be truncated depending on how fast it runs
}
#[tokio::test]
async fn test_exact_limit_no_truncation() {
let runtime = PythonRuntime::new();
// Output a small amount that won't trigger truncation
// The Python wrapper adds JSON result output, so we need headroom
let code = r#"
import sys
sys.stdout.write("Small output")
"#;
let context = ExecutionContext {
execution_id: 7,
action_ref: "test.exact_limit".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(10),
working_dir: None,
entry_point: "test_script".to_string(),
code: Some(code.to_string()),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024, // Large limit to avoid truncation
max_stderr_bytes: 10 * 1024 * 1024,
};
let result = runtime.execute(context).await.unwrap();
// Should succeed without truncation
eprintln!(
"test_exact_limit_no_truncation: exit_code={}, error={:?}, stdout={:?}, stderr={:?}",
result.exit_code, result.error, result.stdout, result.stderr
);
assert!(result.is_success());
assert!(!result.stdout_truncated);
assert!(result.stdout.contains("Small output"));
}

View File

@@ -0,0 +1,415 @@
//! Security Tests for Secret Handling
//!
//! These tests verify that secrets are NOT exposed in process environment
//! or command-line arguments, ensuring secure secret passing via stdin.
use attune_worker::runtime::python::PythonRuntime;
use attune_worker::runtime::shell::ShellRuntime;
use attune_worker::runtime::{ExecutionContext, Runtime};
use std::collections::HashMap;
#[tokio::test]
async fn test_python_secrets_not_in_environ() {
let runtime = PythonRuntime::new();
let context = ExecutionContext {
execution_id: 1,
action_ref: "security.test_environ".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: {
let mut s = HashMap::new();
s.insert(
"api_key".to_string(),
"super_secret_key_do_not_expose".to_string(),
);
s.insert("password".to_string(), "secret_pass_123".to_string());
s
},
timeout: Some(10),
working_dir: None,
entry_point: "run".to_string(),
code: Some(
r#"
import os
def run():
# Check if secrets are in environment variables
environ_str = str(os.environ)
# Secrets should NOT be in environment
has_secret_in_env = 'super_secret_key_do_not_expose' in environ_str
has_password_in_env = 'secret_pass_123' in environ_str
has_secret_prefix = 'SECRET_API_KEY' in os.environ or 'SECRET_PASSWORD' in os.environ
# But they SHOULD be accessible via get_secret()
api_key_accessible = get_secret('api_key') == 'super_secret_key_do_not_expose'
password_accessible = get_secret('password') == 'secret_pass_123'
return {
'secrets_in_environ': has_secret_in_env or has_password_in_env or has_secret_prefix,
'api_key_accessible': api_key_accessible,
'password_accessible': password_accessible,
'environ_check': 'SECRET_' not in environ_str
}
"#
.to_string(),
),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
};
let result = runtime.execute(context).await.unwrap();
assert!(result.is_success(), "Execution should succeed");
let result_data = result.result.unwrap();
let result_obj = result_data.get("result").unwrap();
// Critical security check: secrets should NOT be in environment
assert_eq!(
result_obj.get("secrets_in_environ").unwrap(),
&serde_json::json!(false),
"SECURITY FAILURE: Secrets found in process environment!"
);
// Verify secrets ARE accessible via secure method
assert_eq!(
result_obj.get("api_key_accessible").unwrap(),
&serde_json::json!(true),
"Secrets should be accessible via get_secret()"
);
assert_eq!(
result_obj.get("password_accessible").unwrap(),
&serde_json::json!(true),
"Secrets should be accessible via get_secret()"
);
// Verify no SECRET_ prefix in environment
assert_eq!(
result_obj.get("environ_check").unwrap(),
&serde_json::json!(true),
"Environment should not contain SECRET_ prefix variables"
);
}
#[tokio::test]
async fn test_shell_secrets_not_in_environ() {
let runtime = ShellRuntime::new();
let context = ExecutionContext {
execution_id: 2,
action_ref: "security.test_shell_environ".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: {
let mut s = HashMap::new();
s.insert(
"api_key".to_string(),
"super_secret_key_do_not_expose".to_string(),
);
s.insert("password".to_string(), "secret_pass_123".to_string());
s
},
timeout: Some(10),
working_dir: None,
entry_point: "shell".to_string(),
code: Some(
r#"
# Check if secrets are in environment variables
if printenv | grep -q "super_secret_key_do_not_expose"; then
echo "SECURITY_FAIL: Secret found in environment"
exit 1
fi
if printenv | grep -q "secret_pass_123"; then
echo "SECURITY_FAIL: Password found in environment"
exit 1
fi
if printenv | grep -q "SECRET_API_KEY"; then
echo "SECURITY_FAIL: SECRET_ prefix found in environment"
exit 1
fi
# But secrets SHOULD be accessible via get_secret function
api_key=$(get_secret 'api_key')
password=$(get_secret 'password')
if [ "$api_key" != "super_secret_key_do_not_expose" ]; then
echo "ERROR: Secret not accessible via get_secret"
exit 1
fi
if [ "$password" != "secret_pass_123" ]; then
echo "ERROR: Password not accessible via get_secret"
exit 1
fi
echo "SECURITY_PASS: Secrets not in environment but accessible via get_secret"
"#
.to_string(),
),
code_path: None,
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
};
let result = runtime.execute(context).await.unwrap();
// Check execution succeeded
assert!(result.is_success(), "Execution should succeed");
assert_eq!(result.exit_code, 0, "Exit code should be 0");
// Verify security pass message
assert!(
result.stdout.contains("SECURITY_PASS"),
"Security checks should pass"
);
assert!(
!result.stdout.contains("SECURITY_FAIL"),
"Should not have security failures"
);
}
#[tokio::test]
async fn test_python_secret_isolation_between_actions() {
let runtime = PythonRuntime::new();
// First action with secret A
let context1 = ExecutionContext {
execution_id: 3,
action_ref: "security.action1".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: {
let mut s = HashMap::new();
s.insert("secret_a".to_string(), "value_a".to_string());
s
},
timeout: Some(10),
working_dir: None,
entry_point: "run".to_string(),
code: Some(
r#"
def run():
return {'secret_a': get_secret('secret_a')}
"#
.to_string(),
),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
};
let result1 = runtime.execute(context1).await.unwrap();
assert!(result1.is_success());
// Second action with secret B (should not see secret A)
let context2 = ExecutionContext {
execution_id: 4,
action_ref: "security.action2".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: {
let mut s = HashMap::new();
s.insert("secret_b".to_string(), "value_b".to_string());
s
},
timeout: Some(10),
working_dir: None,
entry_point: "run".to_string(),
code: Some(
r#"
def run():
# Should NOT see secret_a from previous action
secret_a = get_secret('secret_a')
secret_b = get_secret('secret_b')
return {
'secret_a_leaked': secret_a is not None,
'secret_b_present': secret_b == 'value_b'
}
"#
.to_string(),
),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
};
let result2 = runtime.execute(context2).await.unwrap();
assert!(result2.is_success());
let result_data = result2.result.unwrap();
let result_obj = result_data.get("result").unwrap();
// Verify secrets don't leak between actions
assert_eq!(
result_obj.get("secret_a_leaked").unwrap(),
&serde_json::json!(false),
"Secret from previous action should not leak"
);
assert_eq!(
result_obj.get("secret_b_present").unwrap(),
&serde_json::json!(true),
"Current action's secret should be present"
);
}
#[tokio::test]
async fn test_python_empty_secrets() {
let runtime = PythonRuntime::new();
let context = ExecutionContext {
execution_id: 5,
action_ref: "security.no_secrets".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(), // No secrets
timeout: Some(10),
working_dir: None,
entry_point: "run".to_string(),
code: Some(
r#"
def run():
# get_secret should return None for non-existent secrets
result = get_secret('nonexistent')
return {'result': result}
"#
.to_string(),
),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
};
let result = runtime.execute(context).await.unwrap();
assert!(
result.is_success(),
"Should handle empty secrets gracefully"
);
let result_data = result.result.unwrap();
let result_obj = result_data.get("result").unwrap();
assert_eq!(result_obj.get("result").unwrap(), &serde_json::Value::Null);
}
#[tokio::test]
async fn test_shell_empty_secrets() {
let runtime = ShellRuntime::new();
let context = ExecutionContext {
execution_id: 6,
action_ref: "security.no_secrets".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(), // No secrets
timeout: Some(10),
working_dir: None,
entry_point: "shell".to_string(),
code: Some(
r#"
# get_secret should return empty string for non-existent secrets
result=$(get_secret 'nonexistent')
if [ -z "$result" ]; then
echo "PASS: Empty secret returns empty string"
else
echo "FAIL: Expected empty string"
exit 1
fi
"#
.to_string(),
),
code_path: None,
runtime_name: Some("shell".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
};
let result = runtime.execute(context).await.unwrap();
assert!(
result.is_success(),
"Should handle empty secrets gracefully"
);
assert!(result.stdout.contains("PASS"));
}
#[tokio::test]
async fn test_python_special_characters_in_secrets() {
let runtime = PythonRuntime::new();
let context = ExecutionContext {
execution_id: 7,
action_ref: "security.special_chars".to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: {
let mut s = HashMap::new();
s.insert("special_chars".to_string(), "test!@#$%^&*()".to_string());
s.insert("with_newline".to_string(), "line1\nline2".to_string());
s
},
timeout: Some(10),
working_dir: None,
entry_point: "run".to_string(),
code: Some(
r#"
def run():
special = get_secret('special_chars')
newline = get_secret('with_newline')
newline_char = chr(10)
newline_parts = newline.split(newline_char) if newline else []
return {
'special_correct': special == 'test!@#$%^&*()',
'newline_has_two_parts': len(newline_parts) == 2,
'newline_first_part': newline_parts[0] if len(newline_parts) > 0 else '',
'newline_second_part': newline_parts[1] if len(newline_parts) > 1 else '',
'special_len': len(special) if special else 0
}
"#
.to_string(),
),
code_path: None,
runtime_name: Some("python".to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
};
let result = runtime.execute(context).await.unwrap();
assert!(
result.is_success(),
"Should handle special characters: {:?}",
result.error
);
let result_data = result.result.unwrap();
let result_obj = result_data.get("result").unwrap();
assert_eq!(
result_obj.get("special_correct").unwrap(),
&serde_json::json!(true),
"Special characters should be preserved"
);
assert_eq!(
result_obj.get("newline_has_two_parts").unwrap(),
&serde_json::json!(true),
"Newline should split into two parts"
);
assert_eq!(
result_obj.get("newline_first_part").unwrap(),
&serde_json::json!("line1"),
"First part should be 'line1'"
);
assert_eq!(
result_obj.get("newline_second_part").unwrap(),
&serde_json::json!("line2"),
"Second part should be 'line2'"
);
}