@@ -342,3 +342,196 @@ fn now_unix_ms() -> i64 {
342342 . unwrap_or ( 0 )
343343}
344344
345+ #[ cfg( test) ]
346+ mod tests {
347+ use super :: * ;
348+ use std:: sync:: Mutex ;
349+ use std:: time:: Duration ;
350+
351+ use prost:: Message ;
352+ use serde:: Deserialize ;
353+ use taurus_core:: runtime:: engine:: ExecutionEngine ;
354+ use taurus_provider:: providers:: emitter:: nats_emitter:: NATSRespondEmitter ;
355+ use taurus_provider:: providers:: remote:: nats_remote_runtime:: NATSRemoteRuntime ;
356+ use tucana:: shared:: {
357+ ValidationFlow , execution_result,
358+ helper:: value:: { from_json_value, to_json_value} ,
359+ } ;
360+
361+ #[ derive( Deserialize ) ]
362+ struct FixtureInput {
363+ input : Option < serde_json:: Value > ,
364+ expected_result : serde_json:: Value ,
365+ }
366+
367+ #[ derive( Deserialize ) ]
368+ struct FlowFixture {
369+ inputs : Vec < FixtureInput > ,
370+ flow : ValidationFlow ,
371+ }
372+
373+ static NATS_TEST_LOCK : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
374+
375+ #[ test]
376+ #[ ignore = "requires a running NATS server at NATS_URL or nats://127.0.0.1:4222" ]
377+ fn test_execution_request_returns_execution_result_over_nats ( ) {
378+ let _lock = NATS_TEST_LOCK
379+ . lock ( )
380+ . expect ( "NATS test lock should not be poisoned" ) ;
381+ runtime ( ) . block_on ( async {
382+ let client = connect_test_nats ( ) . await ;
383+ let worker = spawn_test_worker ( client. clone ( ) ) ;
384+ wait_for_worker_subscription ( ) . await ;
385+
386+ let execution_id = ExecutionId :: new_v4 ( ) ;
387+ let fixture = load_fixture ( "flows/01_return_object.json" ) ;
388+ let expected_result = fixture. inputs [ 0 ] . expected_result . clone ( ) ;
389+ let flow = execution_flow_from_fixture ( fixture) ;
390+ let response = request_execution_result ( & client, execution_id, flow. encode_to_vec ( ) )
391+ . await
392+ . expect ( "test execution request should receive an ExecutionResult response" ) ;
393+
394+ worker. abort ( ) ;
395+
396+ assert_eq ! ( response. execution_identifier, execution_id. to_string( ) ) ;
397+ assert_eq ! ( response. flow_id, flow. flow_id) ;
398+ assert ! ( response. started_at > 0 ) ;
399+ assert ! ( response. finished_at >= response. started_at) ;
400+ assert_eq ! ( response. input, None ) ;
401+ assert ! ( response. node_execution_results. is_empty( ) ) ;
402+
403+ match response. result {
404+ Some ( execution_result:: Result :: Success ( value) ) => {
405+ assert_eq ! ( to_json_value( value) , expected_result) ;
406+ }
407+ other => panic ! ( "expected successful test execution result, got {:?}" , other) ,
408+ }
409+ } ) ;
410+ }
411+
412+ #[ test]
413+ #[ ignore = "requires a running NATS server at NATS_URL or nats://127.0.0.1:4222" ]
414+ fn test_execution_request_returns_decode_error_over_nats ( ) {
415+ let _lock = NATS_TEST_LOCK
416+ . lock ( )
417+ . expect ( "NATS test lock should not be poisoned" ) ;
418+ runtime ( ) . block_on ( async {
419+ let client = connect_test_nats ( ) . await ;
420+ let worker = spawn_test_worker ( client. clone ( ) ) ;
421+ wait_for_worker_subscription ( ) . await ;
422+
423+ let execution_id = ExecutionId :: new_v4 ( ) ;
424+ let response =
425+ request_execution_result ( & client, execution_id, b"not protobuf" . to_vec ( ) )
426+ . await
427+ . expect ( "malformed test execution request should receive an error response" ) ;
428+
429+ worker. abort ( ) ;
430+
431+ assert_eq ! ( response. execution_identifier, execution_id. to_string( ) ) ;
432+ assert_eq ! ( response. flow_id, 0 ) ;
433+ assert ! ( response. started_at > 0 ) ;
434+ assert ! ( response. finished_at >= response. started_at) ;
435+ assert_eq ! ( response. input, None ) ;
436+ assert ! ( response. node_execution_results. is_empty( ) ) ;
437+
438+ match response. result {
439+ Some ( execution_result:: Result :: Error ( error) ) => {
440+ assert_eq ! ( error. code, "T-TAURUS-000001" ) ;
441+ assert_eq ! ( error. category, "ExecutionFlowDecodeError" ) ;
442+ assert_eq ! (
443+ error. message,
444+ "Failed to decode test execution flow payload"
445+ ) ;
446+ }
447+ other => panic ! ( "expected decode error result, got {:?}" , other) ,
448+ }
449+ } ) ;
450+ }
451+
452+ fn runtime ( ) -> tokio:: runtime:: Runtime {
453+ tokio:: runtime:: Builder :: new_multi_thread ( )
454+ . enable_all ( )
455+ . build ( )
456+ . expect ( "test runtime should build" )
457+ }
458+
459+ async fn connect_test_nats ( ) -> async_nats:: Client {
460+ let nats_url =
461+ std:: env:: var ( "NATS_URL" ) . unwrap_or_else ( |_| "nats://127.0.0.1:4222" . to_string ( ) ) ;
462+ async_nats:: connect ( & nats_url)
463+ . await
464+ . unwrap_or_else ( |err| panic ! ( "failed to connect to NATS at {nats_url}: {err}" ) )
465+ }
466+
467+ fn spawn_test_worker ( client : async_nats:: Client ) -> tokio:: task:: JoinHandle < ( ) > {
468+ let engine = ExecutionEngine :: new ( ) ;
469+ let nats_remote = NATSRemoteRuntime :: new ( client. clone ( ) ) ;
470+ let runtime_emitter = NATSRespondEmitter :: new ( client. clone ( ) ) ;
471+ spawn_worker ( client, engine, nats_remote, runtime_emitter, None )
472+ }
473+
474+ async fn wait_for_worker_subscription ( ) {
475+ tokio:: time:: sleep ( Duration :: from_millis ( 250 ) ) . await ;
476+ }
477+
478+ async fn request_execution_result (
479+ client : & async_nats:: Client ,
480+ execution_id : ExecutionId ,
481+ payload : Vec < u8 > ,
482+ ) -> Result < ExecutionResult , String > {
483+ let subject = format ! ( "test_executions.{execution_id}" ) ;
484+
485+ for attempt in 1 ..=10 {
486+ match tokio:: time:: timeout (
487+ Duration :: from_secs ( 2 ) ,
488+ client. request ( subject. clone ( ) , payload. clone ( ) . into ( ) ) ,
489+ )
490+ . await
491+ {
492+ Ok ( Ok ( message) ) => {
493+ return ExecutionResult :: decode ( & * message. payload )
494+ . map_err ( |err| format ! ( "failed to decode ExecutionResult: {err}" ) ) ;
495+ }
496+ Ok ( Err ( err) ) if attempt < 10 => {
497+ log:: debug!(
498+ "test execution request attempt {} failed before subscription was ready: {:?}" ,
499+ attempt,
500+ err
501+ ) ;
502+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
503+ }
504+ Ok ( Err ( err) ) => return Err ( format ! ( "NATS request failed: {err}" ) ) ,
505+ Err ( _) if attempt < 10 => {
506+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
507+ }
508+ Err ( _) => return Err ( "timed out waiting for test execution response" . to_string ( ) ) ,
509+ }
510+ }
511+
512+ Err ( "test execution request did not complete" . to_string ( ) )
513+ }
514+
515+ fn load_fixture ( path : & str ) -> FlowFixture {
516+ let path = std:: path:: Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) )
517+ . join ( "../.." )
518+ . join ( path) ;
519+ let content = std:: fs:: read_to_string ( & path)
520+ . unwrap_or_else ( |err| panic ! ( "failed to read fixture {}: {err}" , path. display( ) ) ) ;
521+ serde_json:: from_str ( & content)
522+ . unwrap_or_else ( |err| panic ! ( "failed to parse fixture {}: {err}" , path. display( ) ) )
523+ }
524+
525+ fn execution_flow_from_fixture ( fixture : FlowFixture ) -> ExecutionFlow {
526+ ExecutionFlow {
527+ flow_id : fixture. flow . flow_id ,
528+ project_id : fixture. flow . project_id ,
529+ starting_node_id : fixture. flow . starting_node_id ,
530+ node_functions : fixture. flow . node_functions ,
531+ input_value : fixture
532+ . inputs
533+ . first ( )
534+ . and_then ( |input| input. input . clone ( ) . map ( from_json_value) ) ,
535+ }
536+ }
537+ }
0 commit comments