@@ -751,3 +751,83 @@ def test_load_append_windows_to_current_session(
751751
752752 assert len (server .sessions ) == 1
753753 assert len (server .windows ) == 6
754+
755+
756+ class LoadAttachExceptionFixture (t .NamedTuple ):
757+ """Test fixture for _load_attached() exception handling regression.
758+
759+ This tests the scenario where Session.attach() raises TmuxObjectDoesNotExist
760+ because the session was killed while the user was attached (e.g., user killed
761+ all windows from within tmux before detaching).
762+
763+ See: https://github.com/tmux-python/tmuxp/issues/1002
764+ """
765+
766+ test_id : str
767+ session_killed_during_attach : bool
768+ should_not_raise : bool
769+
770+
771+ LOAD_ATTACH_EXCEPTION_FIXTURES : list [LoadAttachExceptionFixture ] = [
772+ LoadAttachExceptionFixture (
773+ test_id = "session_killed_during_attach_should_not_propagate" ,
774+ session_killed_during_attach = True ,
775+ should_not_raise = True , # We expect _load_attached to NOT propagate the exception
776+ ),
777+ ]
778+
779+
780+ @pytest .mark .parametrize (
781+ list (LoadAttachExceptionFixture ._fields ),
782+ LOAD_ATTACH_EXCEPTION_FIXTURES ,
783+ ids = [test .test_id for test in LOAD_ATTACH_EXCEPTION_FIXTURES ],
784+ )
785+ def test_load_attached_handles_session_killed_during_attach (
786+ server : Server ,
787+ monkeypatch : pytest .MonkeyPatch ,
788+ mocker : MockerFixture ,
789+ test_id : str ,
790+ session_killed_during_attach : bool ,
791+ should_not_raise : bool ,
792+ ) -> None :
793+ """Regression test: _load_attached() handles session killed during attach.
794+
795+ When a user is attached to a tmux session via `tmuxp load`, then kills the
796+ session from within tmux (e.g., kills all windows), and then detaches,
797+ the _load_attached() function should complete without raising an exception.
798+
799+ This was fixed by removing the refresh() call from Session.attach() in libtmux
800+ since attach-session is a blocking interactive command and session state can
801+ change arbitrarily while the user is attached.
802+
803+ See: https://github.com/tmux-python/tmuxp/issues/1002
804+ """
805+ # Load outside of tmux
806+ monkeypatch .delenv ("TMUX" , raising = False )
807+
808+ yaml_config = test_utils .read_workspace_file ("workspace/builder/two_pane.yaml" )
809+ session_config = ConfigReader ._load (fmt = "yaml" , content = yaml_config )
810+
811+ builder = WorkspaceBuilder (session_config = session_config , server = server )
812+
813+ if session_killed_during_attach :
814+ # Simulate attach returning successfully but session being killed during
815+ # the attachment period. With the fix in libtmux, attach() doesn't call
816+ # refresh() anymore, so this doesn't raise an exception.
817+ #
818+ # We patch at class level since builder.session doesn't exist until build()
819+ def mock_attach (self : Session , * args : t .Any , ** kwargs : t .Any ) -> Session :
820+ """Simulate attach() completing after session was killed during attach."""
821+ # Kill the session to simulate user action during attachment
822+ self .kill ()
823+ # Return the session object (even though it's now dead)
824+ # This is what attach() does now - it doesn't try to refresh
825+ return self
826+
827+ mocker .patch ("libtmux.session.Session.attach" , mock_attach )
828+
829+ if should_not_raise :
830+ # With the libtmux fix, attach() doesn't call refresh(), so even if the
831+ # session was killed during attachment, no exception is raised
832+ _load_attached (builder , detached = False )
833+ # If we get here without exception, the test passes
0 commit comments