2026-07-02 03:28:19 +08:00
|
|
|
|
"""channels 限界上下文单元测试跨层通用 fixture。
|
|
|
|
|
|
|
|
|
|
|
|
本文件提供横跨 core / application / adapters / plugins 各层复用的被驱动
|
|
|
|
|
|
端口桩(LoggerPort / ConfigPort / CachePort / EventPublisherPort /
|
|
|
|
|
|
TracerPort)。桩使用 ``unittest.mock`` 的 ``MagicMock`` / ``AsyncMock``,
|
|
|
|
|
|
不实现真实逻辑;端口方法签名以 ``yuxi.channels.contract.ports.driven``
|
|
|
|
|
|
下的 Protocol 为准。
|
|
|
|
|
|
|
|
|
|
|
|
注意:LoggerPort / TracerPort 的方法在端口协议中均为 ``async def``,因此
|
|
|
|
|
|
此处使用 ``MagicMock(spec=...)`` —— MagicMock 会自动为 async 方法生成
|
|
|
|
|
|
AsyncMock(可 ``await``),与端口协议保持一致。单文件独有的 helper 应保
|
|
|
|
|
|
留在对应测试文件内(见 testing-guidelines.md)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.cache_port import CachePort
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.config_port import ConfigPort
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.event_publisher_port import (
|
|
|
|
|
|
EventPublisherPort,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven.tracer_port import TracerPort
|
2026-07-06 20:52:20 +08:00
|
|
|
|
from yuxi.channels.core.service.config_manager import ConfigManager
|
2026-07-02 03:28:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def fake_logger() -> LoggerPort:
|
|
|
|
|
|
"""LoggerPort 桩。
|
|
|
|
|
|
|
|
|
|
|
|
``MagicMock(spec=LoggerPort)`` 自动为 ``log`` / ``debug`` / ``info`` /
|
|
|
|
|
|
``warn`` / ``error`` / ``exception`` 生成 AsyncMock(可 ``await``),
|
|
|
|
|
|
默认返回 None。需要断言调用参数时直接使用对应方法的 ``assert_*``。
|
|
|
|
|
|
"""
|
|
|
|
|
|
return MagicMock(spec=LoggerPort)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def fake_config() -> ConfigPort:
|
|
|
|
|
|
"""ConfigPort 桩,``get`` 默认返回 None,``update`` 默认返回 None。"""
|
|
|
|
|
|
mock = AsyncMock(spec=ConfigPort)
|
|
|
|
|
|
mock.get.return_value = None
|
|
|
|
|
|
mock.update.return_value = None
|
|
|
|
|
|
return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-06 20:52:20 +08:00
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def fake_config_manager() -> ConfigManager:
|
|
|
|
|
|
"""ConfigManager 桩。
|
|
|
|
|
|
|
|
|
|
|
|
``AsyncMock(spec=ConfigManager)`` 自动为 ``update`` / ``updateInternal``
|
|
|
|
|
|
/ ``rollback`` 等 async 方法生成 AsyncMock。``update`` /
|
|
|
|
|
|
``updateInternal`` 默认返回 None,调用方按需覆盖返回值。
|
|
|
|
|
|
"""
|
|
|
|
|
|
mock = AsyncMock(spec=ConfigManager)
|
|
|
|
|
|
mock.update.return_value = None
|
|
|
|
|
|
mock.updateInternal.return_value = None
|
|
|
|
|
|
return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-02 03:28:19 +08:00
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def fake_cache() -> CachePort:
|
|
|
|
|
|
"""CachePort 桩,预设常用方法返回值。
|
|
|
|
|
|
|
|
|
|
|
|
``get`` 返回 None,``set`` / ``delete`` / ``expire`` /
|
|
|
|
|
|
``acquireAdvisoryLock`` / ``releaseAdvisoryLock`` / ``ping`` 返回 True,
|
|
|
|
|
|
``incr`` 返回 1。
|
|
|
|
|
|
"""
|
|
|
|
|
|
mock = AsyncMock(spec=CachePort)
|
|
|
|
|
|
mock.get.return_value = None
|
|
|
|
|
|
mock.set.return_value = True
|
|
|
|
|
|
mock.delete.return_value = True
|
|
|
|
|
|
mock.incr.return_value = 1
|
|
|
|
|
|
mock.expire.return_value = True
|
|
|
|
|
|
mock.acquireAdvisoryLock.return_value = True
|
|
|
|
|
|
mock.releaseAdvisoryLock.return_value = True
|
|
|
|
|
|
mock.ping.return_value = True
|
|
|
|
|
|
return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def fake_event_publisher() -> EventPublisherPort:
|
|
|
|
|
|
"""EventPublisherPort 桩,``publish`` 默认返回 None。"""
|
|
|
|
|
|
mock = AsyncMock(spec=EventPublisherPort)
|
|
|
|
|
|
mock.publish.return_value = None
|
|
|
|
|
|
return mock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def fake_tracer() -> TracerPort:
|
|
|
|
|
|
"""TracerPort 桩。
|
|
|
|
|
|
|
|
|
|
|
|
``MagicMock(spec=TracerPort)`` 自动为 ``startSpan`` / ``endSpan`` /
|
|
|
|
|
|
``getCurrentTrace`` 生成 AsyncMock(可 ``await``)。``startSpan`` 默认
|
|
|
|
|
|
返回 Span-like MagicMock,``endSpan`` 返回 None,``getCurrentTrace``
|
|
|
|
|
|
返回 Option-like MagicMock,调用方可按需覆盖。
|
|
|
|
|
|
"""
|
|
|
|
|
|
mock = MagicMock(spec=TracerPort)
|
|
|
|
|
|
mock.startSpan.return_value = MagicMock()
|
|
|
|
|
|
mock.endSpan.return_value = None
|
|
|
|
|
|
mock.getCurrentTrace.return_value = MagicMock()
|
|
|
|
|
|
return mock
|
2026-07-09 04:23:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
|
def fake_plugin_host() -> MagicMock:
|
|
|
|
|
|
"""PluginHost 桩(跨层共享)。
|
|
|
|
|
|
|
|
|
|
|
|
``MagicMock()`` 模拟 ``PluginHost`` 协议
|
|
|
|
|
|
(``yuxi.channels.contract.plugin.entry.PluginHost``):
|
|
|
|
|
|
- 所有 ``getXxxPort()`` 返回 ``MagicMock`` 桩(调用方可覆盖为带
|
|
|
|
|
|
``spec`` 的端口桩,如 ``host.getLoggerPort.return_value = fake_logger``);
|
|
|
|
|
|
- ``registerAdapter()`` / ``registerStageSlot()`` / ``registerEventSubscription()``
|
|
|
|
|
|
/ ``registerConfigSource()`` / ``registerMatchTier()`` / ``registerMatcher()``
|
|
|
|
|
|
返回 ``None``;
|
|
|
|
|
|
- ``publishEvent`` 为 ``AsyncMock``(可 ``await``),返回 ``None``。
|
|
|
|
|
|
|
|
|
|
|
|
本 fixture 提升至 channels 限界上下文根 conftest,供 application/lifecycle
|
|
|
|
|
|
与 plugins 各层测试复用(避免在子目录 conftest 重复定义)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
host = MagicMock()
|
|
|
|
|
|
# 端口获取:返回 MagicMock 桩,调用方可覆盖为带 spec 的端口桩
|
|
|
|
|
|
host.getConfigPort.return_value = MagicMock()
|
|
|
|
|
|
host.getLoggerPort.return_value = MagicMock()
|
|
|
|
|
|
host.getPersistencePort.return_value = MagicMock()
|
|
|
|
|
|
host.getCachePort.return_value = MagicMock()
|
|
|
|
|
|
host.getTracerPort.return_value = MagicMock()
|
|
|
|
|
|
host.getQueuePort.return_value = MagicMock()
|
|
|
|
|
|
host.getConversationPort.return_value = MagicMock()
|
|
|
|
|
|
host.getAgentRunPort.return_value = MagicMock()
|
|
|
|
|
|
host.getIdentityResolverPort.return_value = MagicMock()
|
|
|
|
|
|
host.getChannelAccountRepositoryPort.return_value = MagicMock()
|
|
|
|
|
|
host.getChannelSessionRepositoryPort.return_value = MagicMock()
|
|
|
|
|
|
host.getPairingRepositoryPort.return_value = MagicMock()
|
|
|
|
|
|
host.getAuditLogRepositoryPort.return_value = MagicMock()
|
|
|
|
|
|
host.getOutboxRepositoryPort.return_value = MagicMock()
|
|
|
|
|
|
host.getUserIdentityRepositoryPort.return_value = MagicMock()
|
|
|
|
|
|
host.getIdempotencyRepositoryPort.return_value = MagicMock()
|
|
|
|
|
|
host.getPersistenceHealthPort.return_value = MagicMock()
|
|
|
|
|
|
# 扩展点注册:返回 None
|
|
|
|
|
|
host.registerAdapter.return_value = None
|
|
|
|
|
|
host.registerStageSlot.return_value = None
|
|
|
|
|
|
host.registerEventSubscription.return_value = None
|
|
|
|
|
|
host.registerConfigSource.return_value = None
|
|
|
|
|
|
host.registerMatchTier.return_value = None
|
|
|
|
|
|
host.registerMatcher.return_value = None
|
|
|
|
|
|
# 事件发布:async def,使用 AsyncMock
|
|
|
|
|
|
host.publishEvent = AsyncMock(return_value=None)
|
|
|
|
|
|
return host
|