test(03-02): add failing tests for approval handler and progress monitor
- Add test_approval_handler.py with 8 tests for: - Approval request detection (AskUserQuestion, 需要确认) - Approval card creation with confirm/reject buttons - Callback handling (approved, rejected, error) - Add test_progress_monitor.py with 7 tests for: - Progress extraction (percentage, fraction, phase) - Update throttling (60 second interval) - Progress card formatting TDD RED phase - tests will fail until implementation is complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
@@ -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 == "正在执行..."
|
||||
Reference in New Issue
Block a user