Files
kefu/wechat_rpa/tests/test_review_customer_notifications.py
T
2026-09-21 10:34:06 +08:00

182 lines
7.5 KiB
Python

"""Synthetic customer-recipient reminders; no live databases or native sends."""
import json
import sys
import time
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from review_assistant import ReviewAssistantCoordinator, notification_text
ACCOUNT = '1688856770803435'
PEER = '7881302444555666'
CUSTOMER = 'M:12345678'
RECIPIENTS = ['M:' + PEER, f'S:{ACCOUNT}_{PEER}', f'S:{PEER}_{ACCOUNT}']
class SendCancelled(Exception):
pass
class DeliveryUnknown(Exception):
pass
def make_service(tmp_path, recipient):
settings = {'review_assistant_accounts': {ACCOUNT: {
'enabled': True, 'recipient_conv_id': recipient}}}
service = ReviewAssistantCoordinator(tmp_path, lambda: settings, ACCOUNT)
return service, settings
def make_context(conv=CUSTOMER, row=1):
incoming = {'account': ACCOUNT, 'conv_id': conv, 'rowid': row,
'is_self': False, 'sender_id': '12345678',
'server_id': str(100 + row), 'dedup_key': f'{ACCOUNT}:{conv}:server:{100 + row}',
'content': '合成客户问题', 'send_time': time.time()}
return {'account': ACCOUNT, 'conv_id': conv, 'display_name': '合成客户',
'messages': [incoming], 'last_message': incoming}
def add_episode(service):
state = {'account': ACCOUNT, 'conv_id': CUSTOMER, 'display_name': '合成客户',
'awaiting_review': True, 'stage': 'awaiting_review', 'send_state': '',
'review_reason': '合成审核原因', 'reply_text': '未发送的合成草稿'}
state['assistant_review_id'] = service.ensure_review(state, make_context())
assert state['assistant_review_id']
return state
def read_episode(service):
with service._connect() as conn:
return json.loads(conn.execute('SELECT data FROM episodes ORDER BY rowid DESC LIMIT 1').fetchone()[0])
def fake_native(monkeypatch, *, validations=None, failure=None):
"""Mimic native preflight and final guard without loading native modules."""
validator = Mock(return_value={'ok': True})
if validations is not None:
validator.side_effect = validations
delivered = Mock()
sender, db = Mock(), Mock()
def send(*args, guard):
if not guard() or not guard():
raise SendCancelled('合成发送门禁取消')
delivered(*args)
if failure is not None:
raise failure
return {'status': 'confirmed', 'serverId': 'synthetic-909'}
sender.send.side_effect = send
monkeypatch.setitem(sys.modules, 'wecom_native_sender', SimpleNamespace(
NativeSender=Mock(return_value=sender),
discover=lambda: {'accountId': ACCOUNT, 'pid': 123}))
monkeypatch.setitem(sys.modules, 'reply_database', SimpleNamespace(
LiveReplyDatabase=SimpleNamespace(_open_database=lambda **kwargs: db)))
monkeypatch.setitem(sys.modules, 'review_assistant_contacts', SimpleNamespace(validate_recipient=validator))
lock = SimpleNamespace(try_acquire=Mock(return_value=True), release=Mock())
monkeypatch.setitem(sys.modules, 'send_lock', lock)
return validator, sender, db, delivered
@pytest.mark.parametrize('recipient', RECIPIENTS)
def test_customer_recipient_m_and_both_s_orders_send_reminder(tmp_path, monkeypatch, recipient):
service, _ = make_service(tmp_path, recipient)
add_episode(service)
validator, sender, db, delivered = fake_native(monkeypatch)
item = read_episode(service)
service._notify(item)
assert read_episode(service)['notification'] == 'sent'
assert delivered.call_args.args[2:4] == (ACCOUNT, recipient)
assert validator.call_count == 3 # initial check and both native gates
assert all(call.args == (ACCOUNT, recipient) for call in validator.call_args_list)
assert service.has_pending(CUSTOMER) and not service.resumed(CUSTOMER)
sender.close.assert_called_once()
db.close.assert_called_once()
assert '此助理联系人仅接收提醒' in notification_text(item)
assert '私人微信' not in notification_text(item)
@pytest.mark.parametrize('recipient', RECIPIENTS[1:])
def test_assistant_s_incoming_does_not_create_or_approve_customer_review(tmp_path, recipient):
service, _ = make_service(tmp_path, recipient)
state = add_episode(service)
before = read_episode(service)
assistant_state = dict(state, conv_id=recipient, assistant_review_id='')
incoming = make_context(recipient)
assert service.is_recipient(recipient)
assert service.ensure_review(assistant_state, incoming) == ''
assert not service.reconcile(state, incoming)
assert read_episode(service) == before
with service._connect() as conn:
assert conn.execute('SELECT COUNT(*) FROM episodes').fetchone()[0] == 1
@pytest.mark.parametrize('recipient', [f'S:111_{PEER}', f'S:{ACCOUNT}_{ACCOUNT}',
f'R:{ACCOUNT}_{PEER}', 'customer:' + PEER,
'S:bad_' + ACCOUNT, 'M:' + ACCOUNT])
def test_unbound_group_placeholder_or_self_recipient_disabled(tmp_path, recipient):
service, _ = make_service(tmp_path, recipient)
assert not service.enabled()
assert not service.is_recipient(recipient)
assert service.ensure_review({'account': ACCOUNT, 'conv_id': CUSTOMER}, make_context()) == ''
assert not service.path.exists()
@pytest.mark.parametrize('last_check', [{'ok': False, 'reason': '客户关系已解除'},
RuntimeError('合成关系读取失败')])
def test_relationship_revoked_or_unreadable_at_final_native_guard_blocks_send(tmp_path, monkeypatch, last_check):
service, _ = make_service(tmp_path, RECIPIENTS[1])
add_episode(service)
validator, _, _, delivered = fake_native(monkeypatch, validations=[{'ok': True}, {'ok': True}, last_check])
service._notify(read_episode(service))
assert validator.call_count == 3
delivered.assert_not_called()
assert read_episode(service)['notification'] == 'retry'
assert service.has_pending(CUSTOMER) and not service.resumed(CUSTOMER)
def test_recipient_changed_during_fresh_relationship_check_blocks_send(tmp_path, monkeypatch):
service, settings = make_service(tmp_path, RECIPIENTS[1])
add_episode(service)
validator, _, _, delivered = fake_native(monkeypatch)
checks = []
def validate(*args):
checks.append(args)
if len(checks) == 2:
settings['review_assistant_accounts'][ACCOUNT]['recipient_conv_id'] = 'M:999'
return {'ok': True}
validator.side_effect = validate
service._notify(read_episode(service))
assert len(checks) == 2
delivered.assert_not_called()
assert read_episode(service)['notification'] == 'retry'
assert service.has_pending(CUSTOMER)
def test_unknown_notification_survives_restart_without_retry(tmp_path, monkeypatch):
service, settings = make_service(tmp_path, RECIPIENTS[1])
add_episode(service)
_, _, _, delivered = fake_native(monkeypatch, failure=DeliveryUnknown('合成回执未知'))
service._notify(read_episode(service))
delivered.assert_called_once()
assert read_episode(service)['notification'] == 'unknown'
restarted = ReviewAssistantCoordinator(tmp_path, lambda: settings, ACCOUNT)
restarted._notify = Mock()
waits = []
def wait(_timeout):
waits.append(True)
if len(waits) == 2:
restarted._stop.set()
restarted._wake = SimpleNamespace(wait=wait, clear=lambda: None)
restarted._worker()
restarted._notify.assert_not_called()
assert read_episode(restarted)['notification'] == 'unknown'
assert service.has_pending(CUSTOMER) and not service.resumed(CUSTOMER)