@@ -613,13 +613,23 @@ def capture_and_stop(*args, **kwargs):
613613 captured ["kwargs" ] = kwargs
614614 raise _StopHere ("captured Popen args" )
615615
616+ backend = MagicMock ()
617+ backend .prepare_web_attach .return_value = [
618+ "tmux" ,
619+ "-u" ,
620+ "attach-session" ,
621+ "-t" ,
622+ "cao-s:w" ,
623+ ]
624+
616625 with (
617626 patch .dict ("os.environ" , {"TERM" : "dumb" , "HOME" : "/root" }, clear = True ),
618627 patch .object (
619628 main_module ,
620629 "get_terminal_metadata" ,
621630 return_value = {"tmux_session" : "cao-s" , "tmux_window" : "w" },
622631 ),
632+ patch .object (main_module , "get_backend" , return_value = backend ),
623633 patch .object (main_module .subprocess , "Popen" , side_effect = capture_and_stop ),
624634 patch .object (main_module .pty , "openpty" , return_value = (100 , 101 )),
625635 patch .object (main_module .fcntl , "ioctl" ),
@@ -636,6 +646,72 @@ def capture_and_stop(*args, **kwargs):
636646 )
637647 assert passed_env ["TERM" ] == "xterm-256color"
638648
649+ @pytest .mark .asyncio
650+ async def test_websocket_uses_configured_backend_attach_command (self ):
651+ """Herdr-backed terminals must not be attached through tmux."""
652+ from cli_agent_orchestrator .api import main as main_module
653+
654+ ws = MagicMock ()
655+ ws .client = MagicMock (host = "127.0.0.1" )
656+ ws .accept = AsyncMock ()
657+ ws .close = AsyncMock ()
658+
659+ backend = MagicMock ()
660+ backend .prepare_web_attach .return_value = ["herdr" , "--session" , "cao" ]
661+
662+ captured : Dict [str , object ] = {}
663+
664+ def capture_and_stop (* args , ** kwargs ):
665+ captured ["args" ] = args
666+ raise _StopHere ("captured Popen args" )
667+
668+ with (
669+ patch .object (
670+ main_module ,
671+ "get_terminal_metadata" ,
672+ return_value = {"tmux_session" : "cao-s" , "tmux_window" : "w" },
673+ ),
674+ patch .object (main_module , "get_backend" , return_value = backend ),
675+ patch .object (main_module .subprocess , "Popen" , side_effect = capture_and_stop ),
676+ patch .object (main_module .pty , "openpty" , return_value = (100 , 101 )),
677+ patch .object (main_module .fcntl , "ioctl" ),
678+ patch .object (main_module .fcntl , "fcntl" ),
679+ patch .object (main_module .os , "close" ),
680+ ):
681+ with pytest .raises (_StopHere ):
682+ await main_module .terminal_ws (ws , "abcd1234" )
683+
684+ backend .prepare_web_attach .assert_called_once_with ("cao-s" , "w" )
685+ assert captured ["args" ][0 ] == ["herdr" , "--session" , "cao" ]
686+
687+ @pytest .mark .asyncio
688+ async def test_websocket_closes_safely_when_backend_attach_fails (self ):
689+ """Backend attach errors close safely before allocating a PTY."""
690+ from cli_agent_orchestrator .api import main as main_module
691+ from cli_agent_orchestrator .backends import TerminalBackendError
692+
693+ ws = MagicMock ()
694+ ws .client = MagicMock (host = "127.0.0.1" )
695+ ws .accept = AsyncMock ()
696+ ws .close = AsyncMock ()
697+
698+ backend = MagicMock ()
699+ backend .prepare_web_attach .side_effect = TerminalBackendError ("sensitive backend detail" )
700+
701+ with (
702+ patch .object (
703+ main_module ,
704+ "get_terminal_metadata" ,
705+ return_value = {"tmux_session" : "cao-s" , "tmux_window" : "w" },
706+ ),
707+ patch .object (main_module , "get_backend" , return_value = backend ),
708+ patch .object (main_module .pty , "openpty" ) as mock_openpty ,
709+ ):
710+ await main_module .terminal_ws (ws , "abcd1234" )
711+
712+ ws .close .assert_awaited_once_with (code = 4004 , reason = "Failed to attach terminal" )
713+ mock_openpty .assert_not_called ()
714+
639715
640716class _StopHere (Exception ):
641717 """Sentinel raised by the wiring test once Popen args are captured."""
0 commit comments