ForcePilot/backend/test/unit/channel/outbound/test_publisher.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

130 lines
5.0 KiB
Python

from __future__ import annotations
import json
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channel.constants import CHANNEL_DELIVERED_TTL_SECONDS, CHANNEL_STREAM_KEY
from yuxi.channel.outbound.publisher import publish_channel_message_event
from yuxi.storage.postgres.models_business import Conversation, Message
@pytest.fixture
def redis_mock():
return MagicMock(
exists=AsyncMock(return_value=0),
set=AsyncMock(return_value=True),
setex=AsyncMock(),
xadd=AsyncMock(return_value="123-0"),
)
@pytest.fixture
def conversation():
conv = MagicMock(spec=Conversation)
conv.id = 1
conv.channel_type = "feishu"
conv.channel_metadata = {"account_id": "acc-1"}
conv.thread_id = "thread-1"
return conv
@pytest.fixture
def message():
msg = MagicMock(spec=Message)
msg.id = 10
msg.created_at = datetime(2026, 1, 1, tzinfo=UTC)
msg.request_id = "req-1"
msg.channel_metadata = {"attempts": 2, "next_retry_at": "2026-01-01T00:01:00+00:00"}
msg.delivery_status = "pending"
return msg
async def test_publish_channel_message_event_success(redis_mock, conversation, message):
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
event_id = await publish_channel_message_event(message, conversation, "sk-1")
assert event_id == "123-0"
redis_mock.xadd.assert_awaited_once()
call_args = redis_mock.xadd.call_args
assert call_args.args[0] == CHANNEL_STREAM_KEY
assert json.loads(call_args.args[1]["payload"])["message_id"] == 10
assert call_args.args[1]["attempt"] == "2"
assert call_args.args[1]["next_retry_at"] == "2026-01-01T00:01:00+00:00"
async def test_publish_skips_non_channel_conversation(redis_mock, message):
for channel_type in [None, "web"]:
conv = MagicMock(spec=Conversation)
conv.channel_type = channel_type
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
result = await publish_channel_message_event(message, conv, "sk-1")
assert result is None
async def test_publish_skips_missing_account_id(redis_mock, conversation, message):
conversation.channel_metadata = {}
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
result = await publish_channel_message_event(message, conversation, "sk-1")
assert result is None
async def test_publish_skips_already_delivered(redis_mock, conversation, message):
redis_mock.exists.return_value = 1
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
result = await publish_channel_message_event(message, conversation, "sk-1")
assert result is None
redis_mock.xadd.assert_not_called()
async def test_publish_skips_when_publishing_locked(redis_mock, conversation, message):
redis_mock.set.return_value = None
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
result = await publish_channel_message_event(message, conversation, "sk-1")
assert result is None
redis_mock.xadd.assert_not_called()
async def test_publish_sets_delivered_for_complete_status(redis_mock, conversation, message):
message.delivery_status = "complete"
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
await publish_channel_message_event(message, conversation, "sk-1")
redis_mock.setex.assert_awaited_once_with("ch:delivered:10", CHANNEL_DELIVERED_TTL_SECONDS, "1")
async def test_publish_sets_delivered_for_partial_failed(redis_mock, conversation, message):
message.delivery_status = "partial_failed"
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
await publish_channel_message_event(message, conversation, "sk-1")
redis_mock.setex.assert_awaited_once()
async def test_publish_no_delivered_for_pending(redis_mock, conversation, message):
message.delivery_status = "pending"
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
await publish_channel_message_event(message, conversation, "sk-1")
redis_mock.setex.assert_not_called()
async def test_publish_default_timestamps_and_empty_metadata(redis_mock, conversation):
msg = MagicMock(spec=Message)
msg.id = 11
msg.created_at = None
msg.request_id = None
msg.channel_metadata = None
msg.delivery_status = "pending"
with patch("yuxi.channel.outbound.publisher.get_redis_client", AsyncMock(return_value=redis_mock)):
await publish_channel_message_event(msg, conversation, "sk-1")
payload = json.loads(redis_mock.xadd.call_args.args[1]["payload"])
assert payload["created_at"]
assert payload["request_id"] is None
assert redis_mock.xadd.call_args.args[1]["attempt"] == "0"
assert redis_mock.xadd.call_args.args[1]["next_retry_at"] == ""