311 lines
11 KiB
Python
311 lines
11 KiB
Python
"""Tests for CLI entry point."""
|
|
import pytest
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
from io import StringIO
|
|
|
|
from gsd_openclaw.tmux_session import TmuxSession, TmuxSessionError, CleanupResult, SessionInfo
|
|
from gsd_openclaw.cli import main, cmd_create, cmd_list, cmd_destroy, cmd_resume, cmd_cleanup
|
|
|
|
|
|
class TestCliCreate:
|
|
"""Tests for CLI create command."""
|
|
|
|
def test_cli_create_creates_session(self):
|
|
"""`gsd-ocl create <project>` creates session."""
|
|
mock_session = MagicMock()
|
|
mock_session.tmux_session_name = "gsd-test-project"
|
|
mock_session.session_id = "test-123"
|
|
mock_session.project_name = "test-project"
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.create_session.return_value = mock_session
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'create', 'test-project']):
|
|
main()
|
|
|
|
mock_manager.create_session.assert_called_once_with(
|
|
project_name='test-project',
|
|
display_name=None
|
|
)
|
|
|
|
def test_cli_create_with_display_name(self):
|
|
"""`gsd-ocl create <project> --display-name` sets display name."""
|
|
mock_session = MagicMock()
|
|
mock_session.tmux_session_name = "gsd-test-project"
|
|
mock_session.session_id = "test-123"
|
|
mock_session.project_name = "test-project"
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.create_session.return_value = mock_session
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'create', 'test-project', '-d', 'My Project']):
|
|
main()
|
|
|
|
mock_manager.create_session.assert_called_once_with(
|
|
project_name='test-project',
|
|
display_name='My Project'
|
|
)
|
|
|
|
def test_cli_create_exits_on_error(self):
|
|
"""CLI exits with code 1 on error."""
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.create_session.side_effect = Exception("Create failed")
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'create', 'test-project']):
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
main()
|
|
|
|
assert exc_info.value.code == 1
|
|
|
|
|
|
class TestCliList:
|
|
"""Tests for CLI list command."""
|
|
|
|
def test_cli_list_shows_sessions(self):
|
|
"""`gsd-ocl list` shows sessions."""
|
|
mock_session_info = SessionInfo(
|
|
tmux_name="gsd-test-project",
|
|
project_name="test-project",
|
|
session_id="test-123",
|
|
created_at="2026-03-10T10:00:00",
|
|
last_active_at="2026-03-10T12:00:00",
|
|
command_count=5,
|
|
display_name="Test Project",
|
|
is_orphan=False
|
|
)
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.list_gsd_sessions.return_value = [mock_session_info]
|
|
MockManager.return_value = mock_manager
|
|
|
|
stdout = StringIO()
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'list']):
|
|
with patch('sys.stdout', stdout):
|
|
main()
|
|
|
|
output = stdout.getvalue()
|
|
assert "test-project" in output or "Test Project" in output
|
|
assert "test-123" in output
|
|
assert "active" in output
|
|
|
|
def test_cli_list_shows_no_sessions_message(self):
|
|
"""`gsd-ocl list` shows message when no sessions."""
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.list_gsd_sessions.return_value = []
|
|
MockManager.return_value = mock_manager
|
|
|
|
stdout = StringIO()
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'list']):
|
|
with patch('sys.stdout', stdout):
|
|
main()
|
|
|
|
output = stdout.getvalue()
|
|
assert "No active sessions" in output
|
|
|
|
def test_cli_list_shows_orphan_status(self):
|
|
"""`gsd-ocl list` shows ORPHAN status for orphan sessions."""
|
|
mock_session_info = SessionInfo(
|
|
tmux_name="gsd-orphan",
|
|
project_name="orphan",
|
|
session_id="?",
|
|
created_at="?",
|
|
last_active_at="?",
|
|
command_count=0,
|
|
display_name=None,
|
|
is_orphan=True
|
|
)
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.list_gsd_sessions.return_value = [mock_session_info]
|
|
MockManager.return_value = mock_manager
|
|
|
|
stdout = StringIO()
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'list']):
|
|
with patch('sys.stdout', stdout):
|
|
main()
|
|
|
|
output = stdout.getvalue()
|
|
assert "ORPHAN" in output
|
|
|
|
|
|
class TestCliDestroy:
|
|
"""Tests for CLI destroy command."""
|
|
|
|
def test_cli_destroy_removes_session(self):
|
|
"""`gsd-ocl destroy <project>` removes session."""
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.destroy_session.return_value = True
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'destroy', 'test-project']):
|
|
main()
|
|
|
|
mock_manager.destroy_session.assert_called_once_with(
|
|
project_name='test-project',
|
|
cleanup_workspace=False
|
|
)
|
|
|
|
def test_cli_destroy_with_cleanup(self):
|
|
"""`gsd-ocl destroy <project> --cleanup` removes workspace too."""
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.destroy_session.return_value = True
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'destroy', 'test-project', '-c']):
|
|
main()
|
|
|
|
mock_manager.destroy_session.assert_called_once_with(
|
|
project_name='test-project',
|
|
cleanup_workspace=True
|
|
)
|
|
|
|
def test_cli_destroy_exits_if_not_found(self):
|
|
"""`gsd-ocl destroy` exits with code 1 if session not found."""
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.destroy_session.return_value = False
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'destroy', 'nonexistent']):
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
main()
|
|
|
|
assert exc_info.value.code == 1
|
|
|
|
|
|
class TestCliResume:
|
|
"""Tests for CLI resume command."""
|
|
|
|
def test_cli_resume_resumes_session(self):
|
|
"""`gsd-ocl resume <project>` resumes session."""
|
|
mock_session = MagicMock()
|
|
mock_session.tmux_session_name = "gsd-test-project"
|
|
mock_session.session_id = "test-123"
|
|
mock_session.project_name = "test-project"
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.resume_session.return_value = mock_session
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'resume', 'test-project']):
|
|
main()
|
|
|
|
mock_manager.resume_session.assert_called_once_with(project_name='test-project')
|
|
|
|
def test_cli_resume_exits_on_error(self):
|
|
"""`gsd-ocl resume` exits with code 1 on error."""
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.resume_session.side_effect = TmuxSessionError("No session found")
|
|
MockManager.return_value = mock_manager
|
|
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'resume', 'nonexistent']):
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
main()
|
|
|
|
assert exc_info.value.code == 1
|
|
|
|
|
|
class TestCliCleanup:
|
|
"""Tests for CLI cleanup command."""
|
|
|
|
def test_cli_cleanup_removes_orphans(self):
|
|
"""`gsd-ocl cleanup` removes orphan sessions."""
|
|
result = CleanupResult(
|
|
cleaned_sessions=["gsd-orphan1", "gsd-orphan2"],
|
|
errors=[]
|
|
)
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.cleanup_orphans.return_value = result
|
|
MockManager.return_value = mock_manager
|
|
|
|
stdout = StringIO()
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'cleanup']):
|
|
with patch('sys.stdout', stdout):
|
|
main()
|
|
|
|
output = stdout.getvalue()
|
|
assert "2" in output
|
|
assert "gsd-orphan1" in output
|
|
assert "gsd-orphan2" in output
|
|
|
|
def test_cli_cleanup_shows_errors(self):
|
|
"""`gsd-ocl cleanup` shows errors."""
|
|
result = CleanupResult(
|
|
cleaned_sessions=[],
|
|
errors=["Failed to kill gsd-failing: error"]
|
|
)
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.cleanup_orphans.return_value = result
|
|
MockManager.return_value = mock_manager
|
|
|
|
stdout = StringIO()
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'cleanup']):
|
|
with patch('sys.stdout', stdout):
|
|
main()
|
|
|
|
output = stdout.getvalue()
|
|
assert "error" in output.lower() or "Error" in output
|
|
|
|
def test_cli_cleanup_no_orphans_found(self):
|
|
"""`gsd-ocl cleanup` shows message when no orphans found."""
|
|
result = CleanupResult(
|
|
cleaned_sessions=[],
|
|
errors=[]
|
|
)
|
|
|
|
with patch('gsd_openclaw.cli.TmuxSessionManager') as MockManager:
|
|
mock_manager = MagicMock()
|
|
mock_manager.cleanup_orphans.return_value = result
|
|
MockManager.return_value = mock_manager
|
|
|
|
stdout = StringIO()
|
|
with patch.object(sys, 'argv', ['gsd-ocl', 'cleanup']):
|
|
with patch('sys.stdout', stdout):
|
|
main()
|
|
|
|
output = stdout.getvalue()
|
|
assert "No orphan sessions found" in output
|
|
|
|
|
|
class TestCliHelp:
|
|
"""Tests for CLI help."""
|
|
|
|
def test_cli_help_works(self):
|
|
"""`gsd-ocl --help` shows help message."""
|
|
stdout = StringIO()
|
|
with patch.object(sys, 'argv', ['gsd-ocl', '--help']):
|
|
with patch('sys.stdout', stdout):
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
main()
|
|
|
|
# --help exits with code 0
|
|
assert exc_info.value.code == 0
|
|
output = stdout.getvalue()
|
|
assert "gsd-ocl" in output
|
|
|
|
def test_cli_requires_subcommand(self):
|
|
"""CLI requires a subcommand."""
|
|
with patch.object(sys, 'argv', ['gsd-ocl']):
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
main()
|
|
|
|
assert exc_info.value.code != 0
|