1105 lines
42 KiB
Python
1105 lines
42 KiB
Python
"""Tests for orchestrator command routing and integration."""
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from gsd_openclaw.command_executor import CommandResult
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
|
|
class TestOrchestratorInit:
|
|
"""Tests for Orchestrator initialization."""
|
|
|
|
def test_orchestrator_initializes_with_required_components(self):
|
|
"""Orchestrator should initialize with all required components."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
mock_tmux_manager = MagicMock()
|
|
mock_formatter = MagicMock()
|
|
|
|
orchestrator = Orchestrator(
|
|
tmux_manager=mock_tmux_manager,
|
|
formatter=mock_formatter
|
|
)
|
|
|
|
assert orchestrator.tmux_manager is mock_tmux_manager
|
|
assert orchestrator.formatter is mock_formatter
|
|
|
|
def test_orchestrator_creates_default_components_if_not_provided(self):
|
|
"""Orchestrator should create default components if not provided."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
orchestrator = Orchestrator()
|
|
|
|
assert orchestrator.tmux_manager is not None
|
|
assert orchestrator.formatter is not None
|
|
|
|
|
|
class TestCommandParsing:
|
|
"""Tests for command parsing."""
|
|
|
|
def test_command_parsing_extracts_gsd_pattern(self):
|
|
"""Command parsing should extract /gsd:* pattern."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
orchestrator = Orchestrator()
|
|
|
|
# Test valid commands
|
|
result = orchestrator.parse_command("/gsd:status")
|
|
assert result == ("status", "")
|
|
|
|
result = orchestrator.parse_command("/gsd:plan feature-x")
|
|
assert result == ("plan", "feature-x")
|
|
|
|
result = orchestrator.parse_command("/gsd:use my-project")
|
|
assert result == ("use", "my-project")
|
|
|
|
def test_command_parsing_returns_none_for_non_gsd(self):
|
|
"""Command parsing should return None for non-gsd commands."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
orchestrator = Orchestrator()
|
|
|
|
result = orchestrator.parse_command("/help")
|
|
assert result is None
|
|
|
|
result = orchestrator.parse_command("regular message")
|
|
assert result is None
|
|
|
|
|
|
class TestHandleMessage:
|
|
"""Tests for handle_message routing."""
|
|
|
|
@pytest.fixture
|
|
def mock_orchestrator(self):
|
|
"""Create orchestrator with mocked dependencies."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
mock_tmux_manager = MagicMock()
|
|
mock_formatter = MagicMock()
|
|
mock_executor = MagicMock()
|
|
mock_approval_handler = MagicMock()
|
|
|
|
orchestrator = Orchestrator(
|
|
tmux_manager=mock_tmux_manager,
|
|
formatter=mock_formatter
|
|
)
|
|
orchestrator.executor = mock_executor
|
|
orchestrator.approval_handler = mock_approval_handler
|
|
|
|
return orchestrator
|
|
|
|
def test_handle_message_routes_to_session_creation_when_no_session(self, mock_orchestrator):
|
|
"""handle_message should create session when no session exists."""
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_orchestrator.tmux_manager.get_session.return_value = None
|
|
mock_orchestrator.tmux_manager.create_session.return_value = mock_tmux_session
|
|
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Command executed",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
result = mock_orchestrator.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify session was created
|
|
mock_orchestrator.tmux_manager.create_session.assert_called_once()
|
|
|
|
def test_handle_message_executes_command_and_returns_success_card(self, mock_orchestrator):
|
|
"""handle_message should execute command and return success card."""
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_tmux_session.tmux_session = MagicMock()
|
|
mock_tmux_session.tmux_session.active_pane = MagicMock()
|
|
mock_orchestrator.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Status: OK",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
# Ensure no approval is detected
|
|
mock_orchestrator.approval_handler.detect_approval_request.return_value = None
|
|
mock_orchestrator.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = mock_orchestrator.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify command was executed
|
|
mock_orchestrator.executor.execute.assert_called_once()
|
|
# Verify success card was returned
|
|
mock_orchestrator.formatter.format_success.assert_called_once()
|
|
|
|
def test_handle_message_returns_error_card_when_command_fails(self, mock_orchestrator):
|
|
"""handle_message should return error card when command fails."""
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_tmux_session.tmux_session = MagicMock()
|
|
mock_tmux_session.tmux_session.active_pane = MagicMock()
|
|
mock_orchestrator.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Error: Command failed",
|
|
completed=True,
|
|
has_error=True,
|
|
error_type="error"
|
|
)
|
|
|
|
# Ensure no approval is detected
|
|
mock_orchestrator.approval_handler.detect_approval_request.return_value = None
|
|
mock_orchestrator.formatter.format_error.return_value = {"card": "error"}
|
|
|
|
result = mock_orchestrator.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify error card was returned
|
|
mock_orchestrator.formatter.format_error.assert_called_once()
|
|
|
|
def test_handle_message_returns_approval_card_when_approval_detected(self, mock_orchestrator):
|
|
"""handle_message should return approval card when approval is detected."""
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_orchestrator.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="AskUserQuestion: Should I continue?",
|
|
completed=False,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
mock_orchestrator.approval_handler.detect_approval_request.return_value = {
|
|
"type": "AskUserQuestion",
|
|
"matched_text": "AskUserQuestion: Should I continue?"
|
|
}
|
|
mock_orchestrator.approval_handler.create_approval_card.return_value = {"card": "approval"}
|
|
|
|
result = mock_orchestrator.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:plan feature",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify approval detection was called
|
|
mock_orchestrator.approval_handler.detect_approval_request.assert_called_once()
|
|
# Verify approval card was created
|
|
mock_orchestrator.approval_handler.create_approval_card.assert_called_once()
|
|
|
|
def test_handle_message_truncates_output_to_2000_chars(self, mock_orchestrator):
|
|
"""handle_message should truncate output to 2000 chars for card limits."""
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_tmux_session.tmux_session = MagicMock()
|
|
mock_tmux_session.tmux_session.active_pane = MagicMock()
|
|
mock_orchestrator.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
# Create long output
|
|
long_output = "x" * 3000
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output=long_output,
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
# Ensure no approval is detected
|
|
mock_orchestrator.approval_handler.detect_approval_request.return_value = None
|
|
mock_orchestrator.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = mock_orchestrator.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify content was truncated
|
|
call_args = mock_orchestrator.formatter.format_success.call_args
|
|
content = call_args[1]["content"]
|
|
assert len(content) <= 2000
|
|
|
|
def test_handle_message_returns_error_on_exception(self, mock_orchestrator):
|
|
"""handle_message should return error card when exception occurs."""
|
|
mock_orchestrator.tmux_manager.get_session.side_effect = Exception("Session error")
|
|
|
|
mock_orchestrator.formatter.format_error.return_value = {"card": "error"}
|
|
|
|
result = mock_orchestrator.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify error card was returned
|
|
mock_orchestrator.formatter.format_error.assert_called_once()
|
|
|
|
|
|
class TestHandleCallback:
|
|
"""Tests for handle_callback routing."""
|
|
|
|
@pytest.fixture
|
|
def mock_orchestrator(self):
|
|
"""Create orchestrator with mocked dependencies."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
mock_tmux_manager = MagicMock()
|
|
mock_formatter = MagicMock()
|
|
mock_approval_handler = MagicMock()
|
|
mock_executor = MagicMock()
|
|
|
|
orchestrator = Orchestrator(
|
|
tmux_manager=mock_tmux_manager,
|
|
formatter=mock_formatter
|
|
)
|
|
orchestrator.approval_handler = mock_approval_handler
|
|
orchestrator.executor = mock_executor
|
|
|
|
return orchestrator
|
|
|
|
def test_handle_callback_routes_approval_response_correctly(self, mock_orchestrator):
|
|
"""handle_callback should route approval response correctly."""
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_orchestrator.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
mock_orchestrator.approval_handler.handle_callback.return_value = "approved"
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Continuing...",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
mock_orchestrator.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = mock_orchestrator.handle_callback({
|
|
"action": "approve",
|
|
"approval_id": "test-approval-id",
|
|
"project": "test-project"
|
|
})
|
|
|
|
# Verify callback was handled
|
|
mock_orchestrator.approval_handler.handle_callback.assert_called_once()
|
|
# Verify confirmation was sent
|
|
mock_orchestrator.formatter.format_success.assert_called_once()
|
|
|
|
def test_handle_callback_sends_rejection_to_tmux(self, mock_orchestrator):
|
|
"""handle_callback should send rejection to tmux session."""
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_orchestrator.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
mock_orchestrator.approval_handler.handle_callback.return_value = "rejected"
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Cancelled",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
mock_orchestrator.formatter.format_info.return_value = {"card": "info"}
|
|
|
|
result = mock_orchestrator.handle_callback({
|
|
"action": "reject",
|
|
"approval_id": "test-approval-id",
|
|
"project": "test-project"
|
|
})
|
|
|
|
# Verify callback was handled
|
|
mock_orchestrator.approval_handler.handle_callback.assert_called_once()
|
|
|
|
def test_handle_callback_returns_error_for_unknown_approval(self, mock_orchestrator):
|
|
"""handle_callback should return error for unknown approval."""
|
|
mock_orchestrator.approval_handler.handle_callback.return_value = "error: approval not found"
|
|
|
|
mock_orchestrator.formatter.format_error.return_value = {"card": "error"}
|
|
|
|
result = mock_orchestrator.handle_callback({
|
|
"action": "approve",
|
|
"approval_id": "unknown-id",
|
|
"project": "test-project"
|
|
})
|
|
|
|
# Verify error card was returned
|
|
mock_orchestrator.formatter.format_error.assert_called_once()
|
|
|
|
def test_handle_callback_new_project_action(self, mock_orchestrator):
|
|
"""handle_callback should handle new-project action from error card button."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
mock_orchestrator.tmux_manager.create_session.return_value = mock_session
|
|
mock_orchestrator.tmux_manager.get_session.return_value = None
|
|
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Project created successfully",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
mock_orchestrator.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = mock_orchestrator.handle_callback({
|
|
"action": "new-project",
|
|
"name": "my-new-project",
|
|
"user_id": "user1"
|
|
})
|
|
|
|
# Verify session was created
|
|
mock_orchestrator.tmux_manager.create_session.assert_called_once_with("my-new-project")
|
|
# Verify command was executed
|
|
mock_orchestrator.executor.execute.assert_called_once()
|
|
# Verify success card was returned
|
|
mock_orchestrator.formatter.format_success.assert_called_once()
|
|
|
|
def test_handle_callback_new_project_returns_success_card(self, mock_orchestrator):
|
|
"""handle_callback new-project action should return success card."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
mock_orchestrator.tmux_manager.create_session.return_value = mock_session
|
|
mock_orchestrator.tmux_manager.get_session.return_value = None
|
|
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Project initialized",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
mock_orchestrator.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = mock_orchestrator.handle_callback({
|
|
"action": "new-project",
|
|
"name": "test-project",
|
|
"user_id": "user1"
|
|
})
|
|
|
|
mock_orchestrator.formatter.format_success.assert_called_once()
|
|
|
|
def test_handle_callback_new_project_with_missing_name_returns_error(self, mock_orchestrator):
|
|
"""handle_callback new-project with missing name should return error."""
|
|
mock_orchestrator.formatter.format_error.return_value = {"card": "error"}
|
|
|
|
result = mock_orchestrator.handle_callback({
|
|
"action": "new-project",
|
|
"user_id": "user1"
|
|
# Missing "name" field
|
|
})
|
|
|
|
mock_orchestrator.formatter.format_error.assert_called_once()
|
|
|
|
def test_handle_callback_new_project_updates_user_context(self, mock_orchestrator):
|
|
"""handle_callback new-project should update user context."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
mock_orchestrator.tmux_manager.create_session.return_value = mock_session
|
|
mock_orchestrator.tmux_manager.get_session.return_value = None
|
|
|
|
mock_orchestrator.executor.execute.return_value = CommandResult(
|
|
output="Project created",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
mock_orchestrator.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = mock_orchestrator.handle_callback({
|
|
"action": "new-project",
|
|
"name": "context-project",
|
|
"user_id": "user1"
|
|
})
|
|
|
|
# Verify user context was updated
|
|
user_ctx = mock_orchestrator._get_user_context_manager("user1")
|
|
assert user_ctx.get_current_project() == "context-project"
|
|
|
|
|
|
class TestIntegrationFlows:
|
|
"""Integration-style tests for complete flows."""
|
|
|
|
@pytest.fixture
|
|
def orchestrator_with_real_components(self, tmp_users_root):
|
|
"""Create orchestrator with real components (mock only tmux)."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
from gsd_openclaw.response_formatter import FeishuResponseFormatter
|
|
from gsd_openclaw.approval_handler import ApprovalHandler
|
|
from gsd_openclaw.command_executor import CommandExecutor
|
|
|
|
mock_tmux_manager = MagicMock()
|
|
|
|
formatter = FeishuResponseFormatter()
|
|
approval_handler = ApprovalHandler(formatter)
|
|
|
|
orchestrator = Orchestrator(
|
|
tmux_manager=mock_tmux_manager,
|
|
formatter=formatter,
|
|
users_root=tmp_users_root
|
|
)
|
|
orchestrator.approval_handler = approval_handler
|
|
orchestrator.executor = MagicMock()
|
|
|
|
return orchestrator
|
|
|
|
def test_full_flow_success(self, orchestrator_with_real_components):
|
|
"""Test full flow: handle_message -> session creation -> command execution -> success card."""
|
|
orch = orchestrator_with_real_components
|
|
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
orch.tmux_manager.get_session.return_value = None
|
|
orch.tmux_manager.create_session.return_value = mock_tmux_session
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Status: All systems operational",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify session was created
|
|
orch.tmux_manager.create_session.assert_called_once_with("test-project")
|
|
# Verify result is a card
|
|
assert "schema" in result
|
|
assert result["header"]["template"] == "green" # success theme
|
|
|
|
def test_error_flow(self, orchestrator_with_real_components):
|
|
"""Test error flow: handle_message -> command fails -> error card."""
|
|
orch = orchestrator_with_real_components
|
|
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_tmux_session.tmux_session = MagicMock()
|
|
mock_tmux_session.tmux_session.active_pane = MagicMock()
|
|
orch.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Error: Command execution failed",
|
|
completed=True,
|
|
has_error=True,
|
|
error_type="error"
|
|
)
|
|
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify result is an error card
|
|
assert "schema" in result
|
|
assert result["header"]["template"] == "red" # error theme
|
|
|
|
def test_approval_flow(self, orchestrator_with_real_components):
|
|
"""Test approval flow: handle_message -> approval detected -> approval card."""
|
|
orch = orchestrator_with_real_components
|
|
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
orch.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="AskUserQuestion: Do you want to continue?",
|
|
completed=False,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:plan feature",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify result is an approval card with buttons
|
|
assert "schema" in result
|
|
# Check for action buttons
|
|
elements = result.get("elements", [])
|
|
has_actions = any(e.get("tag") == "action" for e in elements)
|
|
assert has_actions, "Approval card should have action buttons"
|
|
|
|
def test_callback_flow_approval(self, orchestrator_with_real_components):
|
|
"""Test callback flow: handle_callback -> approval processed -> confirmation."""
|
|
orch = orchestrator_with_real_components
|
|
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_tmux_session.tmux_session = MagicMock()
|
|
mock_tmux_session.tmux_session.active_pane = MagicMock()
|
|
orch.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
# First create an approval via handle_message
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="AskUserQuestion: Continue?",
|
|
completed=False,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:plan feature",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Get the approval_id from the approval handler
|
|
approval_ids = list(orch.approval_handler.pending_approvals.keys())
|
|
assert len(approval_ids) > 0, "Approval should be created"
|
|
approval_id = approval_ids[0]
|
|
|
|
# Now simulate callback
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Continuing with plan...",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
result = orch.handle_callback({
|
|
"action": "approve",
|
|
"approval_id": approval_id,
|
|
"project": "test-project"
|
|
})
|
|
|
|
# Verify result is a confirmation card
|
|
assert "schema" in result
|
|
assert result["header"]["template"] == "green" # success theme
|
|
|
|
def test_callback_flow_rejection(self, orchestrator_with_real_components):
|
|
"""Test callback flow: handle_callback -> rejection processed -> confirmation."""
|
|
orch = orchestrator_with_real_components
|
|
|
|
mock_tmux_session = MagicMock(spec=TmuxSession)
|
|
mock_tmux_session.session_id = "test-123"
|
|
mock_tmux_session.tmux_session = MagicMock()
|
|
mock_tmux_session.tmux_session.active_pane = MagicMock()
|
|
orch.tmux_manager.get_session.return_value = mock_tmux_session
|
|
|
|
# First create an approval via handle_message
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="AskUserQuestion: Continue?",
|
|
completed=False,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:plan feature",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Get the approval_id from the approval handler
|
|
approval_ids = list(orch.approval_handler.pending_approvals.keys())
|
|
assert len(approval_ids) > 0, "Approval should be created"
|
|
approval_id = approval_ids[0]
|
|
|
|
# Now simulate rejection callback
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Operation cancelled",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
result = orch.handle_callback({
|
|
"action": "reject",
|
|
"approval_id": approval_id,
|
|
"project": "test-project"
|
|
})
|
|
|
|
# Verify result is a confirmation card
|
|
assert "schema" in result
|
|
|
|
|
|
class TestSpecialCommands:
|
|
"""Tests for special command handling (new-project, use)."""
|
|
|
|
@pytest.fixture
|
|
def orchestrator_with_mocks(self):
|
|
"""Create orchestrator with mocked dependencies for special commands."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
mock_tmux_manager = MagicMock()
|
|
mock_formatter = MagicMock()
|
|
mock_executor = MagicMock()
|
|
|
|
orchestrator = Orchestrator(
|
|
tmux_manager=mock_tmux_manager,
|
|
formatter=mock_formatter
|
|
)
|
|
orchestrator.executor = mock_executor
|
|
|
|
return orchestrator
|
|
|
|
def test_orchestrator_initializes_with_user_context_manager(self, orchestrator_with_mocks):
|
|
"""Orchestrator should initialize with UserContextManager instance."""
|
|
orch = orchestrator_with_mocks
|
|
|
|
# Should have user_context_managers dict
|
|
assert hasattr(orch, 'user_context_managers')
|
|
assert isinstance(orch.user_context_managers, dict)
|
|
|
|
def test_new_project_creates_session_before_executing(self, orchestrator_with_mocks):
|
|
"""_handle_new_project should create tmux session before executing command."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
orch.tmux_manager.create_session.return_value = mock_session
|
|
orch.tmux_manager.get_session.return_value = None
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Project created successfully",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = orch._handle_new_project("user1", "my-project")
|
|
|
|
# Verify session was created first
|
|
orch.tmux_manager.create_session.assert_called_once_with("my-project")
|
|
|
|
def test_new_project_updates_user_context(self, orchestrator_with_mocks):
|
|
"""_handle_new_project should update user context with new project."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
orch.tmux_manager.create_session.return_value = mock_session
|
|
orch.tmux_manager.get_session.return_value = None
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Project created successfully",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = orch._handle_new_project("user1", "my-project")
|
|
|
|
# Verify user context was updated
|
|
user_ctx = orch._get_user_context_manager("user1")
|
|
assert user_ctx.get_current_project() == "my-project"
|
|
|
|
def test_new_project_executes_command_in_session(self, orchestrator_with_mocks):
|
|
"""_handle_new_project should execute the command in the new session."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
orch.tmux_manager.create_session.return_value = mock_session
|
|
orch.tmux_manager.get_session.return_value = None
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Project initialized",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = orch._handle_new_project("user1", "my-project")
|
|
|
|
# Verify command was executed
|
|
orch.executor.execute.assert_called_once()
|
|
call_args = orch.executor.execute.call_args
|
|
assert call_args[0][0] == mock_session
|
|
assert "/gsd:new-project my-project" in call_args[0][1]
|
|
|
|
def test_new_project_returns_success_card(self, orchestrator_with_mocks):
|
|
"""_handle_new_project should return formatted success card."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
orch.tmux_manager.create_session.return_value = mock_session
|
|
orch.tmux_manager.get_session.return_value = None
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Project created",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = orch._handle_new_project("user1", "my-project")
|
|
|
|
orch.formatter.format_success.assert_called_once()
|
|
|
|
def test_handle_message_routes_new_project_command(self, orchestrator_with_mocks):
|
|
"""handle_message should route 'new-project' command to _handle_new_project."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
orch.tmux_manager.create_session.return_value = mock_session
|
|
orch.tmux_manager.get_session.return_value = None
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Project created",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
# Patch _handle_new_project to verify it's called
|
|
with patch.object(orch, '_handle_new_project', return_value={"card": "mocked"}) as mock_handler:
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:new-project test-project",
|
|
channel="test-channel",
|
|
context={}
|
|
)
|
|
|
|
mock_handler.assert_called_once_with("user1", "test-project")
|
|
|
|
def test_new_project_with_empty_name_returns_error(self, orchestrator_with_mocks):
|
|
"""_handle_new_project with empty project name should return error."""
|
|
orch = orchestrator_with_mocks
|
|
|
|
orch.formatter.format_error.return_value = {"card": "error"}
|
|
|
|
result = orch._handle_new_project("user1", "")
|
|
|
|
orch.formatter.format_error.assert_called_once()
|
|
call_args = orch.formatter.format_error.call_args
|
|
assert "Project name" in str(call_args)
|
|
|
|
# ===== Task 2: /gsd:use command tests =====
|
|
|
|
def test_use_project_updates_user_context(self, orchestrator_with_mocks):
|
|
"""_handle_use_project should update user context to new project."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
orch.tmux_manager.get_session.return_value = mock_session
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = orch._handle_use_project("user1", "existing-project")
|
|
|
|
# Verify user context was updated
|
|
user_ctx = orch._get_user_context_manager("user1")
|
|
assert user_ctx.get_current_project() == "existing-project"
|
|
|
|
def test_use_project_returns_success_when_project_exists(self, orchestrator_with_mocks):
|
|
"""_handle_use_project should return success card when project exists."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
orch.tmux_manager.get_session.return_value = mock_session
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
result = orch._handle_use_project("user1", "existing-project")
|
|
|
|
orch.formatter.format_success.assert_called_once()
|
|
|
|
def test_use_project_returns_error_with_button_when_not_exists(self, orchestrator_with_mocks):
|
|
"""_handle_use_project should return error card with Create button when project doesn't exist."""
|
|
from gsd_openclaw.response_formatter import CardButton
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
orch.tmux_manager.get_session.return_value = None
|
|
|
|
orch.formatter.format_error.return_value = {"card": "error"}
|
|
|
|
result = orch._handle_use_project("user1", "nonexistent-project")
|
|
|
|
# Verify error card was returned with buttons
|
|
orch.formatter.format_error.assert_called_once()
|
|
call_args = orch.formatter.format_error.call_args
|
|
# Check that buttons argument was passed
|
|
assert call_args[1].get("buttons") is not None
|
|
|
|
def test_handle_message_routes_use_command(self, orchestrator_with_mocks):
|
|
"""handle_message should route 'use' command to _handle_use_project."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
orch.tmux_manager.get_session.return_value = mock_session
|
|
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
# Patch _handle_use_project to verify it's called
|
|
with patch.object(orch, '_handle_use_project', return_value={"card": "mocked"}) as mock_handler:
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:use my-project",
|
|
channel="test-channel",
|
|
context={}
|
|
)
|
|
|
|
mock_handler.assert_called_once_with("user1", "my-project")
|
|
|
|
def test_use_project_with_no_args_returns_error(self, orchestrator_with_mocks):
|
|
"""_handle_use_project with no args should return error card."""
|
|
orch = orchestrator_with_mocks
|
|
|
|
orch.formatter.format_error.return_value = {"card": "error"}
|
|
|
|
result = orch._handle_use_project("user1", "")
|
|
|
|
orch.formatter.format_error.assert_called_once()
|
|
call_args = orch.formatter.format_error.call_args
|
|
assert "Project name" in str(call_args)
|
|
|
|
def test_subsequent_commands_use_project_from_user_context(self, orchestrator_with_mocks):
|
|
"""Generic commands should use project from user context after /gsd:use."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
# First set up user context with a project
|
|
user_ctx = orch._get_user_context_manager("user1")
|
|
user_ctx.set_current_project("context-project")
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
orch.tmux_manager.get_session.return_value = mock_session
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Status: OK",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
# Call a generic command without project in context
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={} # No project in context
|
|
)
|
|
|
|
# Verify get_session was called with the project from user context
|
|
orch.tmux_manager.get_session.assert_called_with("context-project")
|
|
|
|
|
|
class TestAdaptiveTimeout:
|
|
"""Tests for adaptive timeout based on command type."""
|
|
|
|
@pytest.fixture
|
|
def orchestrator_with_mocks(self):
|
|
"""Create orchestrator with mocked dependencies for timeout tests."""
|
|
from gsd_openclaw.orchestrator import Orchestrator
|
|
|
|
mock_tmux_manager = MagicMock()
|
|
mock_formatter = MagicMock()
|
|
mock_executor = MagicMock()
|
|
mock_executor.DEFAULT_TIMEOUT = 60.0 # Set real timeout value on mock
|
|
|
|
orchestrator = Orchestrator(
|
|
tmux_manager=mock_tmux_manager,
|
|
formatter=mock_formatter
|
|
)
|
|
orchestrator.executor = mock_executor
|
|
|
|
return orchestrator
|
|
|
|
def test_orchestrator_has_long_commands_set(self, orchestrator_with_mocks):
|
|
"""Orchestrator should have LONG_COMMANDS set defined."""
|
|
orch = orchestrator_with_mocks
|
|
|
|
assert hasattr(orch, 'LONG_COMMANDS')
|
|
assert 'execute-phase' in orch.LONG_COMMANDS
|
|
assert 'plan-phase' in orch.LONG_COMMANDS
|
|
assert 'discuss-phase' in orch.LONG_COMMANDS
|
|
assert 'new-project' in orch.LONG_COMMANDS
|
|
|
|
def test_is_long_command_returns_true_for_long_commands(self, orchestrator_with_mocks):
|
|
"""_is_long_command should return True for commands in LONG_COMMANDS."""
|
|
orch = orchestrator_with_mocks
|
|
|
|
assert orch._is_long_command('execute-phase') is True
|
|
assert orch._is_long_command('plan-phase') is True
|
|
assert orch._is_long_command('discuss-phase') is True
|
|
assert orch._is_long_command('new-project') is True
|
|
|
|
def test_is_long_command_returns_false_for_regular_commands(self, orchestrator_with_mocks):
|
|
"""_is_long_command should return False for commands not in LONG_COMMANDS."""
|
|
orch = orchestrator_with_mocks
|
|
|
|
assert orch._is_long_command('status') is False
|
|
assert orch._is_long_command('list') is False
|
|
assert orch._is_long_command('help') is False
|
|
assert orch._is_long_command('use') is False
|
|
|
|
def test_execute_called_with_none_timeout_for_long_commands(self, orchestrator_with_mocks):
|
|
"""execute() should be called with timeout=None for long commands."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
mock_session.tmux_session.active_pane = MagicMock()
|
|
orch.tmux_manager.get_session.return_value = mock_session
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Executing phase...",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
# Call a long command
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:execute-phase 01-core",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify execute was called with timeout=None
|
|
call_args = orch.executor.execute.call_args
|
|
assert call_args[1].get('timeout') is None
|
|
|
|
def test_execute_called_with_default_timeout_for_regular_commands(self, orchestrator_with_mocks):
|
|
"""execute() should be called with DEFAULT_TIMEOUT for regular commands."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
mock_session.tmux_session.active_pane = MagicMock()
|
|
orch.tmux_manager.get_session.return_value = mock_session
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Status: OK",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
# Call a regular command
|
|
result = orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:status",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
|
|
# Verify execute was called with DEFAULT_TIMEOUT (60.0)
|
|
call_args = orch.executor.execute.call_args
|
|
assert call_args[1].get('timeout') == 60.0
|
|
|
|
def test_handle_message_passes_correct_timeout_based_on_command_type(self, orchestrator_with_mocks):
|
|
"""handle_message should pass correct timeout based on command type."""
|
|
from gsd_openclaw.tmux_session import TmuxSession
|
|
|
|
orch = orchestrator_with_mocks
|
|
|
|
mock_session = MagicMock(spec=TmuxSession)
|
|
mock_session.session_id = "test-123"
|
|
mock_session.tmux_session = MagicMock()
|
|
mock_session.tmux_session.active_pane = MagicMock()
|
|
orch.tmux_manager.get_session.return_value = mock_session
|
|
|
|
orch.executor.execute.return_value = CommandResult(
|
|
output="Output",
|
|
completed=True,
|
|
has_error=False,
|
|
error_type=None
|
|
)
|
|
orch.formatter.format_success.return_value = {"card": "success"}
|
|
|
|
# Test plan-phase (long command)
|
|
orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:plan-phase feature-x",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
call_args = orch.executor.execute.call_args
|
|
assert call_args[1].get('timeout') is None
|
|
|
|
# Reset mock
|
|
orch.executor.execute.reset_mock()
|
|
|
|
# Test discuss-phase (long command)
|
|
orch.handle_message(
|
|
user_id="user1",
|
|
message="/gsd:discuss-phase about architecture",
|
|
channel="test-channel",
|
|
context={"project": "test-project"}
|
|
)
|
|
call_args = orch.executor.execute.call_args
|
|
assert call_args[1].get('timeout') is None
|