ddd0477498
- Add ApprovalState dataclass for approval workflow state - Add ApprovalHandler class with: - APPROVAL_PATTERNS for detection (AskUserQuestion, ExitPlanMode, 需要确认, Please confirm) - detect_approval_request() to find approval requests in output - create_approval_card() with confirm/reject buttons using CardButton - handle_callback() to process approval actions - In-memory pending_approvals cache - Fix test assertions to use kwargs instead of positional args All 8 tests pass for approval handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
6.7 KiB
Python
179 lines
6.7 KiB
Python
"""
|
|
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_kwargs = formatter.format_info.call_args.kwargs
|
|
assert call_kwargs.get("title") == "需要确认"
|
|
|
|
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_kwargs = formatter.format_info.call_args.kwargs
|
|
buttons = call_kwargs.get('buttons')
|
|
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_kwargs = formatter.format_info.call_args.kwargs
|
|
buttons = call_kwargs.get('buttons')
|
|
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()
|