This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw/skills/gsd-openclaw/tests/test_tmux_session.py
T

773 lines
30 KiB
Python

"""Tests for tmux session lifecycle management."""
import pytest
import libtmux
import libtmux.exc
from pathlib import Path
from unittest.mock import MagicMock, patch, PropertyMock
from datetime import datetime
from gsd_openclaw.tmux_session import TmuxSession, TmuxSessionManager, TmuxSessionError, CleanupResult
class TestTmuxSessionDataclass:
"""Tests for TmuxSession dataclass."""
def test_tmux_session_stores_metadata_correctly(self):
"""TmuxSession should store session metadata correctly."""
session = TmuxSession(
session_id="test-123",
project_name="my-project",
tmux_session_name="gsd-my-project"
)
assert session.session_id == "test-123"
assert session.project_name == "my-project"
assert session.tmux_session_name == "gsd-my-project"
assert session.tmux_session is None
def test_tmux_session_exists_false_when_no_runtime_reference(self):
"""TmuxSession.exists should return False when no runtime reference."""
session = TmuxSession(
session_id="test-123",
project_name="my-project",
tmux_session_name="gsd-my-project"
)
assert session.exists is False
def test_tmux_session_exists_true_when_session_active(self):
"""TmuxSession.exists should return True when session is active."""
# Create a mock tmux session
mock_tmux = MagicMock()
mock_tmux.session_id = "%123"
session = TmuxSession(
session_id="test-123",
project_name="my-project",
tmux_session_name="gsd-my-project",
tmux_session=mock_tmux
)
assert session.exists is True
def test_tmux_session_exists_false_on_libtmux_exception(self):
"""TmuxSession.exists should return False on LibTmuxException."""
# Create a mock that raises exception when accessing session_id
mock_tmux = MagicMock()
type(mock_tmux).session_id = PropertyMock(side_effect=libtmux.exc.LibTmuxException("gone"))
session = TmuxSession(
session_id="test-123",
project_name="my-project",
tmux_session_name="gsd-my-project",
tmux_session=mock_tmux
)
assert session.exists is False
class TestTmuxSessionManagerBasic:
"""Tests for TmuxSessionManager basic functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock libtmux server."""
server = MagicMock(spec=libtmux.Server)
server.sessions = MagicMock()
server.sessions.filter = MagicMock(return_value=[])
return server
@pytest.fixture
def manager(self, mock_server):
"""Create TmuxSessionManager with mock server."""
return TmuxSessionManager(server=mock_server)
def test_server_connection_works(self, manager, mock_server):
"""TmuxSessionManager should have a server reference."""
assert manager.server is not None
assert manager.server is mock_server
def test_list_sessions_returns_list(self, manager, mock_server):
"""list_sessions should return a list."""
mock_server.sessions.filter.return_value = []
sessions = manager.list_sessions()
assert isinstance(sessions, list)
def test_list_sessions_filters_gsd_prefix(self, manager, mock_server):
"""list_sessions should filter for gsd- prefix."""
# Create mock sessions
mock_session1 = MagicMock()
mock_session1.name = "gsd-project1"
mock_session2 = MagicMock()
mock_session2.name = "gsd-project2"
mock_server.sessions.filter.return_value = [mock_session1, mock_session2]
sessions = manager.list_sessions()
assert len(sessions) == 2
mock_server.sessions.filter.assert_called_once_with(
session_name__startswith="gsd-"
)
def test_generate_session_name_format(self, manager):
"""_generate_session_name should return gsd-{project} format."""
name = manager._generate_session_name("my-project")
assert name == "gsd-my-project"
name2 = manager._generate_session_name("another_project")
assert name2 == "gsd-another_project"
def test_resolve_collision_returns_base_when_no_collision(self, manager, mock_server):
"""_resolve_collision should return base name when no collision exists."""
mock_server.sessions.filter.return_value = []
result = manager._resolve_collision("gsd-my-project")
assert result == "gsd-my-project"
def test_resolve_collision_adds_timestamp_when_collision_exists(self, manager, mock_server):
"""_resolve_collision should add timestamp suffix when collision exists."""
# Simulate existing session
mock_session = MagicMock()
mock_server.sessions.filter.return_value = [mock_session]
result = manager._resolve_collision("gsd-collision-test")
# Should have timestamp suffix
assert result.startswith("gsd-collision-test-")
# Should match timestamp pattern -YYYYMMDD-HHMMSS
import re
pattern = r"gsd-collision-test-\d{8}-\d{6}"
assert re.match(pattern, result), f"Result '{result}' doesn't match pattern {pattern}"
class TestTmuxSessionManagerCreateSession:
"""Tests for create_session functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock libtmux server."""
server = MagicMock(spec=libtmux.Server)
server.sessions = MagicMock()
server.sessions.filter = MagicMock(return_value=[])
server.sessions.get = MagicMock(return_value=None)
# Mock new_session to return a mock session
mock_session = MagicMock()
mock_session.name = "gsd-test-project"
mock_pane = MagicMock()
mock_pane.pane_id = "%100"
mock_session.active_pane = mock_pane
server.new_session = MagicMock(return_value=mock_session)
return server
@pytest.fixture
def manager(self, mock_server):
"""Create TmuxSessionManager with mock server."""
return TmuxSessionManager(server=mock_server)
def test_create_session_creates_tmux_session(self, manager, mock_server, tmp_workspaces_root):
"""create_session should create a tmux session with correct name."""
with patch('gsd_openclaw.tmux_session.WorkspaceManager') as MockWM:
mock_wm = MagicMock()
MockWM.return_value = mock_wm
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
MockSM.return_value = mock_sm
result = manager.create_session("test-project")
# Verify tmux session was created
mock_server.new_session.assert_called_once()
call_kwargs = mock_server.new_session.call_args[1]
assert call_kwargs['session_name'].startswith("gsd-test-project")
assert call_kwargs['attach'] is False
assert call_kwargs['window_name'] == 'main'
def test_create_session_persists_state(self, manager, mock_server, tmp_workspaces_root):
"""create_session should persist state to workspace."""
with patch('gsd_openclaw.tmux_session.WorkspaceManager') as MockWM:
mock_wm = MagicMock()
MockWM.return_value = mock_wm
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
MockSM.return_value = mock_sm
result = manager.create_session("test-project")
# Verify state was saved
mock_sm.save.assert_called_once()
def test_create_session_handles_collision(self, manager, mock_server, tmp_workspaces_root):
"""create_session should handle name collision with timestamp suffix."""
# Simulate collision
mock_session = MagicMock()
mock_server.sessions.filter.return_value = [mock_session]
with patch('gsd_openclaw.tmux_session.WorkspaceManager') as MockWM:
mock_wm = MagicMock()
MockWM.return_value = mock_wm
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
MockSM.return_value = mock_sm
result = manager.create_session("collision-test")
# Verify session name has timestamp suffix
call_kwargs = mock_server.new_session.call_args[1]
session_name = call_kwargs['session_name']
assert session_name.startswith("gsd-collision-test-")
import re
assert re.match(r"gsd-collision-test-\d{8}-\d{6}", session_name)
def test_create_session_returns_tmux_session(self, manager, mock_server, tmp_workspaces_root):
"""create_session should return TmuxSession with all metadata."""
with patch('gsd_openclaw.tmux_session.WorkspaceManager') as MockWM:
mock_wm = MagicMock()
MockWM.return_value = mock_wm
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
MockSM.return_value = mock_sm
result = manager.create_session("test-project", display_name="My Project")
assert isinstance(result, TmuxSession)
assert result.project_name == "test-project"
assert result.tmux_session_name.startswith("gsd-test-project")
assert result.tmux_session is not None
class TestTmuxSessionManagerDestroySession:
"""Tests for destroy_session functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock libtmux server."""
server = MagicMock(spec=libtmux.Server)
server.sessions = MagicMock()
server.sessions.filter = MagicMock(return_value=[])
server.sessions.get = MagicMock(return_value=None)
return server
@pytest.fixture
def manager(self, mock_server):
"""Create TmuxSessionManager with mock server."""
return TmuxSessionManager(server=mock_server)
def test_destroy_session_kills_tmux_session(self, manager, mock_server, tmp_workspaces_root):
"""destroy_session should kill the tmux session."""
# Mock state
mock_state = MagicMock()
mock_state.tmux_session_name = "gsd-test-project"
# Mock tmux session
mock_tmux_session = MagicMock()
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
mock_sm.state_path = tmp_workspaces_root / "test-project" / ".gsd" / "state.json"
MockSM.return_value = mock_sm
mock_server.sessions.get.return_value = mock_tmux_session
result = manager.destroy_session("test-project")
mock_tmux_session.kill.assert_called_once()
assert result is True
def test_destroy_session_removes_state_file(self, manager, mock_server, tmp_workspaces_root):
"""destroy_session should remove state file."""
# Create a temp state file
state_dir = tmp_workspaces_root / "test-project" / ".gsd"
state_dir.mkdir(parents=True, exist_ok=True)
state_file = state_dir / "state.json"
state_file.write_text('{"test": "data"}')
backup_file = state_dir / "state.json.bak"
backup_file.write_text('{"test": "backup"}')
# Mock state
mock_state = MagicMock()
mock_state.tmux_session_name = "gsd-test-project"
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
mock_sm.state_path = state_file
MockSM.return_value = mock_sm
mock_server.sessions.get.return_value = None
result = manager.destroy_session("test-project")
assert not state_file.exists()
assert not backup_file.exists()
def test_destroy_session_returns_false_when_no_state(self, manager, mock_server, tmp_workspaces_root):
"""destroy_session should return False when no state exists."""
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None
MockSM.return_value = mock_sm
result = manager.destroy_session("nonexistent-project")
assert result is False
def test_destroy_session_handles_already_destroyed_session(self, manager, mock_server, tmp_workspaces_root):
"""destroy_session should handle already-destroyed session gracefully."""
# Mock state
mock_state = MagicMock()
mock_state.tmux_session_name = "gsd-test-project"
state_dir = tmp_workspaces_root / "test-project" / ".gsd"
state_dir.mkdir(parents=True, exist_ok=True)
state_file = state_dir / "state.json"
state_file.write_text('{}')
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
mock_sm.state_path = state_file
MockSM.return_value = mock_sm
# Session already gone
mock_server.sessions.get.return_value = None
result = manager.destroy_session("test-project")
assert result is True
def test_destroy_session_with_cleanup_removes_workspace(self, manager, mock_server, tmp_workspaces_root):
"""destroy_session with cleanup=True should remove workspace."""
# Mock state
mock_state = MagicMock()
mock_state.tmux_session_name = "gsd-test-project"
state_dir = tmp_workspaces_root / "test-project" / ".gsd"
state_dir.mkdir(parents=True, exist_ok=True)
state_file = state_dir / "state.json"
state_file.write_text('{}')
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
mock_sm.state_path = state_file
MockSM.return_value = mock_sm
mock_server.sessions.get.return_value = None
with patch('gsd_openclaw.tmux_session.WorkspaceManager') as MockWM:
mock_wm = MagicMock()
MockWM.return_value = mock_wm
result = manager.destroy_session("test-project", cleanup_workspace=True)
mock_wm.destroy.assert_called_once()
class TestTmuxSessionManagerListGsdSessions:
"""Tests for list_gsd_sessions functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock libtmux server."""
server = MagicMock(spec=libtmux.Server)
server.sessions = MagicMock()
server.sessions.filter = MagicMock(return_value=[])
server.sessions.get = MagicMock(return_value=None)
return server
@pytest.fixture
def manager(self, mock_server):
"""Create TmuxSessionManager with mock server."""
return TmuxSessionManager(server=mock_server)
def test_list_gsd_sessions_returns_all_gsd_sessions(self, manager, mock_server):
"""list_gsd_sessions should return all gsd-* sessions."""
# Create mock sessions
mock_session1 = MagicMock()
mock_session1.name = "gsd-project1"
mock_session2 = MagicMock()
mock_session2.name = "gsd-project2"
mock_server.sessions.filter.return_value = [mock_session1, mock_session2]
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None
MockSM.return_value = mock_sm
sessions = manager.list_gsd_sessions()
assert len(sessions) == 2
def test_list_gsd_sessions_handles_orphan_sessions(self, manager, mock_server):
"""list_gsd_sessions should handle orphan sessions (no state file)."""
# Create mock session
mock_session = MagicMock()
mock_session.name = "gsd-orphan-project"
mock_server.sessions.filter.return_value = [mock_session]
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None # No state
MockSM.return_value = mock_sm
sessions = manager.list_gsd_sessions()
assert len(sessions) == 1
assert sessions[0].is_orphan is True
assert sessions[0].session_id == "?"
def test_list_gsd_sessions_includes_project_associations(self, manager, mock_server):
"""list_gsd_sessions should include project associations from state files."""
# Create mock session
mock_session = MagicMock()
mock_session.name = "gsd-my-project"
mock_server.sessions.filter.return_value = [mock_session]
# Mock state
mock_state = MagicMock()
mock_state.tmux_session_name = "gsd-my-project"
mock_state.session_id = "abc123"
mock_state.created_at = "2026-03-10T10:00:00"
mock_state.last_active_at = "2026-03-10T12:00:00"
mock_state.command_count = 5
mock_state.display_name = "My Project"
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
MockSM.return_value = mock_sm
sessions = manager.list_gsd_sessions()
assert len(sessions) == 1
assert sessions[0].is_orphan is False
assert sessions[0].project_name == "my-project"
assert sessions[0].session_id == "abc123"
assert sessions[0].display_name == "My Project"
class TestTmuxSessionManagerResumeSession:
"""Tests for resume_session functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock libtmux server."""
server = MagicMock(spec=libtmux.Server)
server.sessions = MagicMock()
server.sessions.filter = MagicMock(return_value=[])
server.sessions.get = MagicMock(return_value=None)
# Mock new_session to return a mock session
mock_session = MagicMock()
mock_session.name = "gsd-test-project"
mock_pane = MagicMock()
mock_pane.pane_id = "%200"
mock_session.active_pane = mock_pane
server.new_session = MagicMock(return_value=mock_session)
return server
@pytest.fixture
def manager(self, mock_server):
"""Create TmuxSessionManager with mock server."""
return TmuxSessionManager(server=mock_server)
def test_resume_session_returns_existing_session_if_state_valid(self, manager, mock_server):
"""resume_session should return existing session if state valid and tmux exists."""
from gsd_openclaw.state_manager import SessionState
# Mock state
mock_state = SessionState(
schema_version="1.0",
session_id="test-123",
project_name="test-project",
tmux_session_name="gsd-test-project",
tmux_pane_id="%100",
created_at="2026-03-10T10:00:00",
last_active_at="2026-03-10T12:00:00"
)
# Mock tmux session exists
mock_tmux_session = MagicMock()
mock_server.sessions.get.return_value = mock_tmux_session
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
MockSM.return_value = mock_sm
result = manager.resume_session("test-project")
assert result.session_id == "test-123"
assert result.project_name == "test-project"
assert result.tmux_session_name == "gsd-test-project"
mock_server.sessions.get.assert_called_once_with(session_name="gsd-test-project")
def test_resume_session_recreates_tmux_if_missing(self, manager, mock_server):
"""resume_session should recreate tmux session if state exists but tmux missing."""
from gsd_openclaw.state_manager import SessionState
# Mock state
mock_state = SessionState(
schema_version="1.0",
session_id="test-123",
project_name="test-project",
tmux_session_name="gsd-test-project",
tmux_pane_id="%100",
created_at="2026-03-10T10:00:00",
last_active_at="2026-03-10T12:00:00"
)
# Mock tmux session does not exist initially
mock_server.sessions.get.return_value = None
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
MockSM.return_value = mock_sm
with patch('gsd_openclaw.tmux_session.WorkspaceManager') as MockWM:
mock_wm = MagicMock()
mock_wm.exists.return_value = True
mock_workspace = MagicMock()
mock_workspace.root = "/tmp/test-workspace"
mock_wm.get_workspace.return_value = mock_workspace
MockWM.return_value = mock_wm
result = manager.resume_session("test-project")
# Verify tmux session was recreated
mock_server.new_session.assert_called_once()
assert result.tmux_session is not None
def test_resume_session_raises_error_if_no_state(self, manager, mock_server):
"""resume_session should raise error if no state file exists."""
from gsd_openclaw.tmux_session import TmuxSessionError
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None
MockSM.return_value = mock_sm
with pytest.raises(TmuxSessionError, match="No session found"):
manager.resume_session("nonexistent-project")
def test_resume_session_validates_schema_version(self, manager, mock_server):
"""resume_session should validate schema version before resuming."""
from gsd_openclaw.tmux_session import TmuxSessionError
from gsd_openclaw.state_manager import SessionState
# Mock state with invalid schema version
mock_state = SessionState(
schema_version="2.0", # Invalid version
session_id="test-123",
project_name="test-project",
tmux_session_name="gsd-test-project",
tmux_pane_id="%100",
created_at="2026-03-10T10:00:00",
last_active_at="2026-03-10T12:00:00"
)
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
MockSM.return_value = mock_sm
with pytest.raises(TmuxSessionError, match="Unsupported state version"):
manager.resume_session("test-project")
def test_get_session_returns_none_if_no_state(self, manager, mock_server):
"""get_session should return None if no state file exists."""
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None
MockSM.return_value = mock_sm
result = manager.get_session("nonexistent-project")
assert result is None
def test_get_session_returns_session_if_state_exists(self, manager, mock_server):
"""get_session should return TmuxSession if state exists."""
from gsd_openclaw.state_manager import SessionState
# Mock state
mock_state = SessionState(
schema_version="1.0",
session_id="test-123",
project_name="test-project",
tmux_session_name="gsd-test-project",
tmux_pane_id="%100",
created_at="2026-03-10T10:00:00",
last_active_at="2026-03-10T12:00:00"
)
# Mock tmux session exists
mock_tmux_session = MagicMock()
mock_server.sessions.get.return_value = mock_tmux_session
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
MockSM.return_value = mock_sm
result = manager.get_session("test-project")
assert result is not None
assert result.session_id == "test-123"
assert result.project_name == "test-project"
class TestTmuxSessionManagerCleanupOrphans:
"""Tests for cleanup_orphans functionality."""
@pytest.fixture
def mock_server(self):
"""Create a mock libtmux server."""
server = MagicMock(spec=libtmux.Server)
server.sessions = MagicMock()
server.sessions.filter = MagicMock(return_value=[])
server.sessions.get = MagicMock(return_value=None)
return server
@pytest.fixture
def manager(self, mock_server):
"""Create TmuxSessionManager with mock server."""
return TmuxSessionManager(server=mock_server)
def test_cleanup_orphans_kills_sessions_without_state(self, manager, mock_server):
"""cleanup_orphans should kill tmux sessions without state files."""
# Create orphan tmux session
mock_orphan = MagicMock()
mock_orphan.name = "gsd-orphan-project"
mock_orphan.kill = MagicMock()
mock_server.sessions.filter.return_value = [mock_orphan]
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None # No state file
MockSM.return_value = mock_sm
result = manager.cleanup_orphans()
mock_orphan.kill.assert_called_once()
assert "gsd-orphan-project" in result.cleaned_sessions
assert len(result.errors) == 0
def test_cleanup_orphans_returns_list_of_cleaned_names(self, manager, mock_server):
"""cleanup_orphans should return list of cleaned session names."""
# Create multiple orphan sessions
mock_orphan1 = MagicMock()
mock_orphan1.name = "gsd-orphan1"
mock_orphan1.kill = MagicMock()
mock_orphan2 = MagicMock()
mock_orphan2.name = "gsd-orphan2"
mock_orphan2.kill = MagicMock()
mock_server.sessions.filter.return_value = [mock_orphan1, mock_orphan2]
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None
MockSM.return_value = mock_sm
result = manager.cleanup_orphans()
assert len(result.cleaned_sessions) == 2
assert "gsd-orphan1" in result.cleaned_sessions
assert "gsd-orphan2" in result.cleaned_sessions
def test_cleanup_orphans_handles_kill_errors(self, manager, mock_server):
"""cleanup_orphans should handle errors when killing sessions."""
# Create orphan session that fails to kill
mock_orphan = MagicMock()
mock_orphan.name = "gsd-failing-orphan"
mock_orphan.kill.side_effect = libtmux.exc.LibTmuxException("kill failed")
mock_server.sessions.filter.return_value = [mock_orphan]
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = None
MockSM.return_value = mock_sm
result = manager.cleanup_orphans()
assert len(result.cleaned_sessions) == 0
assert len(result.errors) == 1
assert "gsd-failing-orphan" in result.errors[0]
def test_cleanup_orphans_skips_sessions_with_valid_state(self, manager, mock_server):
"""cleanup_orphans should not kill sessions with valid state files."""
from gsd_openclaw.state_manager import SessionState
# Create session with valid state
mock_session = MagicMock()
mock_session.name = "gsd-valid-project"
mock_session.kill = MagicMock()
mock_server.sessions.filter.return_value = [mock_session]
# Mock valid state
mock_state = SessionState(
schema_version="1.0",
session_id="valid-123",
project_name="valid-project",
tmux_session_name="gsd-valid-project",
tmux_pane_id="%100",
created_at="2026-03-10T10:00:00",
last_active_at="2026-03-10T12:00:00"
)
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
MockSM.return_value = mock_sm
result = manager.cleanup_orphans()
mock_session.kill.assert_not_called()
assert len(result.cleaned_sessions) == 0
def test_cleanup_orphans_handles_mismatched_state(self, manager, mock_server):
"""cleanup_orphans should kill sessions with state referencing different tmux."""
from gsd_openclaw.state_manager import SessionState
# Create session with mismatched state
mock_session = MagicMock()
mock_session.name = "gsd-mismatched"
mock_session.kill = MagicMock()
mock_server.sessions.filter.return_value = [mock_session]
# Mock state that references different tmux session
mock_state = SessionState(
schema_version="1.0",
session_id="test-123",
project_name="mismatched",
tmux_session_name="gsd-different-name", # Different from actual
tmux_pane_id="%100",
created_at="2026-03-10T10:00:00",
last_active_at="2026-03-10T12:00:00"
)
with patch('gsd_openclaw.tmux_session.StateManager') as MockSM:
mock_sm = MagicMock()
mock_sm.load.return_value = mock_state
MockSM.return_value = mock_sm
result = manager.cleanup_orphans()
mock_session.kill.assert_called_once()
assert "gsd-mismatched" in result.cleaned_sessions
# Fixtures needed for tests
@pytest.fixture
def tmp_workspaces_root(tmp_path: Path) -> Path:
"""Temporary root for workspace testing."""
return tmp_path / "gsd-workspaces"