diff --git a/gsd-openclaw/src/gsd_openclaw/orchestrator.py b/gsd-openclaw/src/gsd_openclaw/orchestrator.py index 5043d83..a5575b2 100644 --- a/gsd-openclaw/src/gsd_openclaw/orchestrator.py +++ b/gsd-openclaw/src/gsd_openclaw/orchestrator.py @@ -323,22 +323,38 @@ class Orchestrator: ) def handle_callback(self, callback_data: Dict[str, Any]) -> Dict[str, Any]: - """Handle a button callback from an approval card. + """Handle a button callback from a card. - This processes approval/reject actions and sends the - response to the tmux session. + This processes various actions including: + - approve/reject: For approval workflows + - new-project: For creating new projects from error cards Args: callback_data: Dictionary containing: - - action: "approve" or "reject" - - approval_id: The ID of the approval - - project: The project name + - action: "approve", "reject", or "new-project" + - For approvals: approval_id, project + - For new-project: name, user_id Returns: Feishu Card JSON dict as confirmation response. """ try: action = callback_data.get("action", "") + + # Handle new-project action from error card button + if action == "new-project": + project_name = callback_data.get("name", "") + user_id = callback_data.get("user_id", "") + + if not project_name: + return self.formatter.format_error( + title="Invalid Request", + content="Project name is required" + ) + + return self._handle_new_project(user_id, project_name) + + # Handle approval/reject actions approval_id = callback_data.get("approval_id", "") project_name = callback_data.get("project", "default") diff --git a/gsd-openclaw/tests/test_orchestrator.py b/gsd-openclaw/tests/test_orchestrator.py index 4433a98..51b5f73 100644 --- a/gsd-openclaw/tests/test_orchestrator.py +++ b/gsd-openclaw/tests/test_orchestrator.py @@ -340,6 +340,103 @@ class TestHandleCallback: # 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."""