feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
# =============================================================================
|
||||
# 复杂场景重构 — 超时清理定时任务单元测试
|
||||
# =============================================================================
|
||||
# 测试范围:timeout_cleaner.py
|
||||
# - run_once 24h 超时自动关闭
|
||||
# - run_once 无超时会话时不操作
|
||||
# - run_once Redis 恢复点清理
|
||||
# - run_once 推送 WS 超时关闭事件
|
||||
#
|
||||
# 技术说明:TimeoutCleaner.run_once() 内部使用 db_factory 创建独立 DB 会话并
|
||||
# 调用 commit()。测试中通过 MockDbFactory 复用 db_session(SAVEPOINT),
|
||||
# 并将 commit 替换为 flush,避免干扰 conftest 的事务回滚机制。
|
||||
# =============================================================================
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.constants import (
|
||||
PAUSE_TIMEOUT_HOURS,
|
||||
REDIS_KEY_PAUSED_SESSIONS,
|
||||
REDIS_KEY_RESUME_POINT,
|
||||
)
|
||||
from app.models.automation import AutoSession
|
||||
from app.services.automation.timeout_cleaner import TimeoutCleaner
|
||||
|
||||
|
||||
class MockDbFactory:
|
||||
"""模拟 DB 会话工厂,复用 db_session 的 SAVEPOINT 事务。
|
||||
|
||||
run_once() 内部调用 await db.commit(),在 SAVEPOINT 环境下会导致
|
||||
事务提前提交。此处将 commit 替换为 flush,确保数据写入 SAVEPOINT
|
||||
但不提交外层事务,测试结束后由 conftest 回滚清理。
|
||||
"""
|
||||
|
||||
def __init__(self, db_session):
|
||||
self._db = db_session
|
||||
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
self._db.commit = AsyncMock(side_effect=self._db.flush)
|
||||
return self._db
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class TestTimeoutCleaner:
|
||||
"""超时清理器测试用例"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner(self, db_session, mock_redis):
|
||||
"""测试 24h 超时自动关闭"""
|
||||
# Arrange — 创建一个 25 小时前暂停的会话
|
||||
session = AutoSession(
|
||||
employee_id="emp_001",
|
||||
title="超时任务",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=25),
|
||||
)
|
||||
db_session.add(session)
|
||||
await db_session.flush()
|
||||
|
||||
# Act
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), mock_redis)
|
||||
closed_count = await cleaner.run_once()
|
||||
|
||||
# Assert
|
||||
assert closed_count == 1
|
||||
await db_session.refresh(session)
|
||||
assert session.status == "closed"
|
||||
assert session.closed_by == "system(timeout)"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner_no_expired(self, db_session, mock_redis):
|
||||
"""测试无超时会话时不操作"""
|
||||
# Arrange — 创建一个刚暂停的会话(未超时)
|
||||
session = AutoSession(
|
||||
employee_id="emp_001",
|
||||
title="近期任务",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=1),
|
||||
)
|
||||
db_session.add(session)
|
||||
await db_session.flush()
|
||||
|
||||
# Act
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), mock_redis)
|
||||
closed_count = await cleaner.run_once()
|
||||
|
||||
# Assert
|
||||
assert closed_count == 0
|
||||
await db_session.refresh(session)
|
||||
assert session.status == "paused"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner_redis_cleanup(self, db_session, mock_redis):
|
||||
"""测试超时关闭时清理 Redis 恢复点"""
|
||||
# Arrange
|
||||
session = AutoSession(
|
||||
employee_id="emp_redis_test",
|
||||
title="Redis清理测试",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=25),
|
||||
)
|
||||
db_session.add(session)
|
||||
await db_session.flush()
|
||||
session_id = session.id
|
||||
employee_id = session.employee_id
|
||||
|
||||
# 预置 Redis 恢复点和暂停集合
|
||||
resume_key = REDIS_KEY_RESUME_POINT.format(session_id=session_id)
|
||||
await mock_redis.setex(resume_key, 90000, '{"title":"test"}')
|
||||
paused_key = REDIS_KEY_PAUSED_SESSIONS.format(employee_id=employee_id)
|
||||
await mock_redis.sadd(paused_key, session_id)
|
||||
|
||||
# Act
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), mock_redis)
|
||||
await cleaner.run_once()
|
||||
|
||||
# Assert — Redis 恢复点已删除
|
||||
raw = await mock_redis.get(resume_key)
|
||||
assert raw is None
|
||||
|
||||
# Assert — 暂停集合中已移除
|
||||
members = await mock_redis.smembers(paused_key)
|
||||
assert session_id not in members
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner_multiple_sessions(self, db_session, mock_redis):
|
||||
"""测试多个超时会话一次性关闭"""
|
||||
# Arrange
|
||||
sessions = []
|
||||
for i in range(3):
|
||||
s = AutoSession(
|
||||
employee_id=f"emp_{i}",
|
||||
title=f"超时任务{i}",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=26),
|
||||
)
|
||||
db_session.add(s)
|
||||
sessions.append(s)
|
||||
await db_session.flush()
|
||||
|
||||
# Act
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), mock_redis)
|
||||
closed_count = await cleaner.run_once()
|
||||
|
||||
# Assert
|
||||
assert closed_count == 3
|
||||
for s in sessions:
|
||||
await db_session.refresh(s)
|
||||
assert s.status == "closed"
|
||||
assert s.closed_by == "system(timeout)"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner_mixed_sessions(self, db_session, mock_redis):
|
||||
"""测试混合场景 — 只关闭超时的,不关闭未超时的"""
|
||||
# Arrange
|
||||
expired = AutoSession(
|
||||
employee_id="emp_001",
|
||||
title="超时",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=25),
|
||||
)
|
||||
recent = AutoSession(
|
||||
employee_id="emp_002",
|
||||
title="近期",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(minutes=30),
|
||||
)
|
||||
db_session.add_all([expired, recent])
|
||||
await db_session.flush()
|
||||
|
||||
# Act
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), mock_redis)
|
||||
closed_count = await cleaner.run_once()
|
||||
|
||||
# Assert
|
||||
assert closed_count == 1
|
||||
await db_session.refresh(expired)
|
||||
assert expired.status == "closed"
|
||||
await db_session.refresh(recent)
|
||||
assert recent.status == "paused"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner_without_redis(self, db_session):
|
||||
"""测试无 Redis 时超时关闭仍成功"""
|
||||
# Arrange
|
||||
session = AutoSession(
|
||||
employee_id="emp_001",
|
||||
title="无Redis超时",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=25),
|
||||
)
|
||||
db_session.add(session)
|
||||
await db_session.flush()
|
||||
|
||||
# Act — redis=None
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), redis=None)
|
||||
closed_count = await cleaner.run_once()
|
||||
|
||||
# Assert
|
||||
assert closed_count == 1
|
||||
await db_session.refresh(session)
|
||||
assert session.status == "closed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner_boundary_just_expired(self, db_session, mock_redis):
|
||||
"""测试边界 — 刚好超过 24 小时的会话被关闭"""
|
||||
# Arrange — 24小时 + 1分钟前
|
||||
session = AutoSession(
|
||||
employee_id="emp_001",
|
||||
title="边界测试",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=24, minutes=1),
|
||||
)
|
||||
db_session.add(session)
|
||||
await db_session.flush()
|
||||
|
||||
# Act
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), mock_redis)
|
||||
closed_count = await cleaner.run_once()
|
||||
|
||||
# Assert
|
||||
assert closed_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_cleaner_boundary_just_under(self, db_session, mock_redis):
|
||||
"""测试边界 — 差 1 分钟到 24 小时的会话不被关闭"""
|
||||
# Arrange — 23小时59分钟前
|
||||
session = AutoSession(
|
||||
employee_id="emp_001",
|
||||
title="未超时边界",
|
||||
status="paused",
|
||||
paused_at=datetime.now(timezone.utc) - timedelta(hours=23, minutes=59),
|
||||
)
|
||||
db_session.add(session)
|
||||
await db_session.flush()
|
||||
|
||||
# Act
|
||||
cleaner = TimeoutCleaner(MockDbFactory(db_session), mock_redis)
|
||||
closed_count = await cleaner.run_once()
|
||||
|
||||
# Assert
|
||||
assert closed_count == 0
|
||||
Reference in New Issue
Block a user