//! Integration tests for runtime environment and dependency isolation //! //! Tests the end-to-end flow of creating isolated runtime environments //! for packs using the ProcessRuntime configuration-driven approach. //! //! Environment directories are placed at: //! {runtime_envs_dir}/{pack_ref}/{runtime_name} //! e.g., /tmp/.../runtime_envs/testpack/python //! This keeps the pack directory clean and read-only. use attune_common::models::runtime::{ DependencyConfig, EnvironmentConfig, InterpreterConfig, RuntimeExecutionConfig, }; use attune_worker::runtime::process::ProcessRuntime; use attune_worker::runtime::ExecutionContext; use attune_worker::runtime::Runtime; use attune_worker::runtime::{OutputFormat, ParameterDelivery, ParameterFormat}; use std::collections::HashMap; use std::path::PathBuf; use tempfile::TempDir; fn make_python_config() -> RuntimeExecutionConfig { RuntimeExecutionConfig { interpreter: InterpreterConfig { binary: "python3".to_string(), args: vec!["-u".to_string()], file_extension: Some(".py".to_string()), }, environment: Some(EnvironmentConfig { env_type: "virtualenv".to_string(), dir_name: ".venv".to_string(), create_command: vec![ "python3".to_string(), "-m".to_string(), "venv".to_string(), "{env_dir}".to_string(), ], interpreter_path: Some("{env_dir}/bin/python3".to_string()), }), dependencies: Some(DependencyConfig { manifest_file: "requirements.txt".to_string(), install_command: vec![ "{interpreter}".to_string(), "-m".to_string(), "pip".to_string(), "install".to_string(), "-r".to_string(), "{manifest_path}".to_string(), ], }), env_vars: std::collections::HashMap::new(), } } fn make_shell_config() -> RuntimeExecutionConfig { RuntimeExecutionConfig { interpreter: InterpreterConfig { binary: "/bin/bash".to_string(), args: vec![], file_extension: Some(".sh".to_string()), }, environment: None, dependencies: None, env_vars: std::collections::HashMap::new(), } } fn make_context(action_ref: &str, entry_point: &str, runtime_name: &str) -> ExecutionContext { ExecutionContext { execution_id: 1, action_ref: action_ref.to_string(), parameters: HashMap::new(), env: HashMap::new(), secrets: HashMap::new(), timeout: Some(30), working_dir: None, entry_point: entry_point.to_string(), code: None, code_path: None, runtime_name: Some(runtime_name.to_string()), runtime_config_override: None, runtime_env_dir_suffix: None, selected_runtime_version: None, max_stdout_bytes: 10 * 1024 * 1024, max_stderr_bytes: 10 * 1024 * 1024, parameter_delivery: ParameterDelivery::default(), parameter_format: ParameterFormat::default(), output_format: OutputFormat::default(), cancel_token: None, } } #[tokio::test] async fn test_python_venv_creation_via_process_runtime() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("python"); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // Setup the pack environment (creates venv at external location) runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Failed to create venv environment"); // Verify venv was created at the external runtime_envs location assert!( env_dir.exists(), "Virtualenv directory should exist at external location" ); let venv_python = env_dir.join("bin").join("python3"); assert!( venv_python.exists(), "Virtualenv python3 binary should exist" ); // Verify pack directory was NOT modified assert!( !pack_dir.join(".venv").exists(), "Pack directory should not contain .venv — environments are external" ); } #[tokio::test] async fn test_venv_creation_is_idempotent() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("python"); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // Create environment first time runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Failed to create environment"); assert!(env_dir.exists()); // Create environment second time — should succeed without error runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Second setup should succeed (idempotent)"); assert!(env_dir.exists()); } #[tokio::test] async fn test_dependency_installation() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("python"); // Write a requirements.txt with a simple, fast-to-install package std::fs::write( pack_dir.join("requirements.txt"), "pip>=21.0\n", // pip is already installed, so this is fast ) .unwrap(); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // Setup creates the venv and installs dependencies runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Failed to setup environment with dependencies"); assert!(env_dir.exists()); } #[tokio::test] async fn test_no_environment_for_shell_runtime() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("shell"); let runtime = ProcessRuntime::new( "shell".to_string(), make_shell_config(), packs_base_dir, runtime_envs_dir, ); // Shell runtime has no environment config — should be a no-op runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Shell setup should succeed (no environment to create)"); // No environment should exist assert!(!env_dir.exists()); assert!(!pack_dir.join(".venv").exists()); assert!(!pack_dir.join("node_modules").exists()); } #[tokio::test] async fn test_pack_has_dependencies_detection() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // No requirements.txt yet assert!( !runtime.pack_has_dependencies(&pack_dir), "Should not detect dependencies without manifest file" ); // Create requirements.txt std::fs::write(pack_dir.join("requirements.txt"), "requests>=2.28.0\n").unwrap(); assert!( runtime.pack_has_dependencies(&pack_dir), "Should detect dependencies when manifest file exists" ); } #[tokio::test] async fn test_environment_exists_detection() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("python"); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // No venv yet — environment_exists uses pack_ref string assert!( !runtime.environment_exists("testpack"), "Environment should not exist before setup" ); // Create the venv runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Failed to create environment"); assert!( runtime.environment_exists("testpack"), "Environment should exist after setup" ); } #[tokio::test] async fn test_multiple_pack_isolation() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_a_dir = packs_base_dir.join("pack_a"); let pack_b_dir = packs_base_dir.join("pack_b"); std::fs::create_dir_all(&pack_a_dir).unwrap(); std::fs::create_dir_all(&pack_b_dir).unwrap(); let env_dir_a = runtime_envs_dir.join("pack_a").join("python"); let env_dir_b = runtime_envs_dir.join("pack_b").join("python"); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // Setup environments for two different packs runtime .setup_pack_environment(&pack_a_dir, &env_dir_a) .await .expect("Failed to setup pack_a"); runtime .setup_pack_environment(&pack_b_dir, &env_dir_b) .await .expect("Failed to setup pack_b"); // Each pack should have its own venv at the external location assert!(env_dir_a.exists(), "pack_a should have its own venv"); assert!(env_dir_b.exists(), "pack_b should have its own venv"); assert_ne!( env_dir_a, env_dir_b, "Venvs should be in different directories" ); // Pack directories should remain clean assert!( !pack_a_dir.join(".venv").exists(), "pack_a dir should not contain .venv" ); assert!( !pack_b_dir.join(".venv").exists(), "pack_b dir should not contain .venv" ); } #[tokio::test] async fn test_execute_python_action_with_venv() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); let actions_dir = pack_dir.join("actions"); std::fs::create_dir_all(&actions_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("python"); // Write a Python script std::fs::write( actions_dir.join("hello.py"), r#" import sys print(f"Python from: {sys.executable}") print("Hello from venv action!") "#, ) .unwrap(); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // Setup the venv first runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Failed to setup venv"); // Now execute the action let mut context = make_context("testpack.hello", "hello.py", "python"); context.code_path = Some(actions_dir.join("hello.py")); let result = runtime.execute(context).await.unwrap(); assert_eq!(result.exit_code, 0, "Action should succeed"); assert!( result.stdout.contains("Hello from venv action!"), "Should see output from action. Got: {}", result.stdout ); // Verify it's using the venv Python (at external runtime_envs location) assert!( result.stdout.contains("runtime_envs"), "Should be using the venv python from external runtime_envs dir. Got: {}", result.stdout ); } #[tokio::test] async fn test_execute_shell_action_no_venv() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); let actions_dir = pack_dir.join("actions"); std::fs::create_dir_all(&actions_dir).unwrap(); std::fs::write( actions_dir.join("greet.sh"), "#!/bin/bash\necho 'Hello from shell!'", ) .unwrap(); let runtime = ProcessRuntime::new( "shell".to_string(), make_shell_config(), packs_base_dir, runtime_envs_dir, ); let mut context = make_context("testpack.greet", "greet.sh", "shell"); context.code_path = Some(actions_dir.join("greet.sh")); let result = runtime.execute(context).await.unwrap(); assert_eq!(result.exit_code, 0); assert!(result.stdout.contains("Hello from shell!")); } #[tokio::test] async fn test_working_directory_is_pack_dir() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); let actions_dir = pack_dir.join("actions"); std::fs::create_dir_all(&actions_dir).unwrap(); // Script that prints the working directory std::fs::write(actions_dir.join("cwd.sh"), "#!/bin/bash\npwd").unwrap(); let runtime = ProcessRuntime::new( "shell".to_string(), make_shell_config(), packs_base_dir, runtime_envs_dir, ); let mut context = make_context("testpack.cwd", "cwd.sh", "shell"); context.code_path = Some(actions_dir.join("cwd.sh")); let result = runtime.execute(context).await.unwrap(); assert_eq!(result.exit_code, 0); let output_path = result.stdout.trim(); assert_eq!( output_path, pack_dir.to_string_lossy().as_ref(), "Working directory should be the pack directory" ); } #[tokio::test] async fn test_interpreter_resolution_with_venv() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("python"); let config = make_python_config(); let runtime = ProcessRuntime::new( "python".to_string(), config.clone(), packs_base_dir, runtime_envs_dir, ); // Before venv creation — should resolve to system python let interpreter = config.resolve_interpreter_with_env(&pack_dir, Some(&env_dir)); assert_eq!( interpreter, PathBuf::from("python3"), "Without venv, should use system python" ); // Create venv at external location runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Failed to create venv"); // After venv creation — should resolve to venv python at external location let interpreter = config.resolve_interpreter_with_env(&pack_dir, Some(&env_dir)); let expected_venv_python = env_dir.join("bin").join("python3"); assert_eq!( interpreter, expected_venv_python, "With venv, should use venv python from external runtime_envs dir" ); } #[tokio::test] async fn test_skip_deps_install_without_manifest() { let temp_dir = TempDir::new().unwrap(); let packs_base_dir = temp_dir.path().join("packs"); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let pack_dir = packs_base_dir.join("testpack"); std::fs::create_dir_all(&pack_dir).unwrap(); let env_dir = runtime_envs_dir.join("testpack").join("python"); // No requirements.txt — install_dependencies should be a no-op let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), packs_base_dir, runtime_envs_dir, ); // Setup should still create the venv but skip dependency installation runtime .setup_pack_environment(&pack_dir, &env_dir) .await .expect("Setup should succeed without manifest"); assert!( env_dir.exists(), "Venv should still be created at external location" ); } #[tokio::test] async fn test_runtime_config_matches_file_extension() { let config = make_python_config(); assert!(config.matches_file_extension(std::path::Path::new("hello.py"))); assert!(config.matches_file_extension(std::path::Path::new( "/opt/attune/packs/mypack/actions/script.py" ))); assert!(!config.matches_file_extension(std::path::Path::new("hello.sh"))); assert!(!config.matches_file_extension(std::path::Path::new("hello.js"))); let shell_config = make_shell_config(); assert!(shell_config.matches_file_extension(std::path::Path::new("run.sh"))); assert!(!shell_config.matches_file_extension(std::path::Path::new("run.py"))); } #[tokio::test] async fn test_dependency_spec_builder_still_works() { // The DependencySpec types are still available for generic use use attune_worker::runtime::DependencySpec; 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_process_runtime_setup_and_validate() { let temp_dir = TempDir::new().unwrap(); let runtime_envs_dir = temp_dir.path().join("runtime_envs"); let shell_runtime = ProcessRuntime::new( "shell".to_string(), make_shell_config(), temp_dir.path().to_path_buf(), runtime_envs_dir.clone(), ); // Setup and validate should succeed for shell shell_runtime.setup().await.unwrap(); shell_runtime.validate().await.unwrap(); let python_runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), temp_dir.path().to_path_buf(), runtime_envs_dir, ); // Setup and validate should succeed for python (warns if not available) python_runtime.setup().await.unwrap(); python_runtime.validate().await.unwrap(); } #[tokio::test] async fn test_can_execute_by_runtime_name() { let temp_dir = TempDir::new().unwrap(); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), temp_dir.path().to_path_buf(), temp_dir.path().join("runtime_envs"), ); let context = make_context("mypack.hello", "hello.py", "python"); assert!(runtime.can_execute(&context)); let wrong_context = make_context("mypack.hello", "hello.py", "shell"); assert!(!runtime.can_execute(&wrong_context)); } #[tokio::test] async fn test_can_execute_by_file_extension() { let temp_dir = TempDir::new().unwrap(); let runtime = ProcessRuntime::new( "python".to_string(), make_python_config(), temp_dir.path().to_path_buf(), temp_dir.path().join("runtime_envs"), ); let mut context = make_context("mypack.hello", "hello.py", ""); context.runtime_name = None; context.code_path = Some(PathBuf::from("/tmp/packs/mypack/actions/hello.py")); assert!(runtime.can_execute(&context)); context.code_path = Some(PathBuf::from("/tmp/packs/mypack/actions/hello.sh")); context.entry_point = "hello.sh".to_string(); assert!(!runtime.can_execute(&context)); }