@@ -415,6 +415,167 @@ async fn test_exec_bytestream_behavior() {
415415 assert ! ( !output_str. starts_with( '{' ) ) ; // Not a JSON object
416416}
417417
418+ #[ tokio:: test]
419+ async fn test_exec_cat_streaming ( ) {
420+ let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
421+ let store_path = temp_dir. path ( ) ;
422+
423+ let mut child = spawn_xs_supervisor ( store_path) . await ;
424+
425+ let sock_path = store_path. join ( "sock" ) ;
426+ let start = std:: time:: Instant :: now ( ) ;
427+ while !sock_path. exists ( ) {
428+ if start. elapsed ( ) > Duration :: from_secs ( 5 ) {
429+ panic ! ( "Timeout waiting for sock file" ) ;
430+ }
431+ tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
432+ }
433+ tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
434+
435+ // Append initial test data
436+ cmd ! ( cargo_bin( "xs" ) , "append" , store_path, "stream.test" )
437+ . stdin_bytes ( b"initial" )
438+ . run ( )
439+ . unwrap ( ) ;
440+
441+ // Test 1: .cat without --follow (snapshot mode)
442+ let output = cmd ! (
443+ cargo_bin( "xs" ) ,
444+ "exec" ,
445+ store_path,
446+ ".cat --topic stream.test"
447+ )
448+ . read ( )
449+ . unwrap ( ) ;
450+
451+ let frames: Vec < serde_json:: Value > = output
452+ . lines ( )
453+ . map ( |l| serde_json:: from_str ( l) . unwrap ( ) )
454+ . collect ( ) ;
455+ assert_eq ! ( frames. len( ) , 1 ) ;
456+ assert_eq ! ( frames[ 0 ] [ "topic" ] , "stream.test" ) ;
457+
458+ // Test 2: .cat --follow streams new frames
459+ let mut follow_child = tokio:: process:: Command :: new ( cargo_bin ( "xs" ) )
460+ . arg ( "exec" )
461+ . arg ( store_path)
462+ . arg ( ".cat --topic stream.test --follow" )
463+ . stdout ( std:: process:: Stdio :: piped ( ) )
464+ . spawn ( )
465+ . unwrap ( ) ;
466+
467+ let stdout = follow_child. stdout . take ( ) . unwrap ( ) ;
468+ let mut reader = tokio:: io:: BufReader :: new ( stdout) ;
469+
470+ // Read initial frame (historical)
471+ let mut line = String :: new ( ) ;
472+ let result = tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , reader. read_line ( & mut line) )
473+ . await
474+ . expect ( "Timeout reading initial frame" )
475+ . expect ( "Failed to read initial frame" ) ;
476+ assert ! ( result > 0 , "Should read initial frame" ) ;
477+ let initial_frame: serde_json:: Value = serde_json:: from_str ( & line. trim ( ) ) . unwrap ( ) ;
478+ assert_eq ! ( initial_frame[ "topic" ] , "stream.test" ) ;
479+
480+ // Read threshold frame (indicates caught up to real-time)
481+ line. clear ( ) ;
482+ let result = tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , reader. read_line ( & mut line) )
483+ . await
484+ . expect ( "Timeout reading threshold" )
485+ . expect ( "Failed to read threshold" ) ;
486+ assert ! ( result > 0 , "Should read threshold frame" ) ;
487+ let threshold_frame: serde_json:: Value = serde_json:: from_str ( & line. trim ( ) ) . unwrap ( ) ;
488+ assert_eq ! (
489+ threshold_frame[ "topic" ] , "xs.threshold" ,
490+ "Should receive threshold frame indicating caught up"
491+ ) ;
492+
493+ // Append new frame while following
494+ cmd ! ( cargo_bin( "xs" ) , "append" , store_path, "stream.test" )
495+ . stdin_bytes ( b"streamed" )
496+ . run ( )
497+ . unwrap ( ) ;
498+
499+ // Should receive new frame via streaming
500+ line. clear ( ) ;
501+ let result = tokio:: time:: timeout ( Duration :: from_secs ( 2 ) , reader. read_line ( & mut line) )
502+ . await
503+ . expect ( "Timeout reading streamed frame" )
504+ . expect ( "Failed to read streamed frame" ) ;
505+ assert ! ( result > 0 , "Should read streamed frame" ) ;
506+ let streamed_frame: serde_json:: Value = serde_json:: from_str ( & line. trim ( ) ) . unwrap ( ) ;
507+ assert_eq ! ( streamed_frame[ "topic" ] , "stream.test" ) ;
508+
509+ // Test 3: .cat --tail starts at end (skip for now - can block)
510+ follow_child. kill ( ) . await . unwrap ( ) ;
511+
512+ // Test 4: .cat --limit respects limit
513+ let output = cmd ! (
514+ cargo_bin( "xs" ) ,
515+ "exec" ,
516+ store_path,
517+ ".cat --topic stream.test --limit 1"
518+ )
519+ . read ( )
520+ . unwrap ( ) ;
521+
522+ eprintln ! (
523+ "Output from .cat --topic stream.test --limit 1: '{}'" ,
524+ output
525+ ) ;
526+ assert ! ( !output. trim( ) . is_empty( ) , "Output should not be empty" ) ;
527+
528+ let frames: Vec < serde_json:: Value > = output
529+ . lines ( )
530+ . filter ( |l| !l. is_empty ( ) )
531+ . map ( |l| serde_json:: from_str ( l) . expect ( & format ! ( "Failed to parse JSON: {}" , l) ) )
532+ . collect ( ) ;
533+ assert_eq ! ( frames. len( ) , 1 , "Should respect --limit flag" ) ;
534+
535+ // Test 5: .cat --detail includes context_id and ttl
536+ let output = cmd ! (
537+ cargo_bin( "xs" ) ,
538+ "exec" ,
539+ store_path,
540+ ".cat --topic stream.test --limit 1 --detail"
541+ )
542+ . read ( )
543+ . unwrap ( ) ;
544+
545+ let frame: serde_json:: Value = serde_json:: from_str ( output. lines ( ) . next ( ) . unwrap ( ) ) . unwrap ( ) ;
546+ assert ! (
547+ frame. get( "context_id" ) . is_some( ) ,
548+ "Should include context_id with --detail"
549+ ) ;
550+ assert ! (
551+ frame. get( "ttl" ) . is_some( ) ,
552+ "Should include ttl with --detail"
553+ ) ;
554+
555+ // Test 6: Without --detail, context_id and ttl are filtered
556+ let output = cmd ! (
557+ cargo_bin( "xs" ) ,
558+ "exec" ,
559+ store_path,
560+ ".cat --topic stream.test --limit 1"
561+ )
562+ . read ( )
563+ . unwrap ( ) ;
564+
565+ let frame: serde_json:: Value = serde_json:: from_str ( output. lines ( ) . next ( ) . unwrap ( ) ) . unwrap ( ) ;
566+ assert ! (
567+ frame. get( "context_id" ) . is_none( ) ,
568+ "Should not include context_id without --detail"
569+ ) ;
570+ assert ! (
571+ frame. get( "ttl" ) . is_none( ) ,
572+ "Should not include ttl without --detail"
573+ ) ;
574+
575+ // Clean up
576+ child. kill ( ) . await . unwrap ( ) ;
577+ }
578+
418579#[ tokio:: test]
419580async fn test_iroh_networking ( ) {
420581 let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temp dir" ) ;
0 commit comments