Files
cma-management/backend/tests/test_auto_verify_engine.py
T

197 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""auto-verify 验证引擎修复测试 (2026-08-30 P1)
覆盖 verify.py 4 项缺陷修复:
1. OKR progress 防重复累加(同一 plan 重复 verify 不再 +15%
2. KPIValue 回填 entity_id 多租户隔离
3. 取 KPI 最新值按 period <= 当前月 过滤(跨月验证不取未来/历史期间)
4. status "done" → "completed"(枚举外值修正)
"""
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models import ActionPlan, KPIValue, Objective
from tests.conftest import create_test_user, get_token_for_user, auth_header, create_test_kpi
BASE = "/api/cma/verify"
def _seed_objective(db: Session, progress: int = 10) -> Objective:
obj = Objective(
entity_id=1,
title="测试目标",
quarter="2026Q3",
owner="任富海",
progress=progress,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def _seed_plan(db: Session, kpi_id: int, objective_id: int, rule: dict, status: str = "pending") -> ActionPlan:
plan = ActionPlan(
kpi_id=kpi_id,
objective_id=objective_id,
title="测试行动计划",
status=status,
priority="high",
auto_verify_rule=rule,
)
db.add(plan)
db.commit()
db.refresh(plan)
return plan
def _make_rule(kpi_code: str) -> dict:
return {
"kpi_code": kpi_code,
"condition": "LESS_THAN", # actual < target → passed
"target_value": 80,
"notify": False, # 测试不发企微
}
class TestOkrIdempotent:
def test_repeat_verify_does_not_accumulate(self, client: TestClient, db: Session):
"""缺陷1修复:同一 plan 重复 verifyOKR progress 只累加一次"""
create_test_user(db)
token = get_token_for_user(client)
kpi = create_test_kpi(db, kpi_code="IDEM_001", kpi_name="幂等KPI")
obj = _seed_objective(db, progress=10)
plan = _seed_plan(db, kpi.id, obj.id, _make_rule("IDEM_001"))
# 第一次验证通过 → +15%
r1 = client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={"actual_value": 50})
assert r1.status_code == 200
assert r1.json()["passed"] is True
assert r1.json()["okr_progress"]["updated"] is True
assert r1.json()["okr_progress"]["after"] == 25 # 10 + 15
# 第二次验证通过 → 不再累加(保持 25)
r2 = client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={"actual_value": 40})
assert r2.status_code == 200
assert r2.json()["okr_progress"]["updated"] is False
assert r2.json()["okr_progress"]["reason"] == "already_verified"
assert r2.json()["okr_progress"]["after"] == 25 # 保持原值
db.expire_all()
assert db.query(Objective).filter(Objective.id == obj.id).first().progress == 25
def test_force_recalc_accumulates(self, client: TestClient, db: Session):
"""缺陷1修复:force_recalc=True 保留强制重新累加入口"""
create_test_user(db)
token = get_token_for_user(client)
kpi = create_test_kpi(db, kpi_code="IDEM_002", kpi_name="幂等KPI2")
obj = _seed_objective(db, progress=10)
plan = _seed_plan(db, kpi.id, obj.id, _make_rule("IDEM_002"))
client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={"actual_value": 50})
r2 = client.post(f"{BASE}/{plan.id}", headers=auth_header(token),
json={"actual_value": 40, "force_recalc": True})
assert r2.json()["okr_progress"]["updated"] is True
assert r2.json()["okr_progress"]["after"] == 40 # 25 + 15
class TestEntityBackfill:
def test_kpi_value_gets_entity_id(self, client: TestClient, db: Session):
"""缺陷2修复:回填的 KPIValue 带 entity_id(与 KPI 定义一致,非默认1"""
create_test_user(db)
token = get_token_for_user(client)
# entity_id=2 的 KPI(模拟第二个账套)
kpi = create_test_kpi(db, kpi_code="ENT_001", kpi_name="多租户KPI", entity_id=2)
obj = _seed_objective(db)
plan = _seed_plan(db, kpi.id, obj.id, _make_rule("ENT_001"))
r = client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={"actual_value": 60})
assert r.status_code == 200
val = db.query(KPIValue).filter(KPIValue.source_batch == f"verify-plan-{plan.id}").first()
assert val is not None
assert val.entity_id == 2 # 从 KPI 定义继承,而非默认 1
assert val.kpi_id == kpi.id
class TestPeriodFilter:
def test_latest_value_respects_period_limit(self, client: TestClient, db: Session):
"""缺陷3修复:缺省 actual 时只取 period <= 当前月的值,不取未来期间"""
create_test_user(db)
token = get_token_for_user(client)
kpi = create_test_kpi(db, kpi_code="PER_001", kpi_name="期间KPI")
# 未来月(2026-09)有值 200 → GREATER_THAN 100 会通过;但当前月前(2026-07)值为 50 → 应取到 50
from datetime import datetime
db.add_all([
KPIValue(kpi_id=kpi.id, entity_id=1, period="2026-07", actual_value=50,
source_type="manual", data_status="verified"),
KPIValue(kpi_id=kpi.id, entity_id=1, period="2099-12", actual_value=200,
source_type="manual", data_status="verified"),
])
db.commit()
obj = _seed_objective(db)
plan = _seed_plan(db, kpi.id, obj.id, {
"kpi_code": "PER_001",
"condition": "GREATER_THAN", # actual > target
"target_value": 100,
"notify": False,
})
# 缺省 actual → 应取 2026-07 的 50 → 不通过(若错误取到 2099-12 的 200 则会通过)
r = client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={})
assert r.status_code == 200
assert r.json()["passed"] is False
assert r.json()["kpi_current_after"] == 50
def test_explicit_period_override(self, client: TestClient, db: Session):
"""缺陷3修复:调用方显式传 period 覆盖默认当前月"""
create_test_user(db)
token = get_token_for_user(client)
kpi = create_test_kpi(db, kpi_code="PER_002", kpi_name="期间KPI2")
db.add_all([
KPIValue(kpi_id=kpi.id, entity_id=1, period="2026-06", actual_value=30,
source_type="manual", data_status="verified"),
KPIValue(kpi_id=kpi.id, entity_id=1, period="2026-07", actual_value=90,
source_type="manual", data_status="verified"),
])
db.commit()
obj = _seed_objective(db)
plan = _seed_plan(db, kpi.id, obj.id, {
"kpi_code": "PER_002",
"condition": "GREATER_THAN",
"target_value": 50,
"notify": False,
})
# 显式 period=2026-06 → 取 30 → 不通过
r1 = client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={"period": "2026-06"})
assert r1.json()["kpi_current_after"] == 30
assert r1.json()["passed"] is False
# 显式 period=2026-07 → 取 90 → 通过
r2 = client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={"period": "2026-07"})
assert r2.json()["kpi_current_after"] == 90
assert r2.json()["passed"] is True
class TestStatusEnum:
def test_passed_plan_status_is_completed(self, client: TestClient, db: Session):
"""缺陷4修复:验证通过后 plan.status 写入枚举内值 completed,而非 done"""
create_test_user(db)
token = get_token_for_user(client)
kpi = create_test_kpi(db, kpi_code="ST_001", kpi_name="状态KPI")
obj = _seed_objective(db)
plan = _seed_plan(db, kpi.id, obj.id, _make_rule("ST_001"))
r = client.post(f"{BASE}/{plan.id}", headers=auth_header(token), json={"actual_value": 50})
assert r.status_code == 200
assert r.json()["passed"] is True
db.expire_all()
refreshed = db.query(ActionPlan).filter(ActionPlan.id == plan.id).first()
assert refreshed.status == "completed"
assert refreshed.status != "done"
assert refreshed.progress == 100