openzeppelin_monitor/services/trigger/script/
executor.rs

1//! Trigger script executor implementation.
2//!
3//! This module provides functionality to execute scripts in different languages.
4
5use crate::models::MonitorMatch;
6use anyhow::Context;
7use async_trait::async_trait;
8use std::{any::Any, process::Stdio, time::Duration};
9use tokio::{io::AsyncWriteExt, time::timeout};
10
11/// A trait that defines the interface for executing custom scripts in different languages.
12/// Implementors must be both Send and Sync to ensure thread safety.
13#[async_trait]
14pub trait ScriptExecutor: Send + Sync + Any {
15	/// Enables downcasting by returning a reference to `Any`
16	fn as_any(&self) -> &dyn Any;
17	/// Executes the script with the given MonitorMatch input.
18	///
19	/// # Arguments
20	/// * `input` - A MonitorMatch instance containing the data to be processed by the script
21	/// * `timeout_ms` - The timeout for the script execution in milliseconds
22	/// * `args` - Additional arguments passed to the script
23	/// * `from_custom_notification` - Whether the script is from a custom notification
24	///
25	/// # Returns
26	/// * `Result<bool, anyhow::Error>` - Returns true/false based on script execution or an error
27	async fn execute(
28		&self,
29		input: MonitorMatch,
30		timeout_ms: &u32,
31		args: Option<&[String]>,
32		from_custom_notification: bool,
33	) -> Result<bool, anyhow::Error>;
34}
35
36/// Executes Python scripts using the python3 interpreter.
37pub struct PythonScriptExecutor {
38	/// Content of the Python script file to be executed
39	pub script_content: String,
40}
41
42#[async_trait]
43impl ScriptExecutor for PythonScriptExecutor {
44	fn as_any(&self) -> &dyn Any {
45		self
46	}
47	async fn execute(
48		&self,
49		input: MonitorMatch,
50		timeout_ms: &u32,
51		args: Option<&[String]>,
52		from_custom_notification: bool,
53	) -> Result<bool, anyhow::Error> {
54		let combined_input = serde_json::json!({
55			"monitor_match": input,
56			"args": args
57		});
58		let input_json = serde_json::to_string(&combined_input)
59			.with_context(|| "Failed to serialize monitor match and arguments")?;
60
61		let cmd = tokio::process::Command::new("python3")
62			.arg("-c")
63			.arg(&self.script_content)
64			.stdin(Stdio::piped())
65			.stdout(Stdio::piped())
66			.stderr(Stdio::piped())
67			.spawn()
68			.with_context(|| "Failed to spawn python3 process")?;
69
70		process_command(cmd, &input_json, timeout_ms, from_custom_notification).await
71	}
72}
73
74/// Executes JavaScript scripts using the Node.js runtime.
75pub struct JavaScriptScriptExecutor {
76	/// Content of the JavaScript script file to be executed
77	pub script_content: String,
78}
79
80#[async_trait]
81impl ScriptExecutor for JavaScriptScriptExecutor {
82	fn as_any(&self) -> &dyn Any {
83		self
84	}
85	async fn execute(
86		&self,
87		input: MonitorMatch,
88		timeout_ms: &u32,
89		args: Option<&[String]>,
90		from_custom_notification: bool,
91	) -> Result<bool, anyhow::Error> {
92		// Create a combined input with both the monitor match and arguments
93		let combined_input = serde_json::json!({
94			"monitor_match": input,
95			"args": args
96		});
97		let input_json = serde_json::to_string(&combined_input)
98			.with_context(|| "Failed to serialize monitor match and arguments")?;
99
100		let cmd = tokio::process::Command::new("node")
101			.arg("-e")
102			.arg(&self.script_content)
103			.stdin(Stdio::piped())
104			.stdout(Stdio::piped())
105			.stderr(Stdio::piped())
106			.spawn()
107			.with_context(|| "Failed to spawn node process")?;
108		process_command(cmd, &input_json, timeout_ms, from_custom_notification).await
109	}
110}
111
112/// Executes Bash shell scripts.
113pub struct BashScriptExecutor {
114	/// Content of the Bash script file to be executed
115	pub script_content: String,
116}
117
118#[async_trait]
119impl ScriptExecutor for BashScriptExecutor {
120	fn as_any(&self) -> &dyn Any {
121		self
122	}
123	async fn execute(
124		&self,
125		input: MonitorMatch,
126		timeout_ms: &u32,
127		args: Option<&[String]>,
128		from_custom_notification: bool,
129	) -> Result<bool, anyhow::Error> {
130		// Create a combined input with both the monitor match and arguments
131		let combined_input = serde_json::json!({
132			"monitor_match": input,
133			"args": args
134		});
135
136		let input_json = serde_json::to_string(&combined_input)
137			.with_context(|| "Failed to serialize monitor match and arguments")?;
138
139		let cmd = tokio::process::Command::new("sh")
140			.arg("-c")
141			.arg(&self.script_content)
142			.stdin(Stdio::piped())
143			.stdout(Stdio::piped())
144			.stderr(Stdio::piped())
145			.spawn()
146			.with_context(|| "Failed to spawn shell process")?;
147
148		process_command(cmd, &input_json, timeout_ms, from_custom_notification).await
149	}
150}
151
152/// Processes the output from script execution.
153///
154/// # Arguments
155/// * `output` - The process output containing stdout, stderr, and status
156/// * `from_custom_notification` - Whether the script is from a custom notification
157/// # Returns
158/// * `Result<bool, anyhow::Error>` - Returns parsed boolean result or error
159///
160/// # Errors
161/// Returns an error if:
162/// * The script execution was not successful (non-zero exit code)
163/// * The output cannot be parsed as a boolean
164/// * The script produced no output
165#[allow(clippy::result_large_err)]
166pub fn process_script_output(
167	output: std::process::Output,
168	from_custom_notification: bool,
169) -> Result<bool, anyhow::Error> {
170	if !output.status.success() {
171		let error_message = String::from_utf8_lossy(&output.stderr).to_string();
172		return Err(anyhow::anyhow!(
173			"Script execution failed: {}",
174			error_message
175		));
176	}
177
178	// If the script is from a custom notification and the status is success, we don't need to check
179	// the output
180	if from_custom_notification {
181		return Ok(true);
182	}
183
184	let stdout = String::from_utf8_lossy(&output.stdout);
185
186	if stdout.trim().is_empty() {
187		return Err(anyhow::anyhow!("Script produced no output"));
188	}
189
190	let last_line = stdout
191		.lines()
192		.last()
193		.ok_or_else(|| anyhow::anyhow!("No output from script"))?
194		.trim();
195
196	match last_line.to_lowercase().as_str() {
197		"true" => Ok(true),
198		"false" => Ok(false),
199		_ => Err(anyhow::anyhow!(
200			"Last line of output is not a valid boolean: {}",
201			last_line
202		)),
203	}
204}
205
206async fn process_command(
207	mut cmd: tokio::process::Child,
208	input_json: &str,
209	timeout_ms: &u32,
210	from_custom_notification: bool,
211) -> Result<bool, anyhow::Error> {
212	if let Some(mut stdin) = cmd.stdin.take() {
213		stdin
214			.write_all(input_json.as_bytes())
215			.await
216			.map_err(|e| anyhow::anyhow!("Failed to write input to script: {}", e))?;
217
218		// Explicitly close stdin
219		stdin
220			.shutdown()
221			.await
222			.map_err(|e| anyhow::anyhow!("Failed to close stdin: {}", e))?;
223	} else {
224		return Err(anyhow::anyhow!("Failed to get stdin handle"));
225	}
226
227	let timeout_duration = Duration::from_millis(u64::from(*timeout_ms));
228
229	match timeout(timeout_duration, cmd.wait_with_output()).await {
230		Ok(result) => {
231			let output =
232				result.map_err(|e| anyhow::anyhow!("Failed to wait for script output: {}", e))?;
233			process_script_output(output, from_custom_notification)
234		}
235		Err(_) => Err(anyhow::anyhow!("Script execution timed out")),
236	}
237}
238
239#[cfg(test)]
240mod tests {
241	use super::*;
242	use crate::{
243		models::{
244			AddressWithSpec, EVMMonitorMatch, EVMReceiptLog, EventCondition, FunctionCondition,
245			MatchConditions, Monitor, MonitorMatch, TransactionCondition,
246		},
247		utils::tests::evm::{
248			monitor::MonitorBuilder, receipt::ReceiptBuilder, transaction::TransactionBuilder,
249		},
250	};
251	use std::{fs, path::Path, time::Instant};
252
253	fn read_fixture(filename: &str) -> String {
254		let fixture_path = Path::new(env!("CARGO_MANIFEST_DIR"))
255			.join("tests/integration/fixtures/filters")
256			.join(filename);
257		fs::read_to_string(fixture_path)
258			.unwrap_or_else(|_| panic!("Failed to read fixture file: {}", filename))
259	}
260
261	/// Creates a test monitor with customizable parameters
262	fn create_test_monitor(
263		event_conditions: Vec<EventCondition>,
264		function_conditions: Vec<FunctionCondition>,
265		transaction_conditions: Vec<TransactionCondition>,
266		addresses: Vec<AddressWithSpec>,
267	) -> Monitor {
268		let mut builder = MonitorBuilder::new()
269			.name("test")
270			.networks(vec!["evm_mainnet".to_string()]);
271
272		for event in event_conditions {
273			builder = builder.event(&event.signature, event.expression);
274		}
275		for function in function_conditions {
276			builder = builder.function(&function.signature, function.expression);
277		}
278		for transaction in transaction_conditions {
279			builder = builder.transaction(transaction.status, transaction.expression);
280		}
281
282		builder = builder.addresses_with_spec(
283			addresses
284				.into_iter()
285				.map(|a| (a.address, a.contract_spec))
286				.collect(),
287		);
288
289		builder.build()
290	}
291
292	fn create_test_evm_logs() -> Vec<EVMReceiptLog> {
293		ReceiptBuilder::new().build().logs.clone()
294	}
295
296	fn create_mock_monitor_match() -> MonitorMatch {
297		MonitorMatch::EVM(Box::new(EVMMonitorMatch {
298			monitor: create_test_monitor(vec![], vec![], vec![], vec![]),
299			transaction: TransactionBuilder::new().build(),
300			receipt: Some(ReceiptBuilder::new().build()),
301			logs: Some(create_test_evm_logs()),
302			network_slug: "evm_mainnet".to_string(),
303			matched_on: MatchConditions {
304				functions: vec![],
305				events: vec![],
306				transactions: vec![],
307			},
308			matched_on_args: None,
309		}))
310	}
311
312	#[tokio::test]
313	async fn test_python_script_executor_success() {
314		let script_content = r#"
315import sys
316import json
317
318# Read from stdin instead of command line arguments
319input_json = sys.stdin.read()
320data = json.loads(input_json)
321print("debugging...")
322def test():
323    return True
324result = test()
325print(result)
326"#;
327
328		let executor = PythonScriptExecutor {
329			script_content: script_content.to_string(),
330		};
331
332		let input = create_mock_monitor_match();
333
334		let timeout = 1000;
335		let result = executor.execute(input, &timeout, None, false).await;
336		assert!(result.is_ok());
337		assert!(result.unwrap());
338	}
339
340	#[tokio::test]
341	async fn test_python_script_executor_invalid_output() {
342		let script_content = r#"
343import sys
344input_json = sys.stdin.read()
345print("debugging...")
346def test():
347    return "not a boolean"
348result = test()
349print(result)
350"#;
351
352		let executor = PythonScriptExecutor {
353			script_content: script_content.to_string(),
354		};
355
356		let input = create_mock_monitor_match();
357		let result = executor.execute(input, &1000, None, false).await;
358		assert!(result.is_err());
359		match result {
360			Err(err) => {
361				let err_msg = err.to_string();
362				assert!(
363					err_msg.contains("Last line of output is not a valid boolean: not a boolean")
364				);
365			}
366			_ => panic!("Expected error"),
367		}
368	}
369
370	#[tokio::test]
371	async fn test_python_script_executor_multiple_prints() {
372		let script_content = r#"
373import sys
374import json
375
376# Read from stdin instead of command line arguments
377input_json = sys.stdin.read()
378data = json.loads(input_json)
379print("Starting script execution...")
380print("Processing data...")
381print("More debug info")
382print("true")
383"#;
384
385		let executor = PythonScriptExecutor {
386			script_content: script_content.to_string(),
387		};
388
389		let input = create_mock_monitor_match();
390
391		let result = executor.execute(input, &1000, None, false).await;
392		assert!(result.is_ok());
393		assert!(result.unwrap());
394	}
395
396	#[tokio::test]
397	async fn test_javascript_script_executor_success() {
398		let script_content = r#"
399		// Read input from stdin
400		(async () => {
401			let input = '';
402
403			await new Promise((resolve, reject) => {
404				process.stdin.on('data', (chunk) => {
405					input += chunk;
406				});
407
408				process.stdin.on('end', resolve);
409
410				process.stdin.on('error', reject);
411			});
412
413			try {
414				const data = JSON.parse(input);
415				console.log("debugging...");
416				console.log("finished");
417				console.log("true");
418			} catch (err) {
419				console.error(err);
420			}
421		})();
422		"#;
423
424		let executor = JavaScriptScriptExecutor {
425			script_content: script_content.to_string(),
426		};
427
428		let input = create_mock_monitor_match();
429		let result = executor.execute(input, &5000, None, false).await;
430		assert!(result.is_ok());
431		assert!(result.unwrap());
432	}
433
434	#[tokio::test]
435	async fn test_javascript_script_executor_invalid_output() {
436		let script_content = r#"
437		// Read input from stdin
438		(async () => {
439			let input = '';
440			await new Promise((resolve, reject) => {
441				process.stdin.on('data', chunk => input += chunk);
442				process.stdin.on('end', resolve);
443				process.stdin.on('error', reject);
444			});
445
446			try {
447				JSON.parse(input);
448				console.log("debugging...");
449				console.log("finished");
450				console.log("not a boolean");
451			} catch (err) {
452				console.log(err);
453			}
454		})();
455		"#;
456
457		let executor = JavaScriptScriptExecutor {
458			script_content: script_content.to_string(),
459		};
460
461		let input = create_mock_monitor_match();
462		let result = executor.execute(input, &5000, None, false).await;
463		assert!(result.is_err());
464		match result {
465			Err(err) => {
466				let err_msg = err.to_string();
467				assert!(err_msg.contains("Last line of output is not a valid boolean"));
468			}
469			_ => panic!("Expected error"),
470		}
471	}
472
473	#[tokio::test]
474	async fn test_bash_script_executor_success() {
475		let script_content = r#"
476#!/bin/bash
477set -e  # Exit on any error
478input_json=$(cat)
479sleep 0.1  # Small delay to ensure process startup
480echo "debugging..."
481echo "true"
482"#;
483		let executor = BashScriptExecutor {
484			script_content: script_content.to_string(),
485		};
486
487		let input = create_mock_monitor_match();
488		let result = executor.execute(input, &1000, None, false).await;
489		assert!(result.is_ok());
490		assert!(result.unwrap());
491	}
492
493	#[tokio::test]
494	async fn test_bash_script_executor_invalid_output() {
495		let script_content = r#"
496#!/bin/bash
497set -e  # Exit on any error
498input_json=$(cat)
499sleep 0.1  # Small delay to ensure process startup
500echo "debugging..."
501echo "not a boolean"
502"#;
503
504		let executor = BashScriptExecutor {
505			script_content: script_content.to_string(),
506		};
507
508		let input = create_mock_monitor_match();
509		let result = executor.execute(input, &1000, None, false).await;
510		assert!(result.is_err());
511		match result {
512			Err(e) => {
513				assert!(e
514					.to_string()
515					.contains("Last line of output is not a valid boolean"));
516			}
517			Ok(_) => {
518				panic!("Expected ParseError, got success");
519			}
520		}
521	}
522
523	#[tokio::test]
524	async fn test_script_executor_empty_output() {
525		let script_content = r#"
526import sys
527input_json = sys.stdin.read()
528# This script produces no output
529"#;
530
531		let executor = PythonScriptExecutor {
532			script_content: script_content.to_string(),
533		};
534
535		let input = create_mock_monitor_match();
536		let result = executor.execute(input, &1000, None, false).await;
537
538		match result {
539			Err(e) => {
540				assert!(e.to_string().contains("Script produced no output"));
541			}
542			_ => panic!("Expected error"),
543		}
544	}
545
546	#[tokio::test]
547	async fn test_script_executor_whitespace_output() {
548		let script_content = r#"
549import sys
550input_json = sys.stdin.read()
551print("   ")
552print("     true    ")  # Should handle whitespace correctly
553"#;
554
555		let executor = PythonScriptExecutor {
556			script_content: script_content.to_string(),
557		};
558
559		let input = create_mock_monitor_match();
560		let result = executor.execute(input, &1000, None, false).await;
561		assert!(result.is_ok());
562		assert!(result.unwrap());
563	}
564
565	#[tokio::test]
566	async fn test_script_executor_invalid_json_input() {
567		let script_content = r#"
568	import sys
569	import json
570
571	input_json = sys.argv[1]
572	data = json.loads(input_json)
573	print("true")
574	print("Invalid JSON input")
575	exit(1)
576	"#;
577
578		let executor = PythonScriptExecutor {
579			script_content: script_content.to_string(),
580		};
581
582		// Create an invalid MonitorMatch that will fail JSON serialization
583		let input = create_mock_monitor_match();
584
585		let result = executor.execute(input, &1000, None, false).await;
586		assert!(result.is_err());
587	}
588
589	#[tokio::test]
590	async fn test_script_executor_with_multiple_lines_of_output() {
591		let script_content = r#"
592import sys
593import json
594
595# Read from stdin instead of command line arguments
596input_json = sys.stdin.read()
597data = json.loads(input_json)
598print("debugging...")
599print("false")
600print("true")
601print("false")
602print("true")
603"#;
604
605		let executor = PythonScriptExecutor {
606			script_content: script_content.to_string(),
607		};
608
609		let input = create_mock_monitor_match();
610
611		let result = executor.execute(input, &1000, None, false).await;
612		assert!(result.is_ok());
613		assert!(result.unwrap());
614	}
615
616	#[tokio::test]
617	async fn test_python_script_executor_monitor_match_fields() {
618		let script_content = r#"
619import sys
620import json
621
622input_json = sys.stdin.read()
623data = json.loads(input_json)
624
625monitor_match = data['monitor_match']
626# Verify it's an EVM match type
627if monitor_match['EVM']:
628	block_number = monitor_match['EVM']['transaction']['blockNumber']
629	if block_number:
630		print("true")
631	else:
632		print("false")
633else:
634    print("false")
635"#;
636
637		let executor = PythonScriptExecutor {
638			script_content: script_content.to_string(),
639		};
640
641		let input = create_mock_monitor_match();
642		let result = executor.execute(input, &1000, None, false).await;
643		assert!(!result.unwrap());
644	}
645
646	#[tokio::test]
647	async fn test_python_script_executor_with_args() {
648		let script_content = r#"
649import sys
650import json
651
652input_json = sys.stdin.read()
653data = json.loads(input_json)
654
655# Verify both fields exist
656if 'monitor_match' not in data or 'args' not in data:
657    print("false")
658    exit(1)
659
660# Test args parsing
661args = data['args']
662if "--verbose" in args:
663    print("true")
664else:
665    print("false")
666"#;
667
668		let executor = PythonScriptExecutor {
669			script_content: script_content.to_string(),
670		};
671
672		let input = create_mock_monitor_match();
673
674		// Test with matching argument
675		let args = vec![String::from("test_argument")];
676		let result = executor
677			.execute(input.clone(), &1000, Some(&args), false)
678			.await;
679		assert!(result.is_ok());
680		assert!(!result.unwrap());
681
682		// Test with non-matching argument
683		let args = vec![String::from("--verbose"), String::from("--other-arg")];
684		let result = executor
685			.execute(input.clone(), &1000, Some(&args), false)
686			.await;
687		assert!(result.is_ok());
688		assert!(result.unwrap());
689	}
690
691	#[tokio::test]
692	async fn test_python_script_executor_combined_fields() {
693		let script_content = r#"
694import sys
695import json
696
697input_json = sys.stdin.read()
698data = json.loads(input_json)
699
700monitor_match = data['monitor_match']
701args = data['args']
702
703# Test both monitor_match and args together
704expected_args = ["--verbose", "--specific_arg", "--test"]
705if (monitor_match['EVM'] and
706    args == expected_args):
707    print("true")
708else:
709    print("false")
710"#;
711
712		let executor = PythonScriptExecutor {
713			script_content: script_content.to_string(),
714		};
715
716		let input = create_mock_monitor_match();
717
718		// Test with correct combination
719		let args = vec![
720			String::from("--verbose"),
721			String::from("--specific_arg"),
722			String::from("--test"),
723		];
724		let result = executor
725			.execute(input.clone(), &1000, Some(&args), false)
726			.await;
727		assert!(result.is_ok());
728		assert!(result.unwrap());
729
730		// Test with wrong argument
731		let args = vec![String::from("wrong_arg")];
732		let result = executor
733			.execute(input.clone(), &1000, Some(&args), false)
734			.await;
735		assert!(result.is_ok());
736		assert!(!result.unwrap());
737	}
738
739	#[tokio::test]
740	async fn test_python_script_executor_with_verbose_arg() {
741		let script_content = read_fixture("evm_filter_by_arguments.py");
742		let executor = PythonScriptExecutor { script_content };
743		let input = create_mock_monitor_match();
744		let args = vec![String::from("--verbose")];
745		let result = executor
746			.execute(input.clone(), &1000, Some(&args), false)
747			.await;
748
749		assert!(result.is_ok());
750		assert!(result.unwrap());
751	}
752
753	#[tokio::test]
754	async fn test_python_script_executor_with_wrong_arg() {
755		let script_content = read_fixture("evm_filter_by_arguments.py");
756		let executor = PythonScriptExecutor { script_content };
757
758		let input = create_mock_monitor_match();
759		let args = vec![String::from("--wrong_arg"), String::from("--test")];
760		let result = executor
761			.execute(input.clone(), &1000, Some(&args), false)
762			.await;
763
764		assert!(result.is_ok());
765		assert!(!result.unwrap());
766	}
767
768	#[tokio::test]
769	async fn test_script_executor_with_ignore_output() {
770		let script_content = r#"
771import sys
772input_json = sys.stdin.read()
773"#;
774
775		let executor = PythonScriptExecutor {
776			script_content: script_content.to_string(),
777		};
778
779		let input = create_mock_monitor_match();
780		let result = executor.execute(input, &1000, None, true).await;
781		assert!(result.is_ok());
782		assert!(result.unwrap());
783	}
784
785	#[tokio::test]
786	async fn test_script_executor_with_non_zero_exit() {
787		let script_content = r#"
788import sys
789input_json = sys.stdin.read()
790sys.stderr.write("Error: something went wrong\n")
791sys.exit(1)
792		"#;
793
794		let executor = PythonScriptExecutor {
795			script_content: script_content.to_string(),
796		};
797
798		let input = create_mock_monitor_match();
799		let result = executor.execute(input, &1000, None, true).await;
800
801		assert!(result.is_err());
802		match result {
803			Err(e) => {
804				assert!(e
805					.to_string()
806					.contains("Script execution failed: Error: something went wrong"));
807			}
808			_ => panic!("Expected ExecutionError"),
809		}
810	}
811
812	#[tokio::test]
813	async fn test_script_notify_succeeds_within_timeout() {
814		let script_content = r#"
815import sys
816import time
817input_json = sys.stdin.read()
818time.sleep(0.3)
819		"#;
820
821		let executor = PythonScriptExecutor {
822			script_content: script_content.to_string(),
823		};
824
825		let input = create_mock_monitor_match();
826		let start_time = Instant::now();
827		let result = executor.execute(input, &1000, None, true).await;
828		let elapsed = start_time.elapsed();
829
830		assert!(result.is_ok());
831		// Verify that execution took at least 300ms (the sleep time)
832		assert!(elapsed.as_millis() >= 300);
833		// Verify that execution took less than the timeout
834		assert!(elapsed.as_millis() < 1000);
835	}
836
837	#[tokio::test]
838	async fn test_script_notify_fails_within_timeout() {
839		let script_content = r#"
840import sys
841import time
842input_json = sys.stdin.read()
843time.sleep(0.5)
844		"#;
845
846		let executor = PythonScriptExecutor {
847			script_content: script_content.to_string(),
848		};
849
850		let input = create_mock_monitor_match();
851		let start_time = Instant::now();
852		let result = executor.execute(input, &400, None, true).await;
853		let elapsed = start_time.elapsed();
854
855		assert!(result.is_err());
856		// Verify that execution took at least 300ms (the sleep time)
857		assert!(elapsed.as_millis() >= 400 && elapsed.as_millis() < 600);
858	}
859
860	#[tokio::test]
861	async fn test_script_python_fails_with_non_zero_exit() {
862		let script_content = r#"
863import sys
864import time
865input_json = sys.stdin.read()
866print("This is a python test error message!", file=sys.stderr)
867sys.exit(1)
868		"#;
869
870		let executor = PythonScriptExecutor {
871			script_content: script_content.to_string(),
872		};
873
874		let input = create_mock_monitor_match();
875		let result = executor.execute(input, &1000, None, false).await;
876
877		assert!(result.is_err());
878		match result {
879			Err(e) => {
880				assert!(e
881					.to_string()
882					.contains("Script execution failed: This is a python test error message!"));
883			}
884			_ => panic!("Expected ExecutionError"),
885		}
886	}
887
888	#[tokio::test]
889	async fn test_script_javascript_fails_with_non_zero_exit() {
890		let script_content = r#"
891		// Read input from stdin
892		let input = '';
893		process.stdin.on('data', (chunk) => {
894			input += chunk;
895		});
896
897		process.stdin.on('end', () => {
898			// Parse and validate input
899			try {
900				const data = JSON.parse(input);
901				console.error("This is a JS test error message!");
902				process.exit(1);
903			} catch (err) {
904				console.error(err);
905				process.exit(1);
906			}
907		});
908		"#;
909
910		let executor = JavaScriptScriptExecutor {
911			script_content: script_content.to_string(),
912		};
913
914		let input = create_mock_monitor_match();
915		let result = executor.execute(input, &1000, None, false).await;
916
917		assert!(result.is_err());
918		match result {
919			Err(e) => {
920				assert!(e
921					.to_string()
922					.contains("Script execution failed: This is a JS test error message!"));
923			}
924			_ => panic!("Expected ExecutionError"),
925		}
926	}
927
928	#[tokio::test]
929	async fn test_script_bash_fails_with_non_zero_exit() {
930		let script_content = r#"
931#!/bin/bash
932# Read input from stdin
933input_json=$(cat)
934echo "This is a bash test error message!" >&2
935exit 1
936"#;
937
938		let executor = BashScriptExecutor {
939			script_content: script_content.to_string(),
940		};
941
942		let input = create_mock_monitor_match();
943		let result = executor.execute(input, &1000, None, false).await;
944		assert!(result.is_err());
945		match result {
946			Err(e) => {
947				assert!(e
948					.to_string()
949					.contains("Script execution failed: This is a bash test error message!"));
950			}
951			_ => panic!("Expected ExecutionError"),
952		}
953	}
954}