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:
2026-03-11 19:01:08 +08:00
parent b817b457e9
commit 774e450ee3
2 changed files with 173 additions and 2 deletions
+16 -2
View File
@@ -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
+157
View File
@@ -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