diff --git a/gsd-openclaw/tests/test_approval_handler.py b/gsd-openclaw/tests/test_approval_handler.py new file mode 100644 index 0000000..a025241 --- /dev/null +++ b/gsd-openclaw/tests/test_approval_handler.py @@ -0,0 +1,178 @@ +""" +Tests for approval_handler module - ApprovalHandler and ApprovalState classes. + +TDD Task 1: Test scaffold for approval workflow handling. +""" +import pytest +from unittest.mock import Mock, MagicMock +import time + +# These imports will fail initially (RED phase) +from gsd_openclaw.response_formatter import FeishuResponseFormatter, CardButton + + +class TestApprovalHandlerDetection: + """Test approval request detection from output.""" + + def test_detect_approval_request_finds_ask_user_question(self): + """detect_approval_request returns context for 'AskUserQuestion' output.""" + from gsd_openclaw.approval_handler import ApprovalHandler + + formatter = Mock(spec=FeishuResponseFormatter) + handler = ApprovalHandler(formatter) + + output = "Some text\nAskUserQuestion: Do you want to proceed?\nMore text" + result = handler.detect_approval_request(output) + + assert result is not None + assert "AskUserQuestion" in result.get("type", "") + + def test_detect_approval_request_finds_chinese_confirm(self): + """detect_approval_request returns context for '需要确认' output.""" + from gsd_openclaw.approval_handler import ApprovalHandler + + formatter = Mock(spec=FeishuResponseFormatter) + handler = ApprovalHandler(formatter) + + output = "正在执行...\n需要确认: 是否继续?\n请回复" + result = handler.detect_approval_request(output) + + assert result is not None + assert "需要确认" in result.get("type", "") + + +class TestApprovalHandlerCardCreation: + """Test approval card creation with buttons.""" + + def test_create_approval_card_has_confirm_title(self): + """create_approval_card has title '需要确认'.""" + from gsd_openclaw.approval_handler import ApprovalHandler + + formatter = Mock(spec=FeishuResponseFormatter) + formatter.format_info = Mock(return_value={ + "schema": "2.0", + "header": {"title": {"content": "需要确认"}, "template": "blue"}, + "elements": [] + }) + handler = ApprovalHandler(formatter) + + context = {"question": "是否继续?"} + card = handler.create_approval_card(context, "session-123", "test-project") + + formatter.format_info.assert_called_once() + call_args = formatter.format_info.call_args + assert call_args[0][0] == "需要确认" + + def test_create_approval_card_has_confirm_button(self): + """create_approval_card has confirm button with action='approve'.""" + from gsd_openclaw.approval_handler import ApprovalHandler + + formatter = Mock(spec=FeishuResponseFormatter) + formatter.format_info = Mock(return_value={ + "schema": "2.0", + "header": {"title": {"content": "需要确认"}, "template": "blue"}, + "elements": [] + }) + handler = ApprovalHandler(formatter) + + context = {"question": "是否继续?"} + card = handler.create_approval_card(context, "session-123", "test-project") + + # Check that format_info was called with buttons + call_args = formatter.format_info.call_args + buttons = call_args[1].get('buttons') or (call_args[0][2] if len(call_args[0]) > 2 else None) + assert buttons is not None + # Find confirm button + confirm_btn = next((b for b in buttons if b.value.get("action") == "approve"), None) + assert confirm_btn is not None + assert confirm_btn.button_type == "primary" + + def test_create_approval_card_has_reject_button(self): + """create_approval_card has reject button with action='reject'.""" + from gsd_openclaw.approval_handler import ApprovalHandler + + formatter = Mock(spec=FeishuResponseFormatter) + formatter.format_info = Mock(return_value={ + "schema": "2.0", + "header": {"title": {"content": "需要确认"}, "template": "blue"}, + "elements": [] + }) + handler = ApprovalHandler(formatter) + + context = {"question": "是否继续?"} + card = handler.create_approval_card(context, "session-123", "test-project") + + # Check that format_info was called with buttons + call_args = formatter.format_info.call_args + buttons = call_args[1].get('buttons') or (call_args[0][2] if len(call_args[0]) > 2 else None) + assert buttons is not None + # Find reject button + reject_btn = next((b for b in buttons if b.value.get("action") == "reject"), None) + assert reject_btn is not None + assert reject_btn.button_type == "danger" + + +class TestApprovalHandlerCallback: + """Test approval callback handling.""" + + def test_handle_callback_returns_approved_for_approve_action(self): + """handle_callback returns 'approved' for approve action.""" + from gsd_openclaw.approval_handler import ApprovalHandler, ApprovalState + + formatter = Mock(spec=FeishuResponseFormatter) + handler = ApprovalHandler(formatter) + + # Create a pending approval first + context = {"question": "是否继续?"} + handler.create_approval_card(context, "session-123", "test-project") + + # Get the approval_id from pending_approvals + approval_id = list(handler.pending_approvals.keys())[0] + + callback_data = { + "action": "approve", + "approval_id": approval_id, + "project": "test-project" + } + result = handler.handle_callback(callback_data) + + assert result == "approved" + + def test_handle_callback_returns_rejected_for_reject_action(self): + """handle_callback returns 'rejected' for reject action.""" + from gsd_openclaw.approval_handler import ApprovalHandler + + formatter = Mock(spec=FeishuResponseFormatter) + handler = ApprovalHandler(formatter) + + # Create a pending approval first + context = {"question": "是否继续?"} + handler.create_approval_card(context, "session-123", "test-project") + + # Get the approval_id from pending_approvals + approval_id = list(handler.pending_approvals.keys())[0] + + callback_data = { + "action": "reject", + "approval_id": approval_id, + "project": "test-project" + } + result = handler.handle_callback(callback_data) + + assert result == "rejected" + + def test_handle_callback_returns_error_for_unknown_approval_id(self): + """handle_callback returns error for unknown approval_id.""" + from gsd_openclaw.approval_handler import ApprovalHandler + + formatter = Mock(spec=FeishuResponseFormatter) + handler = ApprovalHandler(formatter) + + callback_data = { + "action": "approve", + "approval_id": "nonexistent-id", + "project": "test-project" + } + result = handler.handle_callback(callback_data) + + assert "error" in result.lower() diff --git a/gsd-openclaw/tests/test_progress_monitor.py b/gsd-openclaw/tests/test_progress_monitor.py new file mode 100644 index 0000000..99fa94e --- /dev/null +++ b/gsd-openclaw/tests/test_progress_monitor.py @@ -0,0 +1,131 @@ +""" +Tests for progress_monitor module - ProgressMonitor and ProgressInfo classes. + +TDD Task 1: Test scaffold for progress detection and update logic. +""" +import pytest +from unittest.mock import Mock, MagicMock +import time + +# These imports will fail initially (RED phase) +from gsd_openclaw.response_formatter import FeishuResponseFormatter + + +class TestProgressMonitorExtraction: + """Test progress extraction from output.""" + + def test_extract_progress_returns_percentage(self): + """extract_progress returns percentage for 'Progress: 75%'.""" + from gsd_openclaw.progress_monitor import ProgressMonitor, ProgressInfo + + formatter = Mock(spec=FeishuResponseFormatter) + monitor = ProgressMonitor(formatter) + + output = "Working on task...\nProgress: 75%\nAlmost done" + result = monitor.extract_progress(output) + + assert result is not None + assert result.type == "percentage" + assert result.value == 75 + + def test_extract_progress_returns_fraction(self): + """extract_progress returns fraction for '[3/5 tasks]'.""" + from gsd_openclaw.progress_monitor import ProgressMonitor, ProgressInfo + + formatter = Mock(spec=FeishuResponseFormatter) + monitor = ProgressMonitor(formatter) + + output = "Processing...\n[3/5 tasks] completed\nContinuing..." + result = monitor.extract_progress(output) + + assert result is not None + assert result.type == "fraction" + assert result.current == 3 + assert result.total == 5 + + def test_extract_progress_returns_phase(self): + """extract_progress returns phase for 'Phase 2 complete'.""" + from gsd_openclaw.progress_monitor import ProgressMonitor, ProgressInfo + + formatter = Mock(spec=FeishuResponseFormatter) + monitor = ProgressMonitor(formatter) + + output = "Starting work...\nPhase 2 complete\nMoving to next" + result = monitor.extract_progress(output) + + assert result is not None + assert result.type == "phase" + assert result.value == 2 + + +class TestProgressMonitorThrottling: + """Test progress update throttling.""" + + def test_should_send_update_returns_false_immediately(self): + """should_send_update returns False immediately after creation.""" + from gsd_openclaw.progress_monitor import ProgressMonitor + + formatter = Mock(spec=FeishuResponseFormatter) + monitor = ProgressMonitor(formatter, update_interval=60.0) + + result = monitor.should_send_update() + + assert result is False + + def test_should_send_update_returns_true_after_interval(self): + """should_send_update returns True after 60 seconds.""" + from gsd_openclaw.progress_monitor import ProgressMonitor + + formatter = Mock(spec=FeishuResponseFormatter) + monitor = ProgressMonitor(formatter, update_interval=60.0) + + # Simulate time passing by modifying last_update_time + monitor.last_update_time = time.time() - 61.0 + + result = monitor.should_send_update() + + assert result is True + + +class TestProgressMonitorCardCreation: + """Test progress card creation.""" + + def test_create_progress_card_includes_percentage(self): + """create_progress_card includes progress percentage in content.""" + from gsd_openclaw.progress_monitor import ProgressMonitor, ProgressInfo + + formatter = Mock(spec=FeishuResponseFormatter) + formatter.format_info = Mock(return_value={ + "schema": "2.0", + "header": {"title": {"content": "正在执行..."}, "template": "blue"}, + "elements": [] + }) + monitor = ProgressMonitor(formatter) + + progress = ProgressInfo(type="percentage", value=75) + card = monitor.create_progress_card(progress, "Processing data...") + + formatter.format_info.assert_called_once() + call_args = formatter.format_info.call_args + content = call_args[0][1] + assert "75%" in content + + def test_create_progress_card_uses_executing_title(self): + """create_progress_card uses title '正在执行...'.""" + from gsd_openclaw.progress_monitor import ProgressMonitor, ProgressInfo + + formatter = Mock(spec=FeishuResponseFormatter) + formatter.format_info = Mock(return_value={ + "schema": "2.0", + "header": {"title": {"content": "正在执行..."}, "template": "blue"}, + "elements": [] + }) + monitor = ProgressMonitor(formatter) + + progress = ProgressInfo(type="percentage", value=50) + card = monitor.create_progress_card(progress, "Working...") + + formatter.format_info.assert_called_once() + call_args = formatter.format_info.call_args + title = call_args[0][0] + assert title == "正在执行..."