# ============================================================================= # 阶段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