@@ -639,6 +639,236 @@ def test_here_mode_provisions_environment(
639639 )
640640
641641
642+ # --- respawn-pane provisioning tests (f5f490a8, 0504d1b4) ---
643+
644+
645+ class HereRespawnFixture (t .NamedTuple ):
646+ """Fixture for --here respawn-pane provisioning scenarios."""
647+
648+ test_id : str
649+ start_directory : bool
650+ environment : dict [str , str ] | None
651+ window_shell : str | None
652+ expect_respawn : bool
653+
654+
655+ HERE_RESPAWN_FIXTURES : list [HereRespawnFixture ] = [
656+ HereRespawnFixture (
657+ test_id = "dir-only" ,
658+ start_directory = True ,
659+ environment = None ,
660+ window_shell = None ,
661+ expect_respawn = True ,
662+ ),
663+ HereRespawnFixture (
664+ test_id = "env-only" ,
665+ start_directory = False ,
666+ environment = {"TMUXP_TEST_VAR" : "respawn_val" },
667+ window_shell = None ,
668+ expect_respawn = True ,
669+ ),
670+ HereRespawnFixture (
671+ test_id = "dir-and-env" ,
672+ start_directory = True ,
673+ environment = {"TMUXP_DIR_ENV" : "combined" },
674+ window_shell = None ,
675+ expect_respawn = True ,
676+ ),
677+ HereRespawnFixture (
678+ test_id = "nothing-to-provision" ,
679+ start_directory = False ,
680+ environment = None ,
681+ window_shell = None ,
682+ expect_respawn = False ,
683+ ),
684+ ]
685+
686+
687+ @pytest .mark .parametrize (
688+ list (HereRespawnFixture ._fields ),
689+ HERE_RESPAWN_FIXTURES ,
690+ ids = [f .test_id for f in HERE_RESPAWN_FIXTURES ],
691+ )
692+ def test_here_mode_respawn_provisioning (
693+ session : Session ,
694+ tmp_path : pathlib .Path ,
695+ test_id : str ,
696+ start_directory : bool ,
697+ environment : dict [str , str ] | None ,
698+ window_shell : str | None ,
699+ expect_respawn : bool ,
700+ ) -> None :
701+ """--here mode uses respawn-pane for provisioning, not send_keys."""
702+ test_dir = tmp_path / "here_respawn"
703+ test_dir .mkdir ()
704+
705+ workspace : dict [str , t .Any ] = {
706+ "session_name" : session .name ,
707+ "windows" : [
708+ {
709+ "window_name" : "respawn-test" ,
710+ "panes" : [{"shell_command" : []}],
711+ },
712+ ],
713+ }
714+ if start_directory :
715+ workspace ["windows" ][0 ]["start_directory" ] = str (test_dir )
716+ if environment :
717+ workspace ["windows" ][0 ]["environment" ] = environment
718+
719+ workspace = loader .expand (workspace )
720+ workspace = loader .trickle (workspace )
721+
722+ original_pane = session .active_window .active_pane
723+ assert original_pane is not None
724+ original_pid = original_pane .pane_pid
725+
726+ builder = WorkspaceBuilder (session_config = workspace , server = session .server )
727+ builder .build (session = session , here = True )
728+
729+ pane = session .active_window .active_pane
730+ assert pane is not None
731+
732+ if expect_respawn :
733+ # respawn-pane -k replaces the shell process, so PID changes
734+ assert pane .pane_pid != original_pid , (
735+ f"Expected new PID after respawn, got same: { pane .pane_pid } "
736+ )
737+ else :
738+ # No provisioning needed — pane process should be unchanged
739+ assert pane .pane_pid == original_pid
740+
741+ if start_directory :
742+ expected_path = os .path .realpath (str (test_dir ))
743+ assert retry_until (
744+ lambda : pane .pane_current_path == expected_path ,
745+ seconds = 5 ,
746+ ), f"Expected { expected_path } , got { pane .pane_current_path } "
747+
748+ if environment :
749+ env = session .show_environment ()
750+ for key , val in environment .items ():
751+ assert env .get (key ) == val
752+
753+
754+ def test_here_mode_respawn_multiple_env_vars (
755+ session : Session ,
756+ ) -> None :
757+ """--here mode sets multiple environment variables via set_environment."""
758+ workspace : dict [str , t .Any ] = {
759+ "session_name" : session .name ,
760+ "windows" : [
761+ {
762+ "window_name" : "multi-env" ,
763+ "environment" : {
764+ "TMUXP_A" : "alpha" ,
765+ "TMUXP_B" : "bravo" ,
766+ "TMUXP_C" : "charlie" ,
767+ },
768+ "panes" : [{"shell_command" : []}],
769+ },
770+ ],
771+ }
772+ workspace = loader .expand (workspace )
773+
774+ builder = WorkspaceBuilder (session_config = workspace , server = session .server )
775+ builder .build (session = session , here = True )
776+
777+ env = session .show_environment ()
778+ assert env .get ("TMUXP_A" ) == "alpha"
779+ assert env .get ("TMUXP_B" ) == "bravo"
780+ assert env .get ("TMUXP_C" ) == "charlie"
781+
782+
783+ def test_here_mode_respawn_warns_on_running_processes (
784+ session : Session ,
785+ caplog : pytest .LogCaptureFixture ,
786+ tmp_path : pathlib .Path ,
787+ ) -> None :
788+ """--here mode warns when respawn-pane will kill child processes."""
789+ # Start a background process in the active pane so pgrep finds children
790+ pane = session .active_window .active_pane
791+ assert pane is not None
792+ pane .send_keys ("sleep 300 &" , enter = True )
793+
794+ # Give the shell time to fork the background job
795+ assert (
796+ retry_until (
797+ lambda : (
798+ "sleep" in (pane .pane_current_command or "" )
799+ or retry_until (
800+ lambda : len (pane .capture_pane ()) > 1 ,
801+ seconds = 2 ,
802+ )
803+ ),
804+ seconds = 3 ,
805+ )
806+ or True
807+ ) # Best-effort; pgrep check below is the real assertion
808+
809+ test_dir = tmp_path / "warn_test"
810+ test_dir .mkdir ()
811+
812+ workspace : dict [str , t .Any ] = {
813+ "session_name" : session .name ,
814+ "windows" : [
815+ {
816+ "window_name" : "warn-test" ,
817+ "start_directory" : str (test_dir ),
818+ "panes" : [{"shell_command" : []}],
819+ },
820+ ],
821+ }
822+ workspace = loader .expand (workspace )
823+ workspace = loader .trickle (workspace )
824+
825+ with caplog .at_level (logging .WARNING , logger = "tmuxp.workspace.builder" ):
826+ builder = WorkspaceBuilder (session_config = workspace , server = session .server )
827+ builder .build (session = session , here = True )
828+
829+ warning_records = [
830+ r
831+ for r in caplog .records
832+ if r .levelno == logging .WARNING and "kill running processes" in r .message
833+ ]
834+ # pgrep should find the sleep background job and emit a warning
835+ assert len (warning_records ) >= 1
836+
837+
838+ def test_here_mode_no_warning_when_pane_idle (
839+ session : Session ,
840+ caplog : pytest .LogCaptureFixture ,
841+ tmp_path : pathlib .Path ,
842+ ) -> None :
843+ """--here mode does not warn when pane has no child processes."""
844+ test_dir = tmp_path / "idle_test"
845+ test_dir .mkdir ()
846+
847+ workspace : dict [str , t .Any ] = {
848+ "session_name" : session .name ,
849+ "windows" : [
850+ {
851+ "window_name" : "idle-test" ,
852+ "start_directory" : str (test_dir ),
853+ "panes" : [{"shell_command" : []}],
854+ },
855+ ],
856+ }
857+ workspace = loader .expand (workspace )
858+ workspace = loader .trickle (workspace )
859+
860+ with caplog .at_level (logging .WARNING , logger = "tmuxp.workspace.builder" ):
861+ builder = WorkspaceBuilder (session_config = workspace , server = session .server )
862+ builder .build (session = session , here = True )
863+
864+ warning_records = [
865+ r
866+ for r in caplog .records
867+ if r .levelno == logging .WARNING and "kill running processes" in r .message
868+ ]
869+ assert len (warning_records ) == 0
870+
871+
642872def test_window_shell (
643873 session : Session ,
644874) -> None :
0 commit comments