test(backend): unit/integration tests for automation, otp, neo4j, contract
新增自动化审批状态机/执行器/意图路由/会话管理、OTP 绑定流程、neo4j 客户端、响应契约、置信度门禁、环境门控、Tier1 API 等测试。
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
# =============================================================================
|
||||
# 阶段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
|
||||
Reference in New Issue
Block a user