@@ -901,6 +901,113 @@ mod tests {
901901 }
902902 }
903903
904+ #[ test]
905+ fn handle_terminal_channel_attach_returns_correct_replay_data ( ) {
906+ #[ cfg( test) ]
907+ {
908+ use serde_json:: Value ;
909+
910+ let ( app, _shutdown_rx) = RuntimeHandle :: new ( ) ;
911+ let conn = rusqlite:: Connection :: open_in_memory ( ) . unwrap ( ) ;
912+ crate :: init_db ( & conn) . unwrap ( ) ;
913+ * app. state ( ) . db . lock ( ) . unwrap ( ) = Some ( conn) ;
914+
915+ // Register a gateway runtime
916+ app. state ( ) . terminal_runtimes . lock ( ) . unwrap ( ) . insert (
917+ GatewayTerminalRuntime :: new (
918+ "runtime-1" . to_string ( ) ,
919+ "ws-1" . to_string ( ) ,
920+ "session-1" . to_string ( ) ,
921+ "claude" . to_string ( ) ,
922+ "tmux-fake" . to_string ( ) ,
923+ "%1" . to_string ( ) ,
924+ ) ,
925+ ) ;
926+
927+ // Set up session -> terminal binding
928+ let terminal_id = 42u64 ;
929+ app. state ( ) . session_runtime_bindings . lock ( ) . unwrap ( ) . insert (
930+ crate :: services:: session_runtime:: session_runtime_key ( "ws-1" , "session-1" ) ,
931+ terminal_id,
932+ ) ;
933+ app. state ( ) . terminal_runtime_bindings . lock ( ) . unwrap ( ) . insert (
934+ terminal_id,
935+ crate :: services:: session_runtime:: session_runtime_key ( "ws-1" , "session-1" ) ,
936+ ) ;
937+
938+ // Seed the output buffer with known data
939+ let expected_data = "hello from replay" ;
940+ let terminal_runtime = Arc :: new ( TerminalRuntime {
941+ io : TerminalIo :: Mock ,
942+ output : Mutex :: new ( expected_data. to_string ( ) ) ,
943+ size : Mutex :: new ( ( 80u16 , 24u16 ) ) ,
944+ persist_workspace_terminal : true ,
945+ child : None ,
946+ killer : None ,
947+ process_id : None ,
948+ process_group_leader : None ,
949+ } ) ;
950+ app. state ( ) . terminals . lock ( ) . unwrap ( ) . insert (
951+ crate :: ws:: server:: terminal_key ( "ws-1" , terminal_id) ,
952+ terminal_runtime,
953+ ) ;
954+
955+ let payload = serde_json:: json!( {
956+ "workspace_id" : "ws-1" ,
957+ "fencing_token" : 1 ,
958+ "runtime_id" : "runtime-1" ,
959+ } ) ;
960+
961+ let result = super :: handle_terminal_channel_attach ( & app, payload) ;
962+ assert ! ( result. is_ok( ) , "attach should succeed: {:?}" , result) ;
963+
964+ let captured = app. state ( ) . captured_transport_events . lock ( ) . unwrap ( ) . clone ( ) ;
965+ let replay_events: Vec < _ > = captured
966+ . iter ( )
967+ . filter ( |( event, payload) | {
968+ * event == "terminal://channel_replay"
969+ && payload. get ( "data" ) . and_then ( Value :: as_str) == Some ( expected_data)
970+ } )
971+ . collect ( ) ;
972+ assert ! (
973+ !replay_events. is_empty( ) ,
974+ "expected terminal://channel_replay with data='{}', got: {:?}" ,
975+ expected_data,
976+ captured
977+ ) ;
978+ }
979+ }
980+
981+ #[ test]
982+ fn handle_terminal_channel_attach_returns_error_for_unknown_runtime ( ) {
983+ #[ cfg( test) ]
984+ {
985+ let ( app, _shutdown_rx) = RuntimeHandle :: new ( ) ;
986+ let conn = rusqlite:: Connection :: open_in_memory ( ) . unwrap ( ) ;
987+ crate :: init_db ( & conn) . unwrap ( ) ;
988+ * app. state ( ) . db . lock ( ) . unwrap ( ) = Some ( conn) ;
989+
990+ // No gateway runtime registered — use a non-existent runtime_id
991+ let payload = serde_json:: json!( {
992+ "workspace_id" : "ws-1" ,
993+ "fencing_token" : 1 ,
994+ "runtime_id" : "nonexistent-runtime" ,
995+ } ) ;
996+
997+ let result = super :: handle_terminal_channel_attach ( & app, payload) ;
998+ assert ! (
999+ result. is_err( ) ,
1000+ "attach with unknown runtime_id should fail, got: {:?}" ,
1001+ result
1002+ ) ;
1003+ assert_eq ! (
1004+ result. unwrap_err( ) ,
1005+ "terminal_runtime_not_found" ,
1006+ "expected 'terminal_runtime_not_found' error"
1007+ ) ;
1008+ }
1009+ }
1010+
9041011 #[ test]
9051012 fn ws_client_envelope_parses_terminal_channel_attach ( ) {
9061013 let raw = serde_json:: json!( {
0 commit comments