Files
AIPortPilot/backend/tests/test_phase2_services.py
T
selfrelease fad458b2a7 docs(uiux): UIUX 设计方案大改 + 5 份作业指导书对齐 + 开发任务文档
- UIUX 文档:填充 19 个缺口(多主体画像/健康度/AI+看板/增长域/洞察域/创始人端/OODA/助推/商密)
- UIUX 文档:插入 6 个新章节(十四~十九),旧章节重编号为二十~三十一,更新目录和交叉引用
- 作业指导书 x5:导航改为 6 域分组,新增 Context Bar/工作模式/Insight Rail/决策线程/多工作区等 UI 概念
- 新建 docs/2-task-uiux.md:50 个代码落地开发任务,按 P0-P6 分优先级 + 8 Sprint 规划
- 后端/前端:大量新增模型、路由、组件(来自之前 Phase 开发)
2026-07-19 11:53:38 +08:00

361 lines
13 KiB
Python

"""Phase 2 Service 层测试 — 财务校验/协议监控/弱信号关联/健康度/及时性追踪。"""
import uuid as uuid_mod
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.services.financial_validator import validate_financial_data
from app.services.agreement_monitor import check_clause_triggers
from app.services.signal_correlator import correlate_signals
from app.services.health_calculator import calculate_health_score, determine_trend
from app.services.report_tracker import compute_timeliness
from app.models.financial_data import FinancialData
from app.models.agreement import InvestmentAgreement
from app.models.report import MonthlyReport
from app.models.company import Company
from app.models.user import User
from app.models.tenant import Tenant
from tests.conftest import test_session_factory
@pytest.fixture
async def db_session():
"""创建数据库会话。"""
async with test_session_factory() as session:
yield session
await session.rollback()
@pytest.fixture
async def seed_tenant_company(db_session: AsyncSession):
"""创建测试租户和企业,返回 (tenant_id, company_id, user_id)。
每次调用使用唯一邮箱避免唯一约束冲突。
"""
unique = uuid_mod.uuid4().hex[:8]
tenant = Tenant(name=f"service测试机构_{unique}")
db_session.add(tenant)
await db_session.flush()
user = User(
email=f"svc_{unique}@example.com",
name="测试用户",
role="investor",
tenant_id=tenant.id,
password_hash="fake_hash",
is_active=True,
)
db_session.add(user)
await db_session.flush()
company = Company(
name=f"service测试公司_{unique}",
industry="AI",
tenant_id=tenant.id,
)
db_session.add(company)
await db_session.flush()
await db_session.commit()
return str(tenant.id), str(company.id), str(user.id)
class TestFinancialValidator:
"""财务数据校验服务测试。"""
@pytest.mark.asyncio
async def test_validate_no_data(self, db_session: AsyncSession):
"""无财务数据时应返回可信度 0 和提示。"""
result = await validate_financial_data(db_session, "nonexistent-id", 2025, 6)
assert result["credibility_score"] == 0.0
assert "无财务数据" in result["issues"]
@pytest.mark.asyncio
async def test_validate_balance_sheet_balanced(self, db_session: AsyncSession, seed_tenant_company):
"""资产负债表平衡时应通过校验。"""
_, company_id, _ = seed_tenant_company
fd = FinancialData(
company_id=company_id,
statement_type="balance_sheet",
period_year=2025,
period_month=6,
data_json={"total_assets": 1000000, "total_liabilities": 600000, "total_equity": 400000},
)
db_session.add(fd)
await db_session.commit()
result = await validate_financial_data(db_session, company_id, 2025, 6)
assert result["checks_total"] >= 1
assert result["checks_passed"] >= 1
assert result["credibility_score"] > 0
@pytest.mark.asyncio
async def test_validate_balance_sheet_unbalanced(self, db_session: AsyncSession, seed_tenant_company):
"""资产负债表不平时应报告问题。"""
_, company_id, _ = seed_tenant_company
fd = FinancialData(
company_id=company_id,
statement_type="balance_sheet",
period_year=2025,
period_month=6,
data_json={"total_assets": 1000000, "total_liabilities": 700000, "total_equity": 200000},
)
db_session.add(fd)
await db_session.commit()
result = await validate_financial_data(db_session, company_id, 2025, 6)
assert any("不平" in issue for issue in result["issues"])
@pytest.mark.asyncio
async def test_validate_negative_revenue(self, db_session: AsyncSession, seed_tenant_company):
"""收入为负数时应报告异常。"""
_, company_id, _ = seed_tenant_company
fd = FinancialData(
company_id=company_id,
statement_type="income",
period_year=2025,
period_month=6,
data_json={"revenue": -50000},
)
db_session.add(fd)
await db_session.commit()
result = await validate_financial_data(db_session, company_id, 2025, 6)
assert any("负数" in issue for issue in result["issues"])
class TestAgreementMonitor:
"""协议条款监控服务测试。"""
@pytest.mark.asyncio
async def test_check_no_agreements(self, db_session: AsyncSession, seed_tenant_company):
"""无协议时应返回空列表。"""
_, company_id, _ = seed_tenant_company
alerts = await check_clause_triggers(db_session, company_id)
assert alerts == []
@pytest.mark.asyncio
async def test_check_agreement_with_rules(self, db_session: AsyncSession, seed_tenant_company):
"""有监控规则的协议应生成预警。"""
_, company_id, _ = seed_tenant_company
agreement = InvestmentAgreement(
company_id=company_id,
title="A轮投资协议",
status="active",
monitoring_rules=[
{"rule": "营收低于 100 万", "metric": "revenue", "threshold": 1000000},
],
)
db_session.add(agreement)
await db_session.commit()
alerts = await check_clause_triggers(db_session, company_id)
assert len(alerts) == 1
assert alerts[0]["agreement_title"] == "A轮投资协议"
assert "营收" in alerts[0]["rule"]
@pytest.mark.asyncio
async def test_check_inactive_agreement_ignored(self, db_session: AsyncSession, seed_tenant_company):
"""非 active 状态的协议不应生成预警。"""
_, company_id, _ = seed_tenant_company
agreement = InvestmentAgreement(
company_id=company_id,
title="已终止协议",
status="terminated",
monitoring_rules=[{"rule": "test", "metric": "revenue", "threshold": 100}],
)
db_session.add(agreement)
await db_session.commit()
alerts = await check_clause_triggers(db_session, company_id)
assert alerts == []
class TestSignalCorrelator:
"""弱信号关联引擎测试。"""
@pytest.mark.asyncio
async def test_single_signal_no_correlation(self):
"""单个信号不应产生关联。"""
result = await correlate_signals([{"signal_type": "tech", "confidence": 0.8}])
assert result == []
@pytest.mark.asyncio
async def test_empty_signals(self):
"""空信号列表不应产生关联。"""
result = await correlate_signals([])
assert result == []
@pytest.mark.asyncio
async def test_cross_dimension_correlation(self):
"""不同类型信号应产生跨维度关联。"""
signals = [
{"id": "s1", "signal_type": "tech", "confidence": 0.7},
{"id": "s2", "signal_type": "market", "confidence": 0.6},
{"id": "s3", "signal_type": "org", "confidence": 0.5},
]
result = await correlate_signals(signals)
assert len(result) >= 1
assert result[0]["correlation_type"] == "cross_dimension"
assert result[0]["risk_probability"] > 0
assert result[0]["risk_probability"] <= 0.95
@pytest.mark.asyncio
async def test_same_type_no_cross_dimension(self):
"""同类型信号不应产生跨维度关联。"""
signals = [
{"id": "s1", "signal_type": "tech", "confidence": 0.7},
{"id": "s2", "signal_type": "tech", "confidence": 0.6},
]
result = await correlate_signals(signals)
assert result == []
class TestHealthCalculator:
"""健康度评分计算测试(补充已有 test_health_calculator.py 的边界用例)。"""
def test_empty_data_returns_zeros(self):
"""空数据应返回全 0。"""
result = calculate_health_score({})
assert result["total_score"] == 0.0
assert result["financial_score"] == 0.0
def test_healthy_company_high_score(self):
"""健康企业应获得高分。"""
data = {
"cash_balance": {"runway_months": 18},
"revenue": {"yoy_change": "+25%"},
"burn_rate": {"trend": "down"},
"headcount": {"new_hires": 10, "departures": 2},
"key_metrics": [
{"name": "AI 推理量", "change": "+30%"},
{"name": "推理成本", "change": "-15%"},
],
}
result = calculate_health_score(data)
assert result["total_score"] > 70
assert result["financial_score"] > 80
def test_unhealthy_company_low_score(self):
"""不健康企业应获得低分。"""
data = {
"cash_balance": {"runway_months": 2},
"revenue": {"yoy_change": "-30%"},
"burn_rate": {"trend": "up"},
"headcount": {"new_hires": 0, "departures": 8},
}
result = calculate_health_score(data)
assert result["total_score"] < 50
assert result["financial_score"] < 40
def test_score_range_0_to_100(self):
"""所有维度分数应在 0-100 范围内。"""
data = {
"cash_balance": {"runway_months": 0},
"revenue": {"yoy_change": "-100%"},
"burn_rate": {"trend": "up"},
"headcount": {"new_hires": 0, "departures": 100},
}
result = calculate_health_score(data)
for v in result.values():
assert 0 <= v <= 100
def test_determine_trend_up(self):
"""评分上升 >5 应为 up。"""
assert determine_trend(80, 70) == "up"
def test_determine_trend_down(self):
"""评分下降 >5 应为 down。"""
assert determine_trend(60, 70) == "down"
def test_determine_trend_stable(self):
"""评分变化 <=5 应为 stable。"""
assert determine_trend(72, 70) == "stable"
def test_determine_trend_no_previous(self):
"""无上期评分应为 stable。"""
assert determine_trend(75, None) == "stable"
class TestReportTracker:
"""月报提交及时性追踪测试。"""
@pytest.mark.asyncio
async def test_compute_timeliness_no_reports(self, db_session: AsyncSession, seed_tenant_company):
"""无月报时应返回空列表。"""
tenant_id, _, _ = seed_tenant_company
result = await compute_timeliness(db_session, tenant_id)
assert result == []
@pytest.mark.asyncio
async def test_compute_timeliness_on_time(self, db_session: AsyncSession, seed_tenant_company):
"""按时提交应延迟 0 天。"""
tenant_id, company_id, user_id = seed_tenant_company
# 2025年6月月报,应提交日期为 2025-07-10
report = MonthlyReport(
company_id=company_id,
period_year=2025,
period_month=6,
status="submitted",
raw_content="6月月报",
submitted_at=__import__("datetime").datetime(2025, 7, 10, tzinfo=__import__("datetime").timezone.utc),
structured_data={"revenue": 100, "cash_balance": {}, "burn_rate": {}, "headcount": {}, "key_metrics": [], "highlights": "", "concerns": ""},
)
db_session.add(report)
await db_session.commit()
result = await compute_timeliness(db_session, tenant_id)
assert len(result) == 1
assert result[0]["delay_days"] == 0
@pytest.mark.asyncio
async def test_compute_timeliness_late(self, db_session: AsyncSession, seed_tenant_company):
"""延迟提交应 delay_days > 0。"""
tenant_id, company_id, _ = seed_tenant_company
report = MonthlyReport(
company_id=company_id,
period_year=2025,
period_month=6,
status="submitted",
raw_content="6月月报",
submitted_at=__import__("datetime").datetime(2025, 7, 20, tzinfo=__import__("datetime").timezone.utc),
structured_data={"revenue": 100},
)
db_session.add(report)
await db_session.commit()
result = await compute_timeliness(db_session, tenant_id)
assert len(result) == 1
assert result[0]["delay_days"] == 10
@pytest.mark.asyncio
async def test_compute_timeliness_quality_score(self, db_session: AsyncSession, seed_tenant_company):
"""数据质量评分应基于结构化字段完整度。"""
tenant_id, company_id, _ = seed_tenant_company
# 只填了 1/8 个字段
report = MonthlyReport(
company_id=company_id,
period_year=2025,
period_month=6,
status="submitted",
raw_content="6月月报",
submitted_at=__import__("datetime").datetime(2025, 7, 10, tzinfo=__import__("datetime").timezone.utc),
structured_data={"revenue": 100},
)
db_session.add(report)
await db_session.commit()
result = await compute_timeliness(db_session, tenant_id)
assert len(result) == 1
# 1/8 = 12.5%
assert result[0]["quality_score"] == 12.5