Files
wecom_it_smart_desk/backend/tests/test_automation_executor.py
T
Simon 5e53146a9a test(backend): unit/integration tests for automation, otp, neo4j, contract
新增自动化审批状态机/执行器/意图路由/会话管理、OTP 绑定流程、neo4j 客户端、响应契约、置信度门禁、环境门控、Tier1 API 等测试。
2026-07-09 11:49:50 +08:00

266 lines
8.8 KiB
Python

# =============================================================================
# 阶段5 自动化 - 执行引擎单元测试
# =============================================================================
# 测试范围:executor.py - 双模式执行引擎、风险分级
# =============================================================================
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime, timezone
from app.services.automation.executor import ActionExecutor
from app.models.automation import AutoSession, AutoAction
from app.constants import (
AUTOMATION_SESSION_TERMINAL_STATES,
AUTOMATION_AUTO_EXECUTABLE_RISKS,
)
class TestActionExecutor:
"""动作执行引擎测试用例"""
@pytest.mark.asyncio
async def test_run_skips_terminal_session(self, db_session, mock_redis):
"""测试 run 跳过终态会话"""
# Arrange - 创建终态会话
session = AutoSession(
id="session_terminal_001",
employee_id="emp_001",
status="closed", # 终态
mode="real_exec"
)
db_session.add(session)
await db_session.flush()
executor = ActionExecutor(db_session, mock_redis)
# Act
await executor.run("session_terminal_001")
# Assert - 不应抛出异常,应直接返回
# (由于会话已终态,不会执行任何动作)
@pytest.mark.asyncio
async def test_run_low_risk_auto_execute(self, db_session, mock_redis):
"""测试 run 低风险动作自动执行"""
# Arrange - 创建会话和动作
session = AutoSession(
id="session_exec_001",
employee_id="emp_001",
status="running",
mode="real_exec"
)
db_session.add(session)
await db_session.flush()
action = AutoAction(
session_id=session.id,
action_index=0,
action_type="terminal_locate",
risk_level="read", # 低风险
status="pending",
adapter="lianruan",
title="定位终端",
description="查询终端"
)
db_session.add(action)
await db_session.flush()
executor = ActionExecutor(db_session, mock_redis)
# Mock handler
mock_handler = AsyncMock()
mock_handler.execute = AsyncMock(return_value={"result": "success"})
with patch("app.services.automation.executor.get_handler", return_value=mock_handler):
# Act
await executor.run(session.id)
# Assert - 动作应被标记为成功
await db_session.refresh(action)
assert action.status == "success"
@pytest.mark.asyncio
async def test_run_high_risk_needs_approval(self, db_session, mock_redis):
"""测试 run 高风险动作需要审批"""
# Arrange - 创建会话和高风险动作
session = AutoSession(
id="session_approval_001",
employee_id="emp_001",
status="running",
mode="real_exec"
)
db_session.add(session)
await db_session.flush()
action = AutoAction(
session_id=session.id,
action_index=0,
action_type="virus_quarantine",
risk_level="high", # 高风险
status="pending",
adapter="huorong",
title="隔离终端",
description="隔离并查杀"
)
db_session.add(action)
await db_session.flush()
executor = ActionExecutor(db_session, mock_redis)
# Act
await executor.run(session.id)
# Assert - 动作应进入待审批状态
await db_session.refresh(action)
await db_session.refresh(session)
assert action.status == "await_approval"
assert session.status == "paused"
assert session.current_action_id == action.id
@pytest.mark.asyncio
async def test_run_plan_only_mode_always_needs_approval(self, db_session, mock_redis):
"""测试 run plan_only 模式所有动作都需审批"""
# Arrange - 创建 plan_only 模式会话
session = AutoSession(
id="session_plan_001",
employee_id="emp_001",
status="running",
mode="plan_only" # 仅方案预览
)
db_session.add(session)
await db_session.flush()
action = AutoAction(
session_id=session.id,
action_index=0,
action_type="terminal_locate",
risk_level="read", # 即使是低风险
status="pending",
adapter="lianruan",
title="定位终端",
description="查询终端"
)
db_session.add(action)
await db_session.flush()
executor = ActionExecutor(db_session, mock_redis)
# Act
await executor.run(session.id)
# Assert - 即使是低风险动作,plan_only 模式也需审批
await db_session.refresh(action)
assert action.status == "await_approval"
@pytest.mark.asyncio
async def test_resume_approve_continues_execution(self, db_session, mock_redis):
"""测试 resume 审批通过后继续执行"""
# Arrange - 创建需审批的会话
session = AutoSession(
id="session_resume_001",
employee_id="emp_001",
status="paused",
mode="real_exec"
)
db_session.add(session)
await db_session.flush()
action = AutoAction(
session_id=session.id,
action_index=0,
action_type="virus_quarantine",
risk_level="high",
status="await_approval",
adapter="huorong",
title="隔离终端",
description="隔离"
)
db_session.add(action)
await db_session.flush()
executor = ActionExecutor(db_session, mock_redis)
# Mock handler for execution after approval
mock_handler = AsyncMock()
mock_handler.execute = AsyncMock(return_value={"result": "success"})
with patch("app.services.automation.executor.get_handler", return_value=mock_handler):
# Act - 审批通过
await executor.resume(
session_id=session.id,
action_id=action.id,
decision="approve",
note="同意执行",
approver_id="agent_001"
)
# Assert - 动作变为 approved,会话继续执行
await db_session.refresh(action)
assert action.status == "approved"
assert action.approved_by == "agent_001"
@pytest.mark.asyncio
async def test_resume_reject_handoff(self, db_session, mock_redis):
"""测试 resume 审批驳回转人工"""
# Arrange
session = AutoSession(
id="session_reject_001",
employee_id="emp_001",
status="paused",
mode="real_exec"
)
db_session.add(session)
await db_session.flush()
action = AutoAction(
session_id=session.id,
action_index=0,
action_type="virus_quarantine",
risk_level="high",
status="await_approval",
adapter="huorong",
title="隔离终端",
description="隔离"
)
db_session.add(action)
await db_session.flush()
executor = ActionExecutor(db_session, mock_redis)
# Act - 审批驳回
await executor.resume(
session_id=session.id,
action_id=action.id,
decision="reject",
note="风险太高",
approver_id="agent_001"
)
# Assert - 动作 rejected,会话转人工
await db_session.refresh(action)
await db_session.refresh(session)
assert action.status == "rejected"
assert session.status == "handoff"
assert session.closed_by == "agent_001"
@pytest.mark.asyncio
async def test_auto_executable_risks_constant(self):
"""测试 AUTOMATION_AUTO_EXECUTABLE_RISKS 常量定义正确"""
# Assert - 低风险和只读风险可自动执行
assert "read" in AUTOMATION_AUTO_EXECUTABLE_RISKS
assert "low" in AUTOMATION_AUTO_EXECUTABLE_RISKS
assert "high" not in AUTOMATION_AUTO_EXECUTABLE_RISKS
@pytest.mark.asyncio
async def test_terminal_states_constant(self):
"""测试 AUTOMATION_SESSION_TERMINAL_STATES 终态集合"""
# Assert - 终态包含 closed, handoff, error
assert "closed" in AUTOMATION_SESSION_TERMINAL_STATES
assert "handoff" in AUTOMATION_SESSION_TERMINAL_STATES
assert "error" in AUTOMATION_SESSION_TERMINAL_STATES
assert "running" not in AUTOMATION_SESSION_TERMINAL_STATES
assert "paused" not in AUTOMATION_SESSION_TERMINAL_STATES