feat(03-02): implement ApprovalHandler class

- 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>
This commit is contained in:
2026-03-11 16:03:54 +08:00
parent efd4892f2f
commit ddd0477498
2 changed files with 249 additions and 6 deletions
+6 -6
View File
@@ -60,8 +60,8 @@ class TestApprovalHandlerCardCreation:
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] == "需要确认"
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'."""
@@ -79,8 +79,8 @@ class TestApprovalHandlerCardCreation:
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)
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)
@@ -103,8 +103,8 @@ class TestApprovalHandlerCardCreation:
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)
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)