feat(04-03): add adaptive timeout to Orchestrator
- Add _is_long_command method to detect long-running commands - Update handle_message to use adaptive timeout based on command type - Long commands (execute-phase, plan-phase, etc.) use timeout=None - Regular commands use DEFAULT_TIMEOUT (60 seconds) - Add TestAdaptiveTimeout test class with 6 test cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,17 @@ class Orchestrator:
|
||||
self.user_context_managers[user_id] = UserContextManager(user_id, self._users_root)
|
||||
return self.user_context_managers[user_id]
|
||||
|
||||
def _is_long_command(self, command: str) -> bool:
|
||||
"""Check if command requires no timeout.
|
||||
|
||||
Args:
|
||||
command: The command name (e.g., 'execute-phase', 'status').
|
||||
|
||||
Returns:
|
||||
True if command is in LONG_COMMANDS, False otherwise.
|
||||
"""
|
||||
return command in self.LONG_COMMANDS
|
||||
|
||||
def _handle_new_project(self, user_id: str, project_name: str) -> Dict[str, Any]:
|
||||
"""Handle /gsd:new-project command.
|
||||
|
||||
@@ -273,8 +284,11 @@ class Orchestrator:
|
||||
if args:
|
||||
full_command = f"{full_command} {args}"
|
||||
|
||||
# Execute command
|
||||
result = self.executor.execute(session, full_command)
|
||||
# Determine timeout based on command type
|
||||
timeout = None if self._is_long_command(command) else self.executor.DEFAULT_TIMEOUT
|
||||
|
||||
# Execute command with adaptive timeout
|
||||
result = self.executor.execute(session, full_command, timeout=timeout)
|
||||
|
||||
# Truncate output if needed
|
||||
output = result.output
|
||||
|
||||
@@ -848,3 +848,160 @@ class TestSpecialCommands:
|
||||
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user