@@ -636,6 +636,45 @@ def test_run_shell_command_retry_for_sudo_password(
636636 get_pty = False ,
637637 )
638638
639+ @mock .patch ("pyinfra.connectors.ssh.SSHClient" )
640+ @mock .patch ("pyinfra.connectors.util.getpass" )
641+ def test_run_shell_command_retry_for_sudo_rs_password (
642+ self ,
643+ fake_getpass ,
644+ fake_ssh_client ,
645+ ):
646+ # sudo-rs (the Rust replacement, default in Ubuntu 25.10+) prints a different message
647+ # when it cannot prompt non-interactively; the retry path should recognize it too.
648+ fake_getpass .return_value = "PASSWORD"
649+
650+ fake_ssh = mock .MagicMock ()
651+ fake_stdin = mock .MagicMock ()
652+ fake_stdout = mock .MagicMock ()
653+ fake_stderr = ["sudo-rs: interactive authentication is required" ]
654+ fake_ssh .exec_command .return_value = fake_stdin , fake_stdout , fake_stderr
655+
656+ fake_ssh_client .return_value = fake_ssh
657+
658+ inventory = make_inventory (hosts = ("somehost" ,))
659+ state = State (inventory , Config ())
660+ host = inventory .get_host ("somehost" )
661+ host .connect (state )
662+ host .connector_data ["sudo_askpass_path" ] = "/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX"
663+
664+ command = "echo hi"
665+ return_values = [1 , 0 ] # return 0 on the second call
666+ fake_stdout .channel .recv_exit_status .side_effect = lambda : return_values .pop (0 )
667+
668+ out = host .run_shell_command (command , _sudo = True )
669+ assert len (out ) == 2
670+ assert out [0 ] is True
671+ assert fake_getpass .called
672+ fake_ssh .exec_command .assert_called_with (
673+ "env SUDO_ASKPASS=/tmp/pyinfra-sudo-askpass-XXXXXXXXXXXX "
674+ "PYINFRA_SUDO_PASSWORD=PASSWORD sudo -H -A -k sh -c 'echo hi'" ,
675+ get_pty = False ,
676+ )
677+
639678 # SSH file put/get tests
640679 #
641680
0 commit comments