mod support; use agentos_execution::{ CreateJavascriptContextRequest, CreatePythonContextRequest, CreateWasmContextRequest, PythonExecutionEvent, StartJavascriptExecutionRequest, StartPythonExecutionRequest, StartWasmExecutionRequest, WasmPermissionTier, }; use std::collections::BTreeMap; use std::fs; use std::path::Path; use std::process::Command; use std::time::{Duration, Instant}; use tempfile::tempdir; fn assert_node_available() { let binary = std::env::var("AGENTOS_NODE_BINARY").unwrap_or_else(|_| String::from("node")); let output = Command::new(binary) .arg("--version") .output() .expect("spawn node --version"); assert!(output.status.success(), "node --version failed"); } fn write_fixture(path: &Path, contents: &str) { fs::write(path, contents).expect("write fixture"); } fn write_pyodide_lock_fixture(path: &Path) { write_fixture(path, "{\"packages\":[]}\n"); let pyodide_dir = path.parent().expect("pyodide fixture parent"); for asset in ["pyodide.asm.js", "pyodide.asm.wasm", "python_stdlib.zip"] { let asset_path = pyodide_dir.join(asset); if !asset_path.exists() { fs::write(&asset_path, []).expect("write pyodide runtime fixture"); } } } fn embedded_runtime_process_keeps_host_pid_internal_for_javascript() { let temp = tempdir().expect("create temp dir"); let mut engine = support::javascript_engine(); let context = engine.create_context(CreateJavascriptContextRequest { vm_id: String::from("vm-js"), bootstrap_module: None, compile_cache_root: None, }); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), wasm_module_bytes: None, inline_code: Some(String::from("globalThis.__secureExecRanInV8 = true;")), }) .expect("start JavaScript execution"); assert!(execution.uses_shared_v8_runtime()); assert_eq!(execution.child_pid(), 0); let result = execution.wait().expect("wait for JavaScript execution"); assert_eq!(result.exit_code, 0); } fn embedded_runtime_process_keeps_host_pid_internal_for_wasm() { assert_node_available(); let temp = tempdir().expect("create temp dir"); let module_path = temp.path().join("guest.wasm"); let module = wat::parse_str( r#"(module (func (export "_start")) )"#, ) .expect("compile wasm fixture"); fs::write(&module_path, module).expect("write wasm fixture"); let mut engine = support::wasm_engine(); let context = engine.create_context(CreateWasmContextRequest { vm_id: String::from("vm-wasm"), module_path: Some(module_path.to_string_lossy().into_owned()), }); let execution = engine .start_execution(StartWasmExecutionRequest { guest_runtime: Default::default(), limits: Default::default(), vm_id: String::from("vm-wasm"), context_id: context.context_id, argv: vec![module_path.to_string_lossy().into_owned()], env: BTreeMap::new(), cwd: temp.path().to_path_buf(), permission_tier: WasmPermissionTier::ReadWrite, }) .expect("start wasm execution"); assert!(execution.uses_shared_v8_runtime()); assert_eq!(execution.child_pid(), 0); let result = execution.wait().expect("wait for wasm execution"); assert_eq!( result.exit_code, 0, "stderr: {}", String::from_utf8_lossy(&result.stderr) ); } fn embedded_runtime_process_uses_session_control_for_python_kill() { assert_node_available(); let temp = tempdir().expect("create temp dir"); let pyodide_dir = temp.path().join("pyodide"); fs::create_dir_all(&pyodide_dir).expect("create pyodide dir"); write_fixture( &pyodide_dir.join("pyodide.mjs"), r#" export async function loadPyodide(options) { options.stdout("ready\n"); return { setStdin(_stdin) {}, async runPythonAsync() { await new Promise(() => setInterval(() => {}, 1000)); }, }; } "#, ); write_pyodide_lock_fixture(&pyodide_dir.join("pyodide-lock.json")); let mut engine = support::python_engine(); let context = engine.create_context(CreatePythonContextRequest { vm_id: String::from("vm-python"), pyodide_dist_path: pyodide_dir, }); let mut execution = engine .start_execution(StartPythonExecutionRequest { guest_runtime: Default::default(), limits: Default::default(), vm_id: String::from("vm-python"), context_id: context.context_id, code: String::from("print('hang')"), file_path: None, env: BTreeMap::new(), cwd: temp.path().to_path_buf(), }) .expect("start Python execution"); assert!(execution.uses_shared_v8_runtime()); assert_eq!(execution.child_pid(), 0); let ready_deadline = Instant::now() + Duration::from_secs(5); let mut saw_ready = false; while !saw_ready { if Instant::now() >= ready_deadline { panic!("timed out waiting for Python execution readiness"); } match execution .poll_event_blocking( ready_deadline .saturating_duration_since(Instant::now()) .min(Duration::from_millis(100)), ) .expect("poll Python event before kill") { Some(PythonExecutionEvent::Stdout(chunk)) => { saw_ready = String::from_utf8(chunk) .expect("stdout utf8") .contains("ready"); } Some(PythonExecutionEvent::Exited(code)) => { panic!("execution exited unexpectedly before kill with code {code}"); } Some(PythonExecutionEvent::Stderr(chunk)) => { panic!("unexpected stderr: {}", String::from_utf8_lossy(&chunk)); } Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request during kill test: {request:?}"); } Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC"), "unexpected JS sync RPC request during kill test: {request:?}" ); } None => panic!("timed out waiting for Python execution readiness"), } } execution.kill().expect("kill hanging Python execution"); let kill_deadline = Instant::now() + Duration::from_secs(5); let mut exit_code = None; while exit_code.is_none() { if Instant::now() >= kill_deadline { panic!("timed out waiting for killed Python execution to exit"); } match execution .poll_event_blocking( kill_deadline .saturating_duration_since(Instant::now()) .min(Duration::from_millis(100)), ) .expect("poll Python event after kill") { Some(PythonExecutionEvent::Exited(code)) => exit_code = Some(code), Some(PythonExecutionEvent::Stdout(_)) | Some(PythonExecutionEvent::Stderr(_)) => {} Some(PythonExecutionEvent::VfsRpcRequest(request)) => { panic!("unexpected VFS RPC request after kill: {request:?}"); } Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { assert!( execution .try_service_standalone_module_sync_rpc(&request) .expect("service module sync RPC"), "unexpected JS sync RPC request after kill: {request:?}" ); } None => {} } } assert_eq!(exit_code, Some(1)); } #[test] fn embedded_runtime_process_suite() { embedded_runtime_process_keeps_host_pid_internal_for_javascript(); embedded_runtime_process_keeps_host_pid_internal_for_wasm(); embedded_runtime_process_uses_session_control_for_python_kill(); }