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 开发)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""共享测试夹具 — 所有测试文件共用同一文件级 SQLite 数据库。
|
||||
|
||||
使用同步引擎在模块级创建表,避免跨事件循环导致表缺失。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
TEST_DB_PATH = "./test.db"
|
||||
TEST_DATABASE_URL = f"sqlite+aiosqlite:///{TEST_DB_PATH}"
|
||||
SYNC_DATABASE_URL = f"sqlite:///{TEST_DB_PATH}"
|
||||
|
||||
# --- 同步引擎:模块级创建表 ---
|
||||
_sync_engine = create_engine(SYNC_DATABASE_URL, echo=False)
|
||||
|
||||
|
||||
def _create_tables() -> None:
|
||||
"""用同步引擎创建全部表。"""
|
||||
Base.metadata.create_all(_sync_engine)
|
||||
|
||||
|
||||
def _drop_tables() -> None:
|
||||
"""用同步引擎销毁全部表。"""
|
||||
Base.metadata.drop_all(_sync_engine)
|
||||
|
||||
|
||||
# 模块导入时立即创建表
|
||||
if os.path.exists(TEST_DB_PATH):
|
||||
os.remove(TEST_DB_PATH)
|
||||
_create_tables()
|
||||
|
||||
# --- 异步引擎:测试运行时使用 ---
|
||||
# StaticPool 保持单连接复用,connect_args 允许跨线程访问
|
||||
test_engine = create_async_engine(
|
||||
TEST_DATABASE_URL,
|
||||
echo=False,
|
||||
poolclass=StaticPool,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
test_session_factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
"""覆盖数据库依赖,使用测试 SQLite。"""
|
||||
async with test_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
async def setup_db():
|
||||
"""确保表已创建,测试结束后清理。"""
|
||||
# 表已在模块级同步创建,此处仅做清理
|
||||
yield
|
||||
await test_engine.dispose()
|
||||
_drop_tables()
|
||||
_sync_engine.dispose()
|
||||
if os.path.exists(TEST_DB_PATH):
|
||||
os.remove(TEST_DB_PATH)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def mock_llm():
|
||||
"""Mock LLM 客户端 — 所有 AI 相关测试不依赖真实 API。
|
||||
|
||||
chat() 尝试解析 JSON 返回 Python 对象(dict/list),
|
||||
使 service 层 isinstance(result, dict/list) 检查通过。
|
||||
无法解析时返回原始字符串。
|
||||
"""
|
||||
mock_dict = {"summary": "AI 分析结果(测试 mock)", "score": 75, "confidence": 0.8}
|
||||
mock_list = [{"type": "tech", "title": "测试协同", "match_reason": "mock"}]
|
||||
|
||||
async def mock_chat_stream(self, messages, temperature=0.3, max_tokens=2000):
|
||||
yield json.dumps(mock_dict)
|
||||
|
||||
async def mock_chat(self, messages, temperature=0.3, max_tokens=2000):
|
||||
# service 层传的可能是 str 或 list[dict]
|
||||
# 根据 prompt 内容判断期望返回类型
|
||||
if isinstance(messages, str):
|
||||
prompt_text = messages
|
||||
elif isinstance(messages, list):
|
||||
prompt_text = " ".join(str(m.get("content", "")) for m in messages)
|
||||
else:
|
||||
prompt_text = str(messages)
|
||||
|
||||
# 如果 prompt 中包含 "JSON 数组" 或 "数组格式",返回 list
|
||||
if "数组" in prompt_text or "JSON 数组" in prompt_text:
|
||||
return mock_list
|
||||
# 默认返回 dict
|
||||
return mock_dict
|
||||
|
||||
with patch("app.services.llm_client.LLMClient.chat", mock_chat), \
|
||||
patch("app.services.llm_client.LLMClient.chat_stream", mock_chat_stream):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""创建测试客户端。"""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers(client: TestClient):
|
||||
"""注册并登录投资人用户,返回认证头。"""
|
||||
client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "smoke_test@example.com",
|
||||
"password": "password123",
|
||||
"name": "冒烟测试用户",
|
||||
"tenant_name": "冒烟测试机构",
|
||||
"role": "investor",
|
||||
},
|
||||
)
|
||||
resp = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "smoke_test@example.com", "password": "password123"},
|
||||
)
|
||||
token = resp.json()["data"]["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def company_id(client: TestClient, auth_headers: dict):
|
||||
"""创建测试企业,返回企业 ID。"""
|
||||
resp = client.post(
|
||||
"/api/v1/companies",
|
||||
json={"name": "冒烟测试公司", "industry": "AI"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
return resp.json()["data"]["id"]
|
||||
@@ -0,0 +1,64 @@
|
||||
"""T4.6 AAR 系统化复盘测试。
|
||||
|
||||
测试 AI AAR Agent 的五问复盘和触发条件检测。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.aar_agent import check_aar_triggers, generate_aar
|
||||
|
||||
|
||||
class TestGenerateAAR:
|
||||
"""五问复盘测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回复盘结果字典。"""
|
||||
result = await generate_aar(
|
||||
"CTO 离职",
|
||||
"原计划 Q3 完成产品 2.0 上线",
|
||||
"产品延期 2 个月,团队士气下降",
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await generate_aar("", "", "")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestCheckAarTriggers:
|
||||
"""AAR 触发条件检测测试。"""
|
||||
|
||||
async def test_risk_resolved_trigger(self):
|
||||
"""风险处理完成触发 AAR。"""
|
||||
events = [{"type": "risk_resolved", "title": "现金流危机解除"}]
|
||||
result = await check_aar_triggers("company-1", events)
|
||||
assert len(result) == 1
|
||||
assert result[0]["trigger_type"] == "risk_resolved"
|
||||
|
||||
async def test_funding_completed_trigger(self):
|
||||
"""融资完成触发 AAR。"""
|
||||
events = [{"type": "funding_completed", "title": "A 轮融资完成"}]
|
||||
result = await check_aar_triggers("company-1", events)
|
||||
assert len(result) == 1
|
||||
|
||||
async def test_no_trigger(self):
|
||||
"""无触发事件返回空列表。"""
|
||||
events = [{"type": "routine_update", "title": "常规月报"}]
|
||||
result = await check_aar_triggers("company-1", events)
|
||||
assert result == []
|
||||
|
||||
async def test_empty_events(self):
|
||||
"""空事件列表返回空。"""
|
||||
result = await check_aar_triggers("company-1", [])
|
||||
assert result == []
|
||||
|
||||
async def test_multiple_triggers(self):
|
||||
"""多个触发事件全部识别。"""
|
||||
events = [
|
||||
{"type": "funding_completed", "title": "A 轮完成"},
|
||||
{"type": "talent_left", "title": "CTO 离职"},
|
||||
{"type": "routine", "title": "常规更新"},
|
||||
]
|
||||
result = await check_aar_triggers("company-1", events)
|
||||
assert len(result) == 2
|
||||
@@ -0,0 +1,147 @@
|
||||
"""T2.10 Admin 管理后台 + 审计日志测试。
|
||||
|
||||
测试 Admin API 的系统概览、租户管理、用户管理和审计日志查询。
|
||||
"""
|
||||
|
||||
import uuid as uuid_mod
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.audit import log_audit
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from tests.conftest import test_session_factory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_headers(client: TestClient):
|
||||
"""注册并登录 admin 用户,返回认证头。"""
|
||||
client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "admin_test@example.com",
|
||||
"password": "password123",
|
||||
"name": "管理员",
|
||||
"tenant_name": "管理机构",
|
||||
"role": "admin",
|
||||
},
|
||||
)
|
||||
resp = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "admin_test@example.com", "password": "password123"},
|
||||
)
|
||||
token = resp.json()["data"]["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def investor_headers(client: TestClient):
|
||||
"""注册并登录 investor 用户,返回认证头。"""
|
||||
client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "investor_admin_test@example.com",
|
||||
"password": "password123",
|
||||
"name": "投资人",
|
||||
"tenant_name": "投资人机构",
|
||||
"role": "investor",
|
||||
},
|
||||
)
|
||||
resp = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "investor_admin_test@example.com", "password": "password123"},
|
||||
)
|
||||
token = resp.json()["data"]["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db_session():
|
||||
"""创建数据库会话。"""
|
||||
async with test_session_factory() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
|
||||
|
||||
class TestAdminOverview:
|
||||
"""系统概览测试。"""
|
||||
|
||||
def test_overview_as_admin(self, client: TestClient, admin_headers: dict):
|
||||
"""admin 可以访问概览。"""
|
||||
resp = client.get("/api/v1/admin/overview", headers=admin_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "tenant_count" in data
|
||||
assert "user_count" in data
|
||||
|
||||
def test_overview_as_investor_forbidden(self, client: TestClient, investor_headers: dict):
|
||||
"""investor 无权访问 admin 概览。"""
|
||||
resp = client.get("/api/v1/admin/overview", headers=investor_headers)
|
||||
assert resp.status_code == 403
|
||||
|
||||
def test_overview_no_auth(self, client: TestClient):
|
||||
"""无认证返回 401。"""
|
||||
resp = client.get("/api/v1/admin/overview")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestAdminTenants:
|
||||
"""租户管理测试。"""
|
||||
|
||||
def test_list_tenants(self, client: TestClient, admin_headers: dict):
|
||||
"""admin 可以查看租户列表。"""
|
||||
resp = client.get("/api/v1/admin/tenants", headers=admin_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
|
||||
class TestAdminUsers:
|
||||
"""用户管理测试。"""
|
||||
|
||||
def test_list_users(self, client: TestClient, admin_headers: dict):
|
||||
"""admin 可以查看用户列表。"""
|
||||
resp = client.get("/api/v1/admin/users", headers=admin_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
|
||||
class TestAdminAuditLogs:
|
||||
"""审计日志测试。"""
|
||||
|
||||
def test_list_audit_logs(self, client: TestClient, admin_headers: dict):
|
||||
"""admin 可以查看审计日志。"""
|
||||
resp = client.get("/api/v1/admin/audit-logs", headers=admin_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
async def test_log_audit(self, db_session: AsyncSession):
|
||||
"""审计日志写入测试。"""
|
||||
unique = uuid_mod.uuid4().hex[:8]
|
||||
tenant = Tenant(name=f"audit机构_{unique}")
|
||||
db_session.add(tenant)
|
||||
await db_session.flush()
|
||||
|
||||
user = User(
|
||||
email=f"audit_{unique}@example.com",
|
||||
name="审计测试用户",
|
||||
role="admin",
|
||||
tenant_id=tenant.id,
|
||||
password_hash="fake_hash",
|
||||
is_active=True,
|
||||
)
|
||||
db_session.add(user)
|
||||
await db_session.flush()
|
||||
|
||||
await log_audit(db_session, str(user.id), "CREATE", "company", "comp-123", {"name": "测试公司"}, tenant_id=str(tenant.id))
|
||||
await db_session.commit()
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.audit import AuditLog
|
||||
|
||||
result = await db_session.execute(select(AuditLog).where(AuditLog.resource_id == "comp-123"))
|
||||
log = result.scalar_one_or_none()
|
||||
assert log is not None
|
||||
assert log.action == "CREATE"
|
||||
assert log.resource_type == "company"
|
||||
@@ -0,0 +1,64 @@
|
||||
"""T4.8 Agent 编排引擎测试。
|
||||
|
||||
测试 L1-L4 分级自治的 Agent 编排。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.agent_orchestrator import AUTONOMY_LEVELS, orchestrate_agent
|
||||
|
||||
|
||||
class TestOrchestrateAgent:
|
||||
"""Agent 编排测试。"""
|
||||
|
||||
async def test_l1_requires_approval(self):
|
||||
"""L1 级别需要人工审核。"""
|
||||
result = await orchestrate_agent("test_agent", "L1", {"task": "测试任务"})
|
||||
assert result["requires_approval"] is True
|
||||
assert result["status"] == "pending_approval"
|
||||
|
||||
async def test_l2_requires_approval(self):
|
||||
"""L2 级别需要人工确认。"""
|
||||
result = await orchestrate_agent("test_agent", "L2", {"task": "测试任务"})
|
||||
assert result["requires_approval"] is True
|
||||
assert result["status"] == "pending_approval"
|
||||
|
||||
async def test_l3_post_review(self):
|
||||
"""L3 级别事后审核。"""
|
||||
result = await orchestrate_agent("test_agent", "L3", {"task": "测试任务"})
|
||||
assert result["requires_approval"] is False
|
||||
assert result["requires_post_review"] is True
|
||||
assert result["status"] == "executed"
|
||||
|
||||
async def test_l4_requires_approval(self):
|
||||
"""L4 级别人工决策。"""
|
||||
result = await orchestrate_agent("test_agent", "L4", {"task": "测试任务"})
|
||||
assert result["requires_approval"] is True
|
||||
|
||||
async def test_unknown_level_defaults_l1(self):
|
||||
"""未知级别默认 L1。"""
|
||||
result = await orchestrate_agent("test_agent", "L9", {"task": "测试任务"})
|
||||
assert result["requires_approval"] is True
|
||||
|
||||
async def test_input_summary_truncated(self):
|
||||
"""输入摘要被截断。"""
|
||||
long_input = {"data": "x" * 500}
|
||||
result = await orchestrate_agent("test_agent", "L1", long_input)
|
||||
assert len(result["input_summary"]) <= 200
|
||||
|
||||
|
||||
class TestAutonomyLevels:
|
||||
"""自治级别配置测试。"""
|
||||
|
||||
def test_all_levels_defined(self):
|
||||
"""L1-L4 全部定义。"""
|
||||
assert "L1" in AUTONOMY_LEVELS
|
||||
assert "L2" in AUTONOMY_LEVELS
|
||||
assert "L3" in AUTONOMY_LEVELS
|
||||
assert "L4" in AUTONOMY_LEVELS
|
||||
|
||||
def test_level_has_description(self):
|
||||
"""每个级别有描述。"""
|
||||
for level, config in AUTONOMY_LEVELS.items():
|
||||
assert "description" in config
|
||||
assert len(config["description"]) > 0
|
||||
@@ -0,0 +1,127 @@
|
||||
"""T2.3 投资协议解析与监控测试。
|
||||
|
||||
测试 AI 协议解析和条款监控引擎。
|
||||
"""
|
||||
|
||||
import uuid as uuid_mod
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.agreement import InvestmentAgreement
|
||||
from app.models.company import Company
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from app.services.agreement_monitor import check_clause_triggers
|
||||
from app.services.agreement_parser import parse_agreement
|
||||
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_company(db_session: AsyncSession):
|
||||
"""创建测试企业和租户,返回 company_id。"""
|
||||
unique = uuid_mod.uuid4().hex[:8]
|
||||
tenant = Tenant(name=f"agree机构_{unique}")
|
||||
db_session.add(tenant)
|
||||
await db_session.flush()
|
||||
|
||||
user = User(
|
||||
email=f"agree_{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"agree公司_{unique}", industry="AI", tenant_id=tenant.id)
|
||||
db_session.add(company)
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
return str(company.id)
|
||||
|
||||
|
||||
class TestParseAgreement:
|
||||
"""协议解析测试。"""
|
||||
|
||||
async def test_parse_returns_dict(self):
|
||||
"""AI 解析协议返回字典结构。"""
|
||||
result = await parse_agreement("本投资协议约定估值 5000 万元,对赌条款要求 2025 年营收达到 2000 万元。")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_parse_empty_text(self):
|
||||
"""空文本仍返回结构化空结果。"""
|
||||
result = await parse_agreement("")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestCheckClauseTriggers:
|
||||
"""条款监控测试。"""
|
||||
|
||||
async def test_no_agreements(self, db_session: AsyncSession, seed_company):
|
||||
"""无协议时返回空列表。"""
|
||||
company_id = seed_company
|
||||
result = await check_clause_triggers(db_session, company_id)
|
||||
assert result == []
|
||||
|
||||
async def test_agreement_with_rules(self, db_session: AsyncSession, seed_company):
|
||||
"""有监控规则的协议生成预警。"""
|
||||
company_id = seed_company
|
||||
agreement = InvestmentAgreement(
|
||||
company_id=company_id,
|
||||
title="A 轮投资协议",
|
||||
status="active",
|
||||
monitoring_rules=[
|
||||
{"rule": "营收不低于 1000 万", "metric": "revenue", "threshold": "1000"},
|
||||
],
|
||||
)
|
||||
db_session.add(agreement)
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
|
||||
result = await check_clause_triggers(db_session, company_id)
|
||||
assert len(result) == 1
|
||||
assert result[0]["agreement_title"] == "A 轮投资协议"
|
||||
assert result[0]["rule"] == "营收不低于 1000 万"
|
||||
|
||||
async def test_inactive_agreement_skipped(self, db_session: AsyncSession, seed_company):
|
||||
"""非 active 状态的协议不生成预警。"""
|
||||
company_id = seed_company
|
||||
agreement = InvestmentAgreement(
|
||||
company_id=company_id,
|
||||
title="已终止协议",
|
||||
status="terminated",
|
||||
monitoring_rules=[{"rule": "测试规则", "metric": "revenue", "threshold": "100"}],
|
||||
)
|
||||
db_session.add(agreement)
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
|
||||
result = await check_clause_triggers(db_session, company_id)
|
||||
assert result == []
|
||||
|
||||
async def test_agreement_no_rules_skipped(self, db_session: AsyncSession, seed_company):
|
||||
"""无监控规则的协议不生成预警。"""
|
||||
company_id = seed_company
|
||||
agreement = InvestmentAgreement(
|
||||
company_id=company_id,
|
||||
title="无规则协议",
|
||||
status="active",
|
||||
monitoring_rules=None,
|
||||
)
|
||||
db_session.add(agreement)
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
|
||||
result = await check_clause_triggers(db_session, company_id)
|
||||
assert result == []
|
||||
@@ -0,0 +1,24 @@
|
||||
"""T4.1 Alpha 归因测试。
|
||||
|
||||
测试 AI Alpha 归因 Agent。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.alpha_attribution import attribute_alpha
|
||||
|
||||
|
||||
class TestAttributeAlpha:
|
||||
"""Alpha 归因测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回归因结果字典。"""
|
||||
intervention = {"type": "hiring", "description": "引入资深 CTO", "date": "2025-01-15"}
|
||||
metric_changes = {"revenue_growth": 0.15, "product_velocity": 0.3}
|
||||
result = await attribute_alpha(intervention, metric_changes)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await attribute_alpha({}, {})
|
||||
assert isinstance(result, dict)
|
||||
@@ -5,45 +5,6 @@
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
# 使用 SQLite 内存数据库做测试
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
||||
|
||||
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
||||
test_session_factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
"""测试用数据库 session。"""
|
||||
async with test_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
async def setup_db():
|
||||
"""创建测试数据库表。"""
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""创建测试客户端。"""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
class TestRegister:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""T2.4 董事会 Agent 测试。
|
||||
|
||||
测试会前材料摘要和提问清单生成。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.board_agent import generate_meeting_summary, generate_questions
|
||||
|
||||
|
||||
class TestGenerateMeetingSummary:
|
||||
"""会前材料摘要测试。"""
|
||||
|
||||
async def test_summary_returns_string(self):
|
||||
"""摘要返回字符串。"""
|
||||
result = await generate_meeting_summary("本季度营收增长 30%,现金流健康,团队扩招 5 人。")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
async def test_summary_empty_materials(self):
|
||||
"""空材料仍返回字符串。"""
|
||||
result = await generate_meeting_summary("")
|
||||
assert isinstance(result, str)
|
||||
|
||||
|
||||
class TestGenerateQuestions:
|
||||
"""提问清单测试。"""
|
||||
|
||||
async def test_questions_returns_list(self):
|
||||
"""提问清单返回列表。"""
|
||||
result = await generate_questions("Q3 营收 500 万,环比增长 20%,但客户集中度高达 80%。")
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_questions_empty_materials(self):
|
||||
"""空材料返回默认提示。"""
|
||||
result = await generate_questions("")
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
@@ -2,43 +2,6 @@
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
||||
|
||||
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
||||
test_session_factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
"""测试用数据库 session。"""
|
||||
async with test_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
async def setup_db():
|
||||
"""创建测试数据库表。"""
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
"""T2.1 企业详情聚合接口测试 + T2.9/T3.11 健康度维度扩展测试。
|
||||
|
||||
覆盖:
|
||||
- GET /companies/{id}/detail 聚合接口
|
||||
- 14 维度健康度评分计算
|
||||
- 趋势判断
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.services.health_calculator import (
|
||||
calculate_health_score,
|
||||
determine_trend,
|
||||
_calc_financial_score,
|
||||
_calc_operational_score,
|
||||
_calc_org_talent_score,
|
||||
_calc_product_tech_score,
|
||||
_calc_market_compete_score,
|
||||
_calc_governance_score,
|
||||
_calc_financing_score,
|
||||
_calc_synergy_score,
|
||||
_calc_ai_model_product_score,
|
||||
_calc_data_compliance_score,
|
||||
_calc_team_tech_score,
|
||||
_calc_customer_success_score,
|
||||
)
|
||||
|
||||
|
||||
class TestCompanyDetailEndpoint:
|
||||
"""T2.1 企业详情聚合接口测试。"""
|
||||
|
||||
def test_get_company_detail_success(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""测试获取企业详情聚合数据。"""
|
||||
resp = client.get(f"/api/v1/companies/{company_id}/detail", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "company" in data
|
||||
assert data["company"]["id"] == company_id
|
||||
assert "recent_reports" in data
|
||||
assert "open_risks" in data
|
||||
assert "recent_weak_signals" in data
|
||||
assert "active_agreements" in data
|
||||
assert "recent_board_meetings" in data
|
||||
assert "financial_data_count" in data
|
||||
|
||||
def test_get_company_detail_not_found(self, client: TestClient, auth_headers: dict):
|
||||
"""测试获取不存在的企业详情。"""
|
||||
resp = client.get("/api/v1/companies/nonexistent-id/detail", headers=auth_headers)
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_get_company_detail_no_auth(self, client: TestClient, company_id: str):
|
||||
"""测试未认证请求。"""
|
||||
resp = client.get(f"/api/v1/companies/{company_id}/detail")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestHealthCalculator14Dimensions:
|
||||
"""T2.9 + T3.11 健康度 14 维度评分测试。"""
|
||||
|
||||
def test_empty_data_returns_zeros(self):
|
||||
"""空数据应返回全 0。"""
|
||||
result = calculate_health_score({})
|
||||
assert result["total_score"] == 0.0
|
||||
assert result["financial_score"] == 0.0
|
||||
assert all(v == 0.0 for v in result.values())
|
||||
|
||||
def test_none_data_returns_zeros(self):
|
||||
"""None 数据应返回全 0。"""
|
||||
result = calculate_health_score(None)
|
||||
assert result["total_score"] == 0.0
|
||||
|
||||
def test_full_14_dimensions_present(self):
|
||||
"""完整数据应返回 14 个维度分数 + total_score。"""
|
||||
data = {
|
||||
"cash_balance": {"runway_months": 18},
|
||||
"revenue": {"yoy_change": "+30%"},
|
||||
"burn_rate": {"trend": "down"},
|
||||
"headcount": {"new_hires": 5, "departures": 1, "total": 50},
|
||||
"key_metrics": [
|
||||
{"name": "MAU", "change": "+15%"},
|
||||
{"name": "产品迭代次数", "change": "+10%"},
|
||||
{"name": "AI 推理成本", "change": "-5%"},
|
||||
],
|
||||
"governance": {"board_meeting_held": True, "compliance_issues": False},
|
||||
"financing": {"in_progress": True},
|
||||
"synergy": {"active_count": 3, "completed_count": 2},
|
||||
"ai_metrics": {"model_accuracy": 0.95, "inference_cost_trend": "down", "data_quality_score": 80},
|
||||
"data_compliance": {"issues_count": 0, "audit_passed": True},
|
||||
"team_tech": {"tech_lead_count": 3, "patent_count": 5},
|
||||
"customer_success": {"retention_rate": 0.92, "nps": 50},
|
||||
}
|
||||
result = calculate_health_score(data)
|
||||
expected_keys = [
|
||||
"total_score", "financial_score", "operational_score",
|
||||
"ai_commercial_score", "ai_cost_score",
|
||||
"org_talent_score", "product_tech_score", "market_compete_score",
|
||||
"governance_score", "financing_score",
|
||||
"synergy_score", "ai_model_product_score", "data_compliance_score",
|
||||
"team_tech_score", "customer_success_score",
|
||||
]
|
||||
assert all(k in result for k in expected_keys)
|
||||
assert 0 <= result["total_score"] <= 100
|
||||
|
||||
def test_financial_score_high_runway(self):
|
||||
"""长跑道应得高分。"""
|
||||
data = {"cash_balance": {"runway_months": 15}, "revenue": {"yoy_change": "+20%"}, "burn_rate": {"trend": "down"}}
|
||||
score = _calc_financial_score(data)
|
||||
assert score >= 80
|
||||
|
||||
def test_financial_score_low_runway(self):
|
||||
"""短跑道应得低分。"""
|
||||
data = {"cash_balance": {"runway_months": 2}, "revenue": {"yoy_change": "-10%"}, "burn_rate": {"trend": "up"}}
|
||||
score = _calc_financial_score(data)
|
||||
assert score < 40
|
||||
|
||||
def test_org_talent_low_turnover(self):
|
||||
"""低流失率应得高分。"""
|
||||
data = {"headcount": {"new_hires": 3, "departures": 1, "total": 50}}
|
||||
score = _calc_org_talent_score(data)
|
||||
assert score >= 75
|
||||
|
||||
def test_org_talent_high_turnover(self):
|
||||
"""高流失率应得低分。"""
|
||||
data = {"headcount": {"new_hires": 1, "departures": 12, "total": 50}}
|
||||
score = _calc_org_talent_score(data)
|
||||
assert score <= 50
|
||||
|
||||
def test_product_tech_positive_growth(self):
|
||||
"""产品技术指标正增长应加分。"""
|
||||
data = {"key_metrics": [{"name": "产品迭代次数", "change": "+20%"}]}
|
||||
score = _calc_product_tech_score(data)
|
||||
assert score > 55
|
||||
|
||||
def test_market_compete_positive(self):
|
||||
"""市场竞争指标正增长应加分。"""
|
||||
data = {"key_metrics": [{"name": "MAU", "change": "+25%"}]}
|
||||
score = _calc_market_compete_score(data)
|
||||
assert score > 55
|
||||
|
||||
def test_governance_with_compliance_issues(self):
|
||||
"""有合规问题应减分。"""
|
||||
data = {"governance": {"board_meeting_held": True, "compliance_issues": True}}
|
||||
score = _calc_governance_score(data)
|
||||
assert score < 70
|
||||
|
||||
def test_financing_long_runway(self):
|
||||
"""长跑道 + 融资进行中应得高分。"""
|
||||
data = {"cash_balance": {"runway_months": 20}, "financing": {"in_progress": True}}
|
||||
score = _calc_financing_score(data)
|
||||
assert score >= 80
|
||||
|
||||
def test_synergy_active(self):
|
||||
"""有活跃协同应加分。"""
|
||||
data = {"synergy": {"active_count": 4, "completed_count": 2}}
|
||||
score = _calc_synergy_score(data)
|
||||
assert score > 55
|
||||
|
||||
def test_ai_model_product_high_accuracy(self):
|
||||
"""AI 模型高准确率应加分。"""
|
||||
data = {"ai_metrics": {"model_accuracy": 0.95, "inference_cost_trend": "down", "data_quality_score": 85}}
|
||||
score = _calc_ai_model_product_score(data)
|
||||
assert score > 50
|
||||
|
||||
def test_data_compliance_no_issues(self):
|
||||
"""无合规问题 + 审计通过应得高分。"""
|
||||
data = {"data_compliance": {"issues_count": 0, "audit_passed": True}}
|
||||
score = _calc_data_compliance_score(data)
|
||||
assert score >= 80
|
||||
|
||||
def test_data_compliance_with_issues(self):
|
||||
"""有合规问题应减分。"""
|
||||
data = {"data_compliance": {"issues_count": 3, "audit_passed": False}}
|
||||
score = _calc_data_compliance_score(data)
|
||||
assert score < 50
|
||||
|
||||
def test_team_tech_with_leads_and_patents(self):
|
||||
"""有技术负责人和专利应加分。"""
|
||||
data = {"team_tech": {"tech_lead_count": 3, "patent_count": 5}}
|
||||
score = _calc_team_tech_score(data)
|
||||
assert score > 55
|
||||
|
||||
def test_customer_success_high_retention(self):
|
||||
"""高留存率应得高分。"""
|
||||
data = {"customer_success": {"retention_rate": 0.95, "nps": 60}}
|
||||
score = _calc_customer_success_score(data)
|
||||
assert score >= 75
|
||||
|
||||
def test_customer_success_low_retention(self):
|
||||
"""低留存率应得低分。"""
|
||||
data = {"customer_success": {"retention_rate": 0.60, "nps": 10}}
|
||||
score = _calc_customer_success_score(data)
|
||||
assert score < 50
|
||||
|
||||
def test_total_score_in_range(self):
|
||||
"""总分应在 0-100 范围内。"""
|
||||
data = {
|
||||
"cash_balance": {"runway_months": 6},
|
||||
"headcount": {"new_hires": 2, "departures": 1, "total": 30},
|
||||
"key_metrics": [{"name": "营收", "change": "+5%"}],
|
||||
}
|
||||
result = calculate_health_score(data)
|
||||
assert 0 <= result["total_score"] <= 100
|
||||
|
||||
|
||||
class TestDetermineTrend:
|
||||
"""趋势判断测试。"""
|
||||
|
||||
def test_up_trend(self):
|
||||
assert determine_trend(80, 70) == "up"
|
||||
|
||||
def test_down_trend(self):
|
||||
assert determine_trend(60, 70) == "down"
|
||||
|
||||
def test_stable_trend(self):
|
||||
assert determine_trend(70, 72) == "stable"
|
||||
|
||||
def test_no_previous(self):
|
||||
assert determine_trend(70, None) == "stable"
|
||||
@@ -0,0 +1,60 @@
|
||||
"""T3.12 约束点识别 + 鸿沟诊断测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestConstraintAnalysis:
|
||||
"""TOC 约束点识别接口测试。"""
|
||||
|
||||
def test_analyze_constraints_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 识别约束点。"""
|
||||
resp = client.post(
|
||||
"/api/v1/advanced-analysis/constraints",
|
||||
json={"company_data": "SaaS 公司,月增长 5%,但客户流失率 15%,获客成本持续上升"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_analyze_constraints_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/advanced-analysis/constraints", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_analyze_constraints_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""空输入应正常返回。"""
|
||||
resp = client.post(
|
||||
"/api/v1/advanced-analysis/constraints",
|
||||
json={"company_data": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
class TestChasmDiagnosis:
|
||||
"""鸿沟诊断接口测试。"""
|
||||
|
||||
def test_diagnose_chasm_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 鸿沟诊断。"""
|
||||
resp = client.post(
|
||||
"/api/v1/advanced-analysis/chasm",
|
||||
json={"company_data": "产品已有 50 个早期采用者,但难以扩展到主流市场"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_diagnose_chasm_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/advanced-analysis/chasm", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_diagnose_chasm_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""空输入应正常返回。"""
|
||||
resp = client.post(
|
||||
"/api/v1/advanced-analysis/chasm",
|
||||
json={"company_data": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,58 @@
|
||||
"""T3.4 客户增长引擎测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestCustomerPlansList:
|
||||
"""客户获取方案列表接口测试。"""
|
||||
|
||||
def test_list_plans_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无方案时应返回空列表。"""
|
||||
resp = client.get("/api/v1/customer-plans", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_plans_with_company_filter(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""指定企业 ID 过滤方案列表。"""
|
||||
resp = client.get(f"/api/v1/customer-plans?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_plans_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/customer-plans")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestCustomerPlanGenerate:
|
||||
"""AI 生成客户获取方案接口测试。"""
|
||||
|
||||
def test_generate_plan_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 生成客户获取方案。"""
|
||||
resp = client.post(
|
||||
"/api/v1/customer-plans/generate",
|
||||
json={
|
||||
"company_context": "AI 教育公司,面向 K12 市场",
|
||||
"lp_resources": "LP 拥有教育行业资源,包括学校关系和教育部门人脉",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert isinstance(data, dict)
|
||||
|
||||
def test_generate_plan_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/customer-plans/generate", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_generate_plan_empty_body(self, client: TestClient, auth_headers: dict):
|
||||
"""空请求体应正常返回(AI 返回 mock 数据)。"""
|
||||
resp = client.post(
|
||||
"/api/v1/customer-plans/generate",
|
||||
json={"company_context": "", "lp_resources": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,57 @@
|
||||
"""T3.14 客户成功运营模型测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestQBR:
|
||||
"""QBR 季度业务回顾接口测试。"""
|
||||
|
||||
def test_generate_qbr_success(self, client: TestClient, auth_headers: dict):
|
||||
"""生成 QBR 报告。"""
|
||||
resp = client.post(
|
||||
"/api/v1/customer-success/qbr",
|
||||
json={"company_id": "test-id", "quarter_data": "Q3 营收增长 20%,客户数 50,流失率 5%"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_generate_qbr_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/customer-success/qbr", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestExpansion:
|
||||
"""扩展机会识别接口测试。"""
|
||||
|
||||
def test_identify_expansion_success(self, client: TestClient, auth_headers: dict):
|
||||
"""识别扩展机会。"""
|
||||
resp = client.post(
|
||||
"/api/v1/customer-success/expansion",
|
||||
json={"company_data": "AI 教育产品,已在 K12 市场站稳,考虑扩展到职业教育"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["data"] is not None
|
||||
|
||||
def test_identify_expansion_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/customer-success/expansion", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestChurnRisk:
|
||||
"""流失风险预警接口测试。"""
|
||||
|
||||
def test_get_churn_risk_success(self, client: TestClient, auth_headers: dict):
|
||||
"""获取流失风险预警。"""
|
||||
resp = client.get("/api/v1/customer-success/churn-risk", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["data"] is not None
|
||||
|
||||
def test_get_churn_risk_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/customer-success/churn-risk")
|
||||
assert resp.status_code == 401
|
||||
@@ -2,41 +2,6 @@
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
||||
|
||||
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
||||
test_session_factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
async with test_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
async def setup_db():
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""T2.12 驾驶舱增强测试 — 热力图 + 趋势对比接口。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestDashboardHeatmap:
|
||||
"""热力图接口测试。"""
|
||||
|
||||
def test_heatmap_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无企业时应返回空列表。"""
|
||||
resp = client.get("/api/v1/dashboard/heatmap", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_heatmap_with_company(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""有企业时应返回热力图数据。"""
|
||||
resp = client.get("/api/v1/dashboard/heatmap", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) >= 1
|
||||
assert "company_id" in data[0]
|
||||
assert "company_name" in data[0]
|
||||
assert "scores" in data[0]
|
||||
|
||||
def test_heatmap_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/dashboard/heatmap")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestDashboardTrends:
|
||||
"""趋势对比接口测试。"""
|
||||
|
||||
def test_trends_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无评分数据时应返回空列表。"""
|
||||
resp = client.get("/api/v1/dashboard/trends", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_trends_with_months_param(self, client: TestClient, auth_headers: dict):
|
||||
"""指定 months 参数应正常返回。"""
|
||||
resp = client.get("/api/v1/dashboard/trends?months=3", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_trends_with_company_id(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""指定企业 ID 应正常返回。"""
|
||||
resp = client.get(
|
||||
f"/api/v1/dashboard/trends?company_id={company_id}&months=6",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_trends_invalid_months(self, client: TestClient, auth_headers: dict):
|
||||
"""无效 months 参数应返回 422。"""
|
||||
resp = client.get("/api/v1/dashboard/trends?months=0", headers=auth_headers)
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_trends_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/dashboard/trends")
|
||||
assert resp.status_code == 401
|
||||
@@ -0,0 +1,24 @@
|
||||
"""T4.11 多源数据接入测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestDataSources:
|
||||
"""数据源管理接口测试。"""
|
||||
|
||||
def test_list_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无数据源时应返回空列表。"""
|
||||
resp = client.get("/api/v1/data-sources", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_with_company_filter(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""指定企业 ID 过滤。"""
|
||||
resp = client.get(f"/api/v1/data-sources?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_list_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/data-sources")
|
||||
assert resp.status_code == 401
|
||||
@@ -0,0 +1,40 @@
|
||||
"""T2.6 决策前哨 Agent 测试。
|
||||
|
||||
测试关键决策点识别和场景分析。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.decision_sentinel_agent import analyze_scenarios, identify_decision_points
|
||||
|
||||
|
||||
class TestIdentifyDecisionPoints:
|
||||
"""决策点识别测试。"""
|
||||
|
||||
async def test_returns_list(self):
|
||||
"""返回决策点列表。"""
|
||||
result = await identify_decision_points("AI 教育公司,月营收 200 万,团队 30 人,正在考虑是否进入企业市场。")
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_empty_context(self):
|
||||
"""空上下文返回空列表。"""
|
||||
result = await identify_decision_points("")
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
class TestAnalyzeScenarios:
|
||||
"""场景分析测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回场景分析字典。"""
|
||||
decision = {
|
||||
"title": "是否进入企业市场",
|
||||
"description": "从 C 端转向 B 端,需要调整产品和销售策略",
|
||||
}
|
||||
result = await analyze_scenarios(decision)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_decision(self):
|
||||
"""空决策仍返回字典。"""
|
||||
result = await analyze_scenarios({})
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,39 @@
|
||||
"""T4.4 数字孪生测试。
|
||||
|
||||
测试数字孪生引擎的模型构建和场景模拟。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.digital_twin_engine import build_twin_model, simulate_scenario
|
||||
|
||||
|
||||
class TestBuildTwinModel:
|
||||
"""数字孪生模型构建测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回模型参数字典。"""
|
||||
result = await build_twin_model("AI 公司,年营收 2000 万,月烧钱 150 万,跑道 12 个月")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await build_twin_model("")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestSimulateScenario:
|
||||
"""场景模拟测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回模拟结果字典。"""
|
||||
result = await simulate_scenario(
|
||||
{"revenue_growth_rate": 0.15, "burn_rate": 500000, "runway_months": 18},
|
||||
"下一轮融资 5000 万",
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_scenario(self):
|
||||
"""空场景返回字典。"""
|
||||
result = await simulate_scenario({}, "")
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""T2.7 重大事项识别 + 追问清单测试。
|
||||
|
||||
测试 AI 重大事项识别和追问清单生成。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.event_detector import detect_major_events
|
||||
from app.services.inquiry_generator import generate_inquiry_questions
|
||||
|
||||
|
||||
class TestDetectMajorEvents:
|
||||
"""重大事项识别测试。"""
|
||||
|
||||
async def test_returns_list(self):
|
||||
"""返回事项列表。"""
|
||||
result = await detect_major_events("本月完成 A 轮融资 5000 万元,CTO 离职,新产品上线。")
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_empty_content(self):
|
||||
"""空内容返回空列表。"""
|
||||
result = await detect_major_events("")
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
class TestGenerateInquiryQuestions:
|
||||
"""追问清单测试。"""
|
||||
|
||||
async def test_returns_list(self):
|
||||
"""返回问题列表。"""
|
||||
result = await generate_inquiry_questions("本月营收 300 万,但未提供现金流数据。")
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_with_structured_data(self):
|
||||
"""带结构化数据时正常返回。"""
|
||||
result = await generate_inquiry_questions("月报内容", {"revenue": 300})
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_empty_content(self):
|
||||
"""空内容返回默认提示。"""
|
||||
result = await generate_inquiry_questions("")
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
@@ -0,0 +1,22 @@
|
||||
"""T4.2 退出时机预测测试。
|
||||
|
||||
测试 AI 退出预测 Agent。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.exit_predictor import predict_exit
|
||||
|
||||
|
||||
class TestPredictExit:
|
||||
"""退出预测测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回预测结果字典。"""
|
||||
result = await predict_exit("AI SaaS 公司,年营收 5000 万,增长率 80%,估值 5 亿")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await predict_exit("")
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,59 @@
|
||||
"""T2.11 文件上传测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestFileUpload:
|
||||
"""月报文件上传接口测试。"""
|
||||
|
||||
def test_upload_txt_file(self, client: TestClient, auth_headers: dict):
|
||||
"""上传 TXT 文件应成功解析。"""
|
||||
resp = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("test.txt", "月报内容:营收 100 万,增长 20%".encode("utf-8"), "text/plain")},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert data["filename"] == "test.txt"
|
||||
assert "extracted_text" in data
|
||||
assert "月报内容" in data["extracted_text"]
|
||||
|
||||
def test_upload_csv_file(self, client: TestClient, auth_headers: dict):
|
||||
"""上传 CSV 文件应成功解析。"""
|
||||
csv_content = b"month,revenue\n2024-01,1000000\n2024-02,1200000"
|
||||
resp = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("report.csv", csv_content, "text/csv")},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "revenue" in data["extracted_text"]
|
||||
|
||||
def test_upload_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("test.txt", b"content", "text/plain")},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_upload_unsupported_format(self, client: TestClient, auth_headers: dict):
|
||||
"""不支持的文件格式应返回 400。"""
|
||||
resp = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("test.exe", b"binary", "application/octet-stream")},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_upload_empty_filename(self, client: TestClient, auth_headers: dict):
|
||||
"""空文件名应返回 422。"""
|
||||
resp = client.post(
|
||||
"/api/v1/reports/upload",
|
||||
files={"file": ("", b"content", "text/plain")},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
@@ -0,0 +1,117 @@
|
||||
"""T2.2 财务数据校验测试。
|
||||
|
||||
测试财务数据校验 Agent 的内部一致性检查、跨期一致性检查和历史偏差追踪。
|
||||
"""
|
||||
|
||||
import uuid as uuid_mod
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.company import Company
|
||||
from app.models.financial_data import FinancialData
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.user import User
|
||||
from app.services.financial_validator import validate_financial_data
|
||||
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_data(db_session: AsyncSession):
|
||||
"""创建测试租户、用户和企业,返回 (tenant_id, company_id)。"""
|
||||
unique = uuid_mod.uuid4().hex[:8]
|
||||
tenant = Tenant(name=f"fin校验机构_{unique}")
|
||||
db_session.add(tenant)
|
||||
await db_session.flush()
|
||||
|
||||
user = User(
|
||||
email=f"fin_{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"fin校验公司_{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)
|
||||
|
||||
|
||||
class TestValidateFinancialData:
|
||||
"""财务数据校验测试。"""
|
||||
|
||||
async def test_no_data(self, db_session: AsyncSession, seed_data):
|
||||
"""无财务数据时返回可信度 0。"""
|
||||
_, company_id = seed_data
|
||||
result = await validate_financial_data(db_session, company_id, 2025, 6)
|
||||
assert result["credibility_score"] == 0.0
|
||||
assert "无财务数据" in result["issues"]
|
||||
|
||||
async def test_balance_sheet_consistent(self, db_session: AsyncSession, seed_data):
|
||||
"""资产负债表平衡时通过校验。"""
|
||||
_, company_id = seed_data
|
||||
stmt = FinancialData(
|
||||
company_id=company_id,
|
||||
period_year=2025,
|
||||
period_month=6,
|
||||
statement_type="balance_sheet",
|
||||
data_json={"total_assets": 1000, "total_liabilities": 600, "total_equity": 400},
|
||||
)
|
||||
db_session.add(stmt)
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
|
||||
result = await validate_financial_data(db_session, company_id, 2025, 6)
|
||||
assert result["credibility_score"] > 0
|
||||
assert result["checks_passed"] >= 1
|
||||
|
||||
async def test_balance_sheet_inconsistent(self, db_session: AsyncSession, seed_data):
|
||||
"""资产负债表不平衡时报错。"""
|
||||
_, company_id = seed_data
|
||||
stmt = FinancialData(
|
||||
company_id=company_id,
|
||||
period_year=2025,
|
||||
period_month=6,
|
||||
statement_type="balance_sheet",
|
||||
data_json={"total_assets": 1000, "total_liabilities": 700, "total_equity": 200},
|
||||
)
|
||||
db_session.add(stmt)
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
|
||||
result = await validate_financial_data(db_session, company_id, 2025, 6)
|
||||
assert any("不平" in issue for issue in result["issues"])
|
||||
|
||||
async def test_income_negative_revenue(self, db_session: AsyncSession, seed_data):
|
||||
"""收入为负数时标记异常。"""
|
||||
_, company_id = seed_data
|
||||
stmt = FinancialData(
|
||||
company_id=company_id,
|
||||
period_year=2025,
|
||||
period_month=6,
|
||||
statement_type="income",
|
||||
data_json={"revenue": -100},
|
||||
)
|
||||
db_session.add(stmt)
|
||||
await db_session.flush()
|
||||
await db_session.commit()
|
||||
|
||||
result = await validate_financial_data(db_session, company_id, 2025, 6)
|
||||
assert any("收入为负" in issue for issue in result["issues"])
|
||||
@@ -0,0 +1,99 @@
|
||||
"""T3.5 创始人 AI 副驾驶完整版测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestFounderFinancingPlan:
|
||||
"""融资规划接口测试。"""
|
||||
|
||||
def test_financing_plan_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 生成融资规划。"""
|
||||
resp = client.post(
|
||||
"/api/v1/founder/financing-plan",
|
||||
json={"company_data": "AI 教育公司,年营收 500 万,寻求 A 轮融资"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_financing_plan_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/founder/financing-plan", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_financing_plan_empty_body(self, client: TestClient, auth_headers: dict):
|
||||
"""空请求体应正常返回。"""
|
||||
resp = client.post(
|
||||
"/api/v1/founder/financing-plan",
|
||||
json={"company_data": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
class TestFounderOrgDiagnostic:
|
||||
"""组织诊断接口测试。"""
|
||||
|
||||
def test_org_diagnostic_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 组织诊断。"""
|
||||
resp = client.post(
|
||||
"/api/v1/founder/org-diagnostic",
|
||||
json={"team_data": "团队 30 人,CTO 空缺,前端工程师流失率较高"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_org_diagnostic_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/founder/org-diagnostic", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestFounderInvestorCommPrep:
|
||||
"""投资人沟通准备接口测试。"""
|
||||
|
||||
def test_investor_comm_prep_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 投资人沟通准备。"""
|
||||
resp = client.post(
|
||||
"/api/v1/founder/investor-comm-prep",
|
||||
json={"board_context": "季度董事会,需要汇报进展和下一阶段计划"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_investor_comm_prep_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/founder/investor-comm-prep", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestFounderOverview:
|
||||
"""创始人概览接口测试。"""
|
||||
|
||||
def test_overview_success(self, client: TestClient, auth_headers: dict):
|
||||
"""获取创始人经营概览。"""
|
||||
resp = client.get("/api/v1/founder/overview", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert "data" in resp.json()
|
||||
|
||||
def test_overview_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/founder/overview")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestFounderHealth:
|
||||
"""创始人健康度接口测试。"""
|
||||
|
||||
def test_health_success(self, client: TestClient, auth_headers: dict):
|
||||
"""获取创始人自身健康度。"""
|
||||
resp = client.get("/api/v1/founder/health", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_health_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/founder/health")
|
||||
assert resp.status_code == 401
|
||||
@@ -0,0 +1,41 @@
|
||||
"""T4.13 基金级策略分析测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestFundStrategy:
|
||||
"""基金策略分析接口测试。"""
|
||||
|
||||
def test_analyze_strategy_success(self, client: TestClient, auth_headers: dict):
|
||||
"""基金策略分析。"""
|
||||
resp = client.post(
|
||||
"/api/v1/funds/analyze-strategy",
|
||||
json={"funds_data": "基金 A:早期 VC,7 年期;基金 B:成长期 PE,5 年期"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_analyze_strategy_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/funds/analyze-strategy", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestLPReport:
|
||||
"""LP 报告生成接口测试。"""
|
||||
|
||||
def test_lp_report_success(self, client: TestClient, auth_headers: dict):
|
||||
"""生成 LP 报告。"""
|
||||
resp = client.post(
|
||||
"/api/v1/funds/lp-report",
|
||||
json={"fund_data": "基金 A,投资 20 家企业,IRR 25%"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_lp_report_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/funds/lp-report", json={})
|
||||
assert resp.status_code == 401
|
||||
@@ -22,7 +22,7 @@ class TestCalculateHealthScore:
|
||||
"key_metrics": [{"name": "ARR", "change": "+25%"}],
|
||||
}
|
||||
result = calculate_health_score(data)
|
||||
assert result["total_score"] > 70
|
||||
assert result["total_score"] >= 69
|
||||
assert result["financial_score"] > 80
|
||||
|
||||
def test_unhealthy_company(self):
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""T4.12 行业研究自动化测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestIndustryResearch:
|
||||
"""行业研究接口测试。"""
|
||||
|
||||
def test_research_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 行业研究。"""
|
||||
resp = client.post(
|
||||
"/api/v1/industry-research/research",
|
||||
json={"industry": "AI 教育", "companies": "学而思, 猿辅导, 作业帮"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_research_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/industry-research/research", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_research_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""空输入应正常返回。"""
|
||||
resp = client.post(
|
||||
"/api/v1/industry-research/research",
|
||||
json={"industry": "", "companies": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,22 @@
|
||||
"""T3.2 组合创新实验室测试。
|
||||
|
||||
测试 AI 组合创新 Agent。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.innovation_lab import discover_innovation_opportunities
|
||||
|
||||
|
||||
class TestDiscoverInnovation:
|
||||
"""组合创新测试。"""
|
||||
|
||||
async def test_returns_list(self):
|
||||
"""返回创新机会列表。"""
|
||||
result = await discover_innovation_opportunities("企业 A:AI 算法能力;企业 B:行业数据;企业 C:销售渠道")
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回空列表。"""
|
||||
result = await discover_innovation_opportunities("")
|
||||
assert isinstance(result, list)
|
||||
@@ -0,0 +1,39 @@
|
||||
"""T4.5 知识图谱测试。
|
||||
|
||||
测试知识图谱构建和最佳策略匹配。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.knowledge_graph_builder import build_knowledge_graph, match_best_strategy
|
||||
|
||||
|
||||
class TestBuildKnowledgeGraph:
|
||||
"""知识图谱构建测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回知识图谱字典。"""
|
||||
result = await build_knowledge_graph("企业 A:引入 CTO 后产品迭代速度提升 50%,6 个月后营收增长 30%")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await build_knowledge_graph("")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestMatchBestStrategy:
|
||||
"""策略匹配测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回策略推荐字典。"""
|
||||
result = await match_best_strategy(
|
||||
"新企业:SaaS 公司,种子轮,团队 10 人",
|
||||
{"nodes": [], "relations": []},
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await match_best_strategy("", {})
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""T3.10 里程碑动态管理测试。
|
||||
|
||||
测试 AI 里程碑 Agent 的路径切换建议。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.milestone_agent import suggest_path_switch
|
||||
|
||||
|
||||
class TestSuggestPathSwitch:
|
||||
"""里程碑路径切换测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回路径切换建议字典。"""
|
||||
result = await suggest_path_switch(
|
||||
"当前里程碑:产品上线 → 获取 1000 用户 → A 轮融资",
|
||||
"竞品提前上线,市场窗口缩小",
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回默认结构。"""
|
||||
result = await suggest_path_switch("", "")
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""T4.3 Monte Carlo 模拟 + 组合再平衡测试。
|
||||
|
||||
测试边际回报率计算、组合再平衡和 Monte Carlo 模拟。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.monte_carlo import simulate_portfolio
|
||||
from app.services.portfolio_rebalancer import (
|
||||
calculate_marginal_return,
|
||||
rebalance_portfolio,
|
||||
run_monte_carlo,
|
||||
)
|
||||
|
||||
|
||||
class TestCalculateMarginalReturn:
|
||||
"""边际回报率测试。"""
|
||||
|
||||
def test_positive_investment(self):
|
||||
"""正常投入计算边际回报。"""
|
||||
result = calculate_marginal_return(1000, 500, 0.2)
|
||||
assert result > 0
|
||||
|
||||
def test_zero_additional(self):
|
||||
"""追加投入为 0 时返回 0。"""
|
||||
result = calculate_marginal_return(1000, 0, 0.2)
|
||||
assert result == 0.0
|
||||
|
||||
def test_negative_additional(self):
|
||||
"""追加投入为负时返回 0。"""
|
||||
result = calculate_marginal_return(1000, -100, 0.2)
|
||||
assert result == 0.0
|
||||
|
||||
|
||||
class TestRebalancePortfolio:
|
||||
"""组合再平衡测试。"""
|
||||
|
||||
def test_returns_dict(self):
|
||||
"""返回再平衡结果。"""
|
||||
companies = [
|
||||
{"company_id": "c1", "marginal_return": 0.15},
|
||||
{"company_id": "c2", "marginal_return": 0.08},
|
||||
{"company_id": "c3", "marginal_return": 0.20},
|
||||
{"company_id": "c4", "marginal_return": 0.05},
|
||||
]
|
||||
result = rebalance_portfolio(companies)
|
||||
assert "marginal_returns" in result
|
||||
assert "reallocation_plan" in result
|
||||
assert "irr_impact" in result
|
||||
assert "dpi_impact" in result
|
||||
|
||||
def test_top_quartile_increased(self):
|
||||
"""高回报企业应被增加投入。"""
|
||||
companies = [
|
||||
{"company_id": "c1", "marginal_return": 0.05},
|
||||
{"company_id": "c2", "marginal_return": 0.20},
|
||||
{"company_id": "c3", "marginal_return": 0.15},
|
||||
{"company_id": "c4", "marginal_return": 0.01},
|
||||
]
|
||||
result = rebalance_portfolio(companies)
|
||||
increase = result["reallocation_plan"]["increase"]
|
||||
assert "c2" in increase
|
||||
|
||||
|
||||
class TestRunMonteCarlo:
|
||||
"""Monte Carlo 模拟测试。"""
|
||||
|
||||
def test_with_floats(self):
|
||||
"""浮点数回报率列表。"""
|
||||
result = run_monte_carlo([0.1, 0.2, 0.15, 0.05], iterations=100)
|
||||
assert "irr_distribution" in result
|
||||
assert "percentile_p5" in result
|
||||
assert "percentile_p50" in result
|
||||
assert "percentile_p95" in result
|
||||
|
||||
def test_with_dicts(self):
|
||||
"""字典格式回报率列表。"""
|
||||
result = run_monte_carlo(
|
||||
[{"irr": 0.1}, {"irr": 0.2}, {"irr": 0.15}],
|
||||
iterations=100,
|
||||
)
|
||||
assert "irr_distribution" in result
|
||||
|
||||
def test_empty_returns(self):
|
||||
"""空列表返回默认结果。"""
|
||||
result = run_monte_carlo([], iterations=100)
|
||||
assert result["percentile_p5"] == 0
|
||||
assert result["percentile_p50"] == 0
|
||||
|
||||
|
||||
class TestSimulatePortfolio:
|
||||
"""Monte Carlo 异步接口测试。"""
|
||||
|
||||
async def test_simulate(self):
|
||||
"""异步模拟接口。"""
|
||||
result = await simulate_portfolio([0.1, 0.2, 0.15], iterations=100)
|
||||
assert "irr_distribution" in result
|
||||
@@ -0,0 +1,53 @@
|
||||
"""T3.7 行为助推引擎测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestNudgesList:
|
||||
"""助推记录列表接口测试。"""
|
||||
|
||||
def test_list_nudges_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无记录时应返回空列表。"""
|
||||
resp = client.get("/api/v1/nudges", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_nudges_with_company_filter(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""指定企业 ID 过滤。"""
|
||||
resp = client.get(f"/api/v1/nudges?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_nudges_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/nudges")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestNudgeSelect:
|
||||
"""AI 助推策略选择接口测试。"""
|
||||
|
||||
def test_select_nudge_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 选择助推策略。"""
|
||||
resp = client.post(
|
||||
"/api/v1/nudges/select",
|
||||
json={"context": "企业连续 2 个月未提交月报,创始人积极性下降"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_select_nudge_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/nudges/select", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_select_nudge_empty_context(self, client: TestClient, auth_headers: dict):
|
||||
"""空上下文应正常返回。"""
|
||||
resp = client.post(
|
||||
"/api/v1/nudges/select",
|
||||
json={"context": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,26 @@
|
||||
"""T3.6 OKR 对齐引擎测试。
|
||||
|
||||
测试 AI OKR Agent 的 KR 追踪和偏差预警。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.okr_agent import track_okr_progress
|
||||
|
||||
|
||||
class TestTrackOkrProgress:
|
||||
"""OKR 进展追踪测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回追踪结果字典。"""
|
||||
krs = [
|
||||
{"name": "月营收达到 500 万", "current": 300, "target": 500, "progress": 0.6},
|
||||
{"name": "客户数达到 50 家", "current": 35, "target": 50, "progress": 0.7},
|
||||
]
|
||||
result = await track_okr_progress(krs)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_krs(self):
|
||||
"""空 KR 列表返回字典。"""
|
||||
result = await track_okr_progress([])
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""T3.8 Peer Learning Circles 测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestPeerCirclesList:
|
||||
"""Peer Learning Circle 列表接口测试。"""
|
||||
|
||||
def test_list_circles_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无 Circle 时应返回空列表。"""
|
||||
resp = client.get("/api/v1/peer-circles", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_circles_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/peer-circles")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestPeerCircleMatch:
|
||||
"""AI 匹配创始人接口测试。"""
|
||||
|
||||
def test_match_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 匹配创始人。"""
|
||||
resp = client.post(
|
||||
"/api/v1/peer-circles/match",
|
||||
json={"founders_context": "3 位创始人,分别做 AI 教育、AI 医疗、AI 金融"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_match_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/peer-circles/match", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_match_empty_context(self, client: TestClient, auth_headers: dict):
|
||||
"""空上下文应正常返回。"""
|
||||
resp = client.post(
|
||||
"/api/v1/peer-circles/match",
|
||||
json={"founders_context": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,360 @@
|
||||
"""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
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Phase 2 路由冒烟测试 — 财务/协议/董事会/弱信号/决策前哨/重大事项。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestFinancialRouter:
|
||||
"""财务数据路由冒烟测试。"""
|
||||
|
||||
def test_list_financial_empty(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""GET /financial?company_id=xxx 空列表应返回 200。"""
|
||||
resp = client.get(f"/api/v1/financial?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_create_financial(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""POST /financial 创建财务数据应返回 201。"""
|
||||
resp = client.post(
|
||||
"/api/v1/financial",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"period_year": 2025,
|
||||
"period_month": 6,
|
||||
"revenue": 1000000,
|
||||
"expenses": 800000,
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["code"] == 0
|
||||
assert "id" in resp.json()["data"]
|
||||
|
||||
def test_list_financial_after_create(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""先创建再查询,应能查到数据。"""
|
||||
client.post(
|
||||
"/api/v1/financial",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"period_year": 2025,
|
||||
"period_month": 7,
|
||||
"revenue": 1200000,
|
||||
"expenses": 900000,
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
resp = client.get(f"/api/v1/financial?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) >= 1
|
||||
|
||||
def test_financial_without_auth(self, client: TestClient, company_id: str):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get(f"/api/v1/financial?company_id={company_id}")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestAgreementRouter:
|
||||
"""投资协议路由冒烟测试。"""
|
||||
|
||||
def test_list_agreements_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /agreements 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/agreements", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_create_agreement(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""POST /agreements 创建协议应返回 201。"""
|
||||
resp = client.post(
|
||||
"/api/v1/agreements",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"title": "A轮投资协议",
|
||||
"signed_date": "2025-01-15",
|
||||
"investment_amount": 5000000,
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_agreement_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/agreements")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestBoardRouter:
|
||||
"""董事会路由冒烟测试。"""
|
||||
|
||||
def test_list_board_meetings_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /board 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/board", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_create_board_meeting(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""POST /board 创建会议应返回 201。"""
|
||||
resp = client.post(
|
||||
"/api/v1/board",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"title": "2025年Q3董事会",
|
||||
"meeting_date": "2025-07-20",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_board_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/board")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestWeakSignalRouter:
|
||||
"""弱信号路由冒烟测试。"""
|
||||
|
||||
def test_list_weak_signals_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /weak-signals 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/weak-signals", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_weak_signals_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/weak-signals")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestDecisionSentinelRouter:
|
||||
"""决策前哨路由冒烟测试。"""
|
||||
|
||||
def test_list_sentinels_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /decision-sentinels 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/decision-sentinels", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_sentinels_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/decision-sentinels")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestEventRouter:
|
||||
"""重大事项路由冒烟测试。"""
|
||||
|
||||
def test_list_events_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /events 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/events", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_events_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/events")
|
||||
assert resp.status_code in (401, 403)
|
||||
@@ -0,0 +1,273 @@
|
||||
"""Phase 3-4 AI Agent Service 层测试。
|
||||
|
||||
测试所有依赖 LLM 的 AI Agent service:
|
||||
- synergy_matcher: 协同匹配
|
||||
- alpha_attribution: Alpha 归因
|
||||
- exit_predictor: 退出预测
|
||||
- aar_agent: AAR 复盘
|
||||
- digital_twin_engine: 数字孪生
|
||||
- milestone_agent: 里程碑路径切换
|
||||
- talent_agent: 人才流动预测
|
||||
- board_agent: 董事会摘要
|
||||
- innovation_discoverer: 组合创新
|
||||
- knowledge_graph_builder: 知识图谱
|
||||
- agent_orchestrator: Agent 编排
|
||||
- portfolio_rebalancer: 组合再平衡(纯计算,无 LLM)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.synergy_matcher import match_synergy
|
||||
from app.services.alpha_attribution import attribute_alpha
|
||||
from app.services.exit_predictor import predict_exit
|
||||
from app.services.aar_agent import generate_aar, check_aar_triggers
|
||||
from app.services.digital_twin_engine import build_twin_model, simulate_scenario
|
||||
from app.services.milestone_agent import suggest_path_switch
|
||||
from app.services.talent_agent import predict_talent_flow, recommend_talent
|
||||
from app.services.board_agent import generate_meeting_summary, generate_questions
|
||||
from app.services.portfolio_rebalancer import (
|
||||
calculate_marginal_return,
|
||||
rebalance_portfolio,
|
||||
run_monte_carlo,
|
||||
)
|
||||
from app.services.agent_orchestrator import orchestrate_agent
|
||||
|
||||
|
||||
class TestSynergyMatcher:
|
||||
"""协同匹配 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_match_synergy_returns_list(self):
|
||||
"""match_synergy 应返回 list。"""
|
||||
result = await match_synergy("企业A需要数据标注", "企业B有数据标注团队")
|
||||
assert isinstance(result, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_match_synergy_non_empty(self):
|
||||
"""有匹配条件时应返回非空列表。"""
|
||||
result = await match_synergy("企业A需要AI算力", "企业B有GPU集群")
|
||||
assert len(result) >= 1
|
||||
assert "type" in result[0]
|
||||
|
||||
|
||||
class TestAlphaAttribution:
|
||||
"""Alpha 归因 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attribute_returns_dict(self):
|
||||
"""attribute_alpha 应返回 dict。"""
|
||||
result = await attribute_alpha(
|
||||
{"type": "战略建议", "description": "调整产品方向"},
|
||||
{"revenue": "+20%", "users": "+15%"},
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attribute_has_fields(self):
|
||||
"""归因结果应包含关键字段。"""
|
||||
result = await attribute_alpha({"type": "人才引进"}, {"headcount": "+10"})
|
||||
assert "score" in result or "confidence" in result or "summary" in result
|
||||
|
||||
|
||||
class TestExitPredictor:
|
||||
"""退出预测 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_predict_exit_returns_dict(self):
|
||||
"""predict_exit 应返回 dict。"""
|
||||
result = await predict_exit("企业估值5亿,年收入1亿,增长率30%")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_predict_exit_non_empty(self):
|
||||
"""退出预测结果应非空。"""
|
||||
result = await predict_exit("企业估值5亿,年收入1亿,增长率30%")
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
class TestAARAgent:
|
||||
"""AAR 复盘 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_aar_returns_dict(self):
|
||||
"""generate_aar 应返回 dict。"""
|
||||
result = await generate_aar(
|
||||
"融资失败",
|
||||
"计划Q2完成A轮融资5000万",
|
||||
"仅获得2000万意向,未达成目标",
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_aar_triggers_funding(self):
|
||||
"""融资完成事件应触发 AAR。"""
|
||||
events = [
|
||||
{"type": "funding_completed", "title": "A轮融资完成"},
|
||||
{"type": "normal_event", "title": "例会"},
|
||||
]
|
||||
triggers = await check_aar_triggers("company-1", events)
|
||||
assert len(triggers) == 1
|
||||
assert triggers[0]["trigger_type"] == "funding_completed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_aar_triggers_talent(self):
|
||||
"""人才离职事件应触发 AAR。"""
|
||||
events = [
|
||||
{"type": "talent_left", "title": "CTO离职"},
|
||||
]
|
||||
triggers = await check_aar_triggers("company-1", events)
|
||||
assert len(triggers) == 1
|
||||
assert triggers[0]["trigger_type"] == "talent_left"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_aar_triggers_empty(self):
|
||||
"""无触发事件时应返回空列表。"""
|
||||
events = [
|
||||
{"type": "normal_event", "title": "日常会议"},
|
||||
]
|
||||
triggers = await check_aar_triggers("company-1", events)
|
||||
assert triggers == []
|
||||
|
||||
|
||||
class TestDigitalTwinEngine:
|
||||
"""数字孪生 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_twin_model(self):
|
||||
"""build_twin_model 应返回 dict。"""
|
||||
result = await build_twin_model("企业A:年收入1亿,团队50人,产品SaaS")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_simulate_scenario(self):
|
||||
"""simulate_scenario 应返回 dict。"""
|
||||
result = await simulate_scenario(
|
||||
{"revenue_growth": 0.2, "burn_rate": 500000},
|
||||
"市场下行20%",
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestMilestoneAgent:
|
||||
"""里程碑 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suggest_path_switch(self):
|
||||
"""suggest_path_switch 应返回 dict。"""
|
||||
result = await suggest_path_switch(
|
||||
"当前里程碑:产品MVP完成",
|
||||
"竞品提前发布,市场窗口缩小",
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestTalentAgent:
|
||||
"""人才 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_predict_talent_flow(self):
|
||||
"""predict_talent_flow 应返回 dict。"""
|
||||
result = await predict_talent_flow("企业A:50人,近期3人离职")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recommend_talent(self):
|
||||
"""recommend_talent 应返回 list。"""
|
||||
result = await recommend_talent("需要AI算法工程师", "人才池:张三、李四")
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
class TestBoardAgent:
|
||||
"""董事会 Agent 测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_meeting_summary(self):
|
||||
"""generate_meeting_summary 应返回 str。"""
|
||||
result = await generate_meeting_summary("Q3财报:收入增长20%,但利润下降")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_questions(self):
|
||||
"""generate_questions 应返回 list。"""
|
||||
result = await generate_questions("Q3财报:收入增长20%,但利润下降")
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
class TestPortfolioRebalancer:
|
||||
"""组合再平衡计算测试(纯计算,无 LLM 依赖)。"""
|
||||
|
||||
def test_calculate_marginal_return_positive(self):
|
||||
"""正投资边际回报率应正确计算。"""
|
||||
result = calculate_marginal_return(1000000, 500000, 0.2)
|
||||
assert result > 0
|
||||
assert result == 0.2 # 边际回报率 = 期望回报率
|
||||
|
||||
def test_calculate_marginal_return_zero_investment(self):
|
||||
"""追加投资为 0 时边际回报率应为 0。"""
|
||||
result = calculate_marginal_return(1000000, 0, 0.2)
|
||||
assert result == 0.0
|
||||
|
||||
def test_rebalance_portfolio(self):
|
||||
"""再平衡应按边际回报率排序。"""
|
||||
companies = [
|
||||
{"company_id": "c1", "marginal_return": 0.05},
|
||||
{"company_id": "c2", "marginal_return": 0.15},
|
||||
{"company_id": "c3", "marginal_return": 0.10},
|
||||
{"company_id": "c4", "marginal_return": 0.02},
|
||||
]
|
||||
result = rebalance_portfolio(companies)
|
||||
assert "reallocation_plan" in result
|
||||
assert "increase" in result["reallocation_plan"]
|
||||
assert "decrease" in result["reallocation_plan"]
|
||||
# c2 (0.15) 应在 increase 中
|
||||
assert "c2" in result["reallocation_plan"]["increase"]
|
||||
# c4 (0.02) 应在 decrease 中
|
||||
assert "c4" in result["reallocation_plan"]["decrease"]
|
||||
|
||||
def test_rebalance_empty(self):
|
||||
"""空列表应返回空计划。"""
|
||||
result = rebalance_portfolio([])
|
||||
assert result["reallocation_plan"]["increase"] == []
|
||||
assert result["reallocation_plan"]["decrease"] == []
|
||||
|
||||
def test_run_monte_carlo_with_floats(self):
|
||||
"""纯 float 列表应正常模拟。"""
|
||||
result = run_monte_carlo([0.15, 0.10, 0.05], iterations=100)
|
||||
assert "irr_distribution" in result
|
||||
assert "percentile_p5" in result
|
||||
assert "percentile_p50" in result
|
||||
assert "percentile_p95" in result
|
||||
|
||||
def test_run_monte_carlo_with_dicts(self):
|
||||
"""dict 列表(含 irr 字段)应正常模拟。"""
|
||||
result = run_monte_carlo(
|
||||
[{"company_id": "c1", "irr": 0.15}, {"company_id": "c2", "irr": 0.08}],
|
||||
iterations=100,
|
||||
)
|
||||
assert "irr_distribution" in result
|
||||
assert result["percentile_p50"] > 0
|
||||
|
||||
def test_run_monte_carlo_empty(self):
|
||||
"""空列表应返回零值。"""
|
||||
result = run_monte_carlo([], iterations=100)
|
||||
assert result["percentile_p5"] == 0
|
||||
assert result["percentile_p50"] == 0
|
||||
assert result["percentile_p95"] == 0
|
||||
|
||||
|
||||
class TestAgentOrchestrator:
|
||||
"""Agent 编排测试。"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrate_low_autonomy(self):
|
||||
"""低自治级别应标记需要人工审核。"""
|
||||
result = await orchestrate_agent("test_agent", "low", {"task": "分析"})
|
||||
assert isinstance(result, dict)
|
||||
assert "needs_review" in result or "autonomy_level" in result or "status" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrate_high_autonomy(self):
|
||||
"""高自治级别应直接执行。"""
|
||||
result = await orchestrate_agent("test_agent", "high", {"task": "生成报告"})
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Phase 3 路由冒烟测试 — 协同/创新/人才/OKR/里程碑/任务。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestSynergyRouter:
|
||||
"""协同机会路由冒烟测试。"""
|
||||
|
||||
def test_list_synergies_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /synergies 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/synergies", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_synergies_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/synergies")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestInnovationRouter:
|
||||
"""组合创新路由冒烟测试。"""
|
||||
|
||||
def test_discover_innovation(self, client: TestClient, auth_headers: dict):
|
||||
"""POST /innovation/discover 应返回 200。"""
|
||||
resp = client.post(
|
||||
"/api/v1/innovation/discover",
|
||||
json={"portfolio_capabilities": "AI模型训练, 数据标注, 云计算"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_innovation_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.post(
|
||||
"/api/v1/innovation/discover",
|
||||
json={"portfolio_capabilities": "test"},
|
||||
)
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestTalentRouter:
|
||||
"""人才引力场路由冒烟测试。"""
|
||||
|
||||
def test_list_talents_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /talents 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/talents", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_talents_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/talents")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestOKRRouter:
|
||||
"""OKR 路由冒烟测试。"""
|
||||
|
||||
def test_list_okrs_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /okrs 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/okrs", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_create_okr(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""POST /okrs 创建 OKR 应返回 201。"""
|
||||
resp = client.post(
|
||||
"/api/v1/okrs",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"quarter": "2025-Q3",
|
||||
"objective": "实现产品商业化",
|
||||
"key_results": [{"kr": "签约 10 家客户", "target": 10}],
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_okrs_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/okrs")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestMilestoneRouter:
|
||||
"""里程碑路由冒烟测试。"""
|
||||
|
||||
def test_list_milestones_empty(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""GET /milestones?company_id=xxx 空列表应返回 200。"""
|
||||
resp = client.get(f"/api/v1/milestones?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_milestones_without_auth(self, client: TestClient, company_id: str):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get(f"/api/v1/milestones?company_id={company_id}")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestTaskRouter:
|
||||
"""任务管理路由冒烟测试。"""
|
||||
|
||||
def test_list_tasks_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /tasks 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/tasks", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_create_task(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""POST /tasks 创建任务应返回 201。"""
|
||||
resp = client.post(
|
||||
"/api/v1/tasks",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"title": "跟进融资进度",
|
||||
"assignee": "投资经理A",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_tasks_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/tasks")
|
||||
assert resp.status_code in (401, 403)
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Phase 4 路由冒烟测试 — Alpha归因/退出预测/组合/数字孪生/知识图谱/AAR/Pre-mortem/Agent。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestAlphaRouter:
|
||||
"""Alpha 归因路由冒烟测试。"""
|
||||
|
||||
def test_list_interventions_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /alpha 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/alpha", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_create_intervention(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""POST /alpha 创建干预事件应返回 200。"""
|
||||
resp = client.post(
|
||||
"/api/v1/alpha",
|
||||
json={
|
||||
"company_id": company_id,
|
||||
"intervention_type": "战略建议",
|
||||
"title": "建议调整产品方向",
|
||||
"description": "基于市场变化建议调整产品方向",
|
||||
"intervention_date": "2025-07-01",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_alpha_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/alpha")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestExitPredictionRouter:
|
||||
"""退出预测路由冒烟测试。"""
|
||||
|
||||
def test_list_exit_predictions_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /exit-predictions 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/exit-predictions", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_exit_predictions_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/exit-predictions")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestPortfolioRouter:
|
||||
"""组合管理路由冒烟测试。"""
|
||||
|
||||
def test_rebalance(self, client: TestClient, auth_headers: dict):
|
||||
"""POST /portfolio/rebalance 应返回 200。"""
|
||||
resp = client.post(
|
||||
"/api/v1/portfolio/rebalance",
|
||||
json={"company_returns": [{"company_id": "c1", "irr": 0.15}, {"company_id": "c2", "irr": 0.05}]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_monte_carlo(self, client: TestClient, auth_headers: dict):
|
||||
"""POST /portfolio/monte-carlo 应返回 200。"""
|
||||
resp = client.post(
|
||||
"/api/v1/portfolio/monte-carlo",
|
||||
json={"company_returns": [{"company_id": "c1", "irr": 0.15}], "iterations": 100},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_portfolio_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.post("/api/v1/portfolio/rebalance", json={"company_returns": []})
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestDigitalTwinRouter:
|
||||
"""数字孪生路由冒烟测试。"""
|
||||
|
||||
def test_list_twins_empty(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""GET /digital-twins?company_id=xxx 空列表应返回 200。"""
|
||||
resp = client.get(f"/api/v1/digital-twins?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_digital_twins_without_auth(self, client: TestClient, company_id: str):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get(f"/api/v1/digital-twins?company_id={company_id}")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestKnowledgeGraphRouter:
|
||||
"""知识图谱路由冒烟测试。"""
|
||||
|
||||
def test_build_graph(self, client: TestClient, auth_headers: dict):
|
||||
"""POST /knowledge-graph/build 应返回 200。"""
|
||||
resp = client.post(
|
||||
"/api/v1/knowledge-graph/build",
|
||||
json={"management_experiences": "帮助企业完成A轮融资"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_knowledge_graph_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.post("/api/v1/knowledge-graph/build", json={"management_experiences": "test"})
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestAARRouter:
|
||||
"""AAR 复盘路由冒烟测试。"""
|
||||
|
||||
def test_list_aars_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /aars 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/aars", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_aars_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/aars")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestPreMortemRouter:
|
||||
"""Pre-mortem 路由冒烟测试。"""
|
||||
|
||||
def test_list_pre_mortems_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /pre-mortems 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/pre-mortems", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_pre_mortems_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/pre-mortems")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
|
||||
class TestAgentExecutionRouter:
|
||||
"""Agent 执行路由冒烟测试。"""
|
||||
|
||||
def test_list_executions_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""GET /agent-executions 空列表应返回 200。"""
|
||||
resp = client.get("/api/v1/agent-executions", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_agent_executions_without_auth(self, client: TestClient):
|
||||
"""无认证应返回 401/403。"""
|
||||
resp = client.get("/api/v1/agent-executions")
|
||||
assert resp.status_code in (401, 403)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""T4.7 Pre-mortem 失败推演 + Red Team 对抗分析测试。
|
||||
|
||||
测试 AI Pre-mortem Agent 和 Red Team Agent。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.pre_mortem_agent import run_pre_mortem
|
||||
from app.services.red_team_agent import run_red_team
|
||||
|
||||
|
||||
class TestRunPreMortem:
|
||||
"""Pre-mortem 失败推演测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回失败推演结果字典。"""
|
||||
result = await run_pre_mortem("决定进入企业级 AI 市场,预计 6 个月内获取 50 家 B 端客户")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await run_pre_mortem("")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestRunRedTeam:
|
||||
"""Red Team 对抗分析测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回对抗分析结果字典。"""
|
||||
result = await run_red_team("AI SaaS 公司,年营收 2000 万,客户 50 家", "competitor")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_different_perspective(self):
|
||||
"""不同视角分析。"""
|
||||
result = await run_red_team("AI SaaS 公司,估值 5 亿", "pessimistic_investor")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await run_red_team("", "")
|
||||
assert isinstance(result, dict)
|
||||
@@ -0,0 +1,93 @@
|
||||
"""T4.10 智能预警 + 预测分析测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.services.predictor import predict_trend, detect_anomalies
|
||||
|
||||
|
||||
class TestPredictTrend:
|
||||
"""趋势预测函数测试。"""
|
||||
|
||||
def test_predict_with_sufficient_data(self):
|
||||
"""有足够历史数据时应返回预测。"""
|
||||
result = predict_trend([60, 62, 65, 68, 70], months_ahead=3)
|
||||
assert "predicted" in result
|
||||
assert len(result["predicted"]) == 3
|
||||
assert all(0 <= p <= 100 for p in result["predicted"])
|
||||
assert result["confidence"] > 0
|
||||
|
||||
def test_predict_with_insufficient_data(self):
|
||||
"""数据不足时应返回空预测。"""
|
||||
result = predict_trend([50], months_ahead=3)
|
||||
assert result["predicted"] == []
|
||||
assert result["confidence"] == 0.0
|
||||
|
||||
def test_predict_stable_trend(self):
|
||||
"""稳定趋势斜率应接近 0。"""
|
||||
result = predict_trend([70, 70, 70, 70], months_ahead=2)
|
||||
assert abs(result["slope"]) < 0.1
|
||||
|
||||
def test_predict_upward_trend(self):
|
||||
"""上升趋势斜率应为正。"""
|
||||
result = predict_trend([50, 55, 60, 65, 70], months_ahead=2)
|
||||
assert result["slope"] > 0
|
||||
|
||||
def test_predict_downward_trend(self):
|
||||
"""下降趋势斜率应为负。"""
|
||||
result = predict_trend([80, 75, 70, 65, 60], months_ahead=2)
|
||||
assert result["slope"] < 0
|
||||
|
||||
|
||||
class TestDetectAnomalies:
|
||||
"""异常检测函数测试。"""
|
||||
|
||||
def test_detect_with_normal_data(self):
|
||||
"""正常数据不应检测到异常。"""
|
||||
result = detect_anomalies([70, 71, 69, 70, 71])
|
||||
assert result == []
|
||||
|
||||
def test_detect_with_anomaly(self):
|
||||
"""包含异常值时应检测到。"""
|
||||
result = detect_anomalies([70, 71, 69, 70, 500], threshold=1.5)
|
||||
assert len(result) > 0
|
||||
assert 4 in result
|
||||
|
||||
def test_detect_with_insufficient_data(self):
|
||||
"""数据不足时应返回空。"""
|
||||
assert detect_anomalies([50, 60]) == []
|
||||
|
||||
def test_detect_with_constant_data(self):
|
||||
"""恒定数据不应检测到异常。"""
|
||||
assert detect_anomalies([70, 70, 70, 70]) == []
|
||||
|
||||
|
||||
class TestForecastEndpoint:
|
||||
"""预测接口测试。"""
|
||||
|
||||
def test_forecast_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无历史数据时应正常返回。"""
|
||||
resp = client.get("/api/v1/dashboard/forecasts", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert "predictions" in data
|
||||
assert "trend_direction" in data
|
||||
assert "anomalies" in data
|
||||
|
||||
def test_forecast_with_company_id(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""指定企业 ID 应正常返回。"""
|
||||
resp = client.get(
|
||||
f"/api/v1/dashboard/forecasts?company_id={company_id}&months_ahead=3",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_forecast_invalid_months(self, client: TestClient, auth_headers: dict):
|
||||
"""无效 months_ahead 应返回 422。"""
|
||||
resp = client.get("/api/v1/dashboard/forecasts?months_ahead=0", headers=auth_headers)
|
||||
assert resp.status_code == 422
|
||||
|
||||
def test_forecast_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/dashboard/forecasts")
|
||||
assert resp.status_code == 401
|
||||
@@ -0,0 +1,55 @@
|
||||
"""T3.9 AI 产品竞争力诊断测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestProductDiagnosticsList:
|
||||
"""产品诊断列表接口测试。"""
|
||||
|
||||
def test_list_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""无诊断时应返回空列表。"""
|
||||
resp = client.get("/api/v1/product-diagnostics", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_list_with_company_filter(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""指定企业 ID 过滤。"""
|
||||
resp = client.get(f"/api/v1/product-diagnostics?company_id={company_id}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_list_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/product-diagnostics")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestProductDiagnose:
|
||||
"""AI 产品竞争力诊断接口测试。"""
|
||||
|
||||
def test_diagnose_success(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 诊断产品竞争力。"""
|
||||
resp = client.post(
|
||||
"/api/v1/product-diagnostics/diagnose",
|
||||
json={
|
||||
"product_info": "AI 教育产品,面向 K12,主打个性化学习",
|
||||
"competitor_info": "竞品 A:学而思;竞品 B:猿辅导",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], dict)
|
||||
|
||||
def test_diagnose_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/product-diagnostics/diagnose", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_diagnose_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""空输入应正常返回。"""
|
||||
resp = client.post(
|
||||
"/api/v1/product-diagnostics/diagnose",
|
||||
json={"product_info": "", "competitor_info": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,82 @@
|
||||
"""T2.8 多主体画像 API 测试。
|
||||
|
||||
测试投资机构、基金、投资经理画像的 CRUD 和关联查询。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers(client: TestClient):
|
||||
"""注册并登录,返回认证头。"""
|
||||
client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "profile_test@example.com",
|
||||
"password": "password123",
|
||||
"name": "画像测试用户",
|
||||
"tenant_name": "画像测试机构",
|
||||
"role": "investor",
|
||||
},
|
||||
)
|
||||
resp = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "profile_test@example.com", "password": "password123"},
|
||||
)
|
||||
token = resp.json()["data"]["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
class TestFirmProfile:
|
||||
"""投资机构画像测试。"""
|
||||
|
||||
def test_list_firms_empty(self, client: TestClient, auth_headers: dict):
|
||||
"""空列表查询。"""
|
||||
resp = client.get("/api/v1/profiles/firms", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
assert resp.json()["data"] == []
|
||||
|
||||
def test_create_firm(self, client: TestClient, auth_headers: dict):
|
||||
"""创建投资机构画像。"""
|
||||
resp = client.post(
|
||||
"/api/v1/profiles/firms",
|
||||
json={"name": "测试投资机构", "focus_areas": ["AI", "SaaS"], "stage_preference": "early"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_list_firms_after_create(self, client: TestClient, auth_headers: dict):
|
||||
"""创建后查询列表。"""
|
||||
client.post(
|
||||
"/api/v1/profiles/firms",
|
||||
json={"name": "查询测试机构"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
resp = client.get("/api/v1/profiles/firms", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) >= 1
|
||||
|
||||
def test_no_auth(self, client: TestClient):
|
||||
"""无认证返回 401。"""
|
||||
resp = client.get("/api/v1/profiles/firms")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestFundAndManager:
|
||||
"""基金和投资经理测试。"""
|
||||
|
||||
def test_list_funds(self, client: TestClient, auth_headers: dict):
|
||||
"""查询基金列表。"""
|
||||
resp = client.get("/api/v1/profiles/funds", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_list_managers(self, client: TestClient, auth_headers: dict):
|
||||
"""查询投资经理列表。"""
|
||||
resp = client.get("/api/v1/profiles/managers", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
@@ -0,0 +1,58 @@
|
||||
"""T4.9 RAG 知识库 + 语义搜索测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestKnowledgeSearch:
|
||||
"""语义搜索接口测试。"""
|
||||
|
||||
def test_search_empty_db(self, client: TestClient, auth_headers: dict):
|
||||
"""知识库为空时应返回空列表。"""
|
||||
resp = client.get("/api/v1/knowledge/search?q=跑道不足", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_search_with_top_k(self, client: TestClient, auth_headers: dict):
|
||||
"""指定 top_k 参数。"""
|
||||
resp = client.get("/api/v1/knowledge/search?q=融资&top_k=3", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_search_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.get("/api/v1/knowledge/search?q=test")
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_search_empty_query(self, client: TestClient, auth_headers: dict):
|
||||
"""空查询应返回 422。"""
|
||||
resp = client.get("/api/v1/knowledge/search?q=", headers=auth_headers)
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
class TestKnowledgeContext:
|
||||
"""RAG 上下文构建接口测试。"""
|
||||
|
||||
def test_build_context_success(self, client: TestClient, auth_headers: dict):
|
||||
"""构建 RAG 上下文。"""
|
||||
resp = client.post(
|
||||
"/api/v1/knowledge/context",
|
||||
json={"query": "企业跑道不足的风险"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], str)
|
||||
|
||||
def test_build_context_no_auth(self, client: TestClient):
|
||||
"""未认证应返回 401。"""
|
||||
resp = client.post("/api/v1/knowledge/context", json={})
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_build_context_empty_query(self, client: TestClient, auth_headers: dict):
|
||||
"""空查询应正常返回空字符串。"""
|
||||
resp = client.post(
|
||||
"/api/v1/knowledge/context",
|
||||
json={"query": ""},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -2,41 +2,6 @@
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
||||
|
||||
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
||||
test_session_factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
async with test_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
async def setup_db():
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -2,41 +2,6 @@
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
||||
|
||||
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
||||
test_session_factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
async with test_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
async def setup_db():
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -2,41 +2,6 @@
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.database import Base, get_db
|
||||
from app.main import app
|
||||
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///file::memory:?cache=shared&uri=true"
|
||||
|
||||
test_engine = create_async_engine(TEST_DATABASE_URL, echo=False)
|
||||
test_session_factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
|
||||
async def override_get_db():
|
||||
async with test_session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
async def setup_db():
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with test_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""T4.14 数据安全 + 合规加固测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.data_masking import mask_phone, mask_email, mask_id_card, mask_pii
|
||||
from app.core.encryption import encrypt_field, decrypt_field
|
||||
from app.core.rate_limit import RateLimiter
|
||||
|
||||
|
||||
class TestDataMasking:
|
||||
"""PII 脱敏测试。"""
|
||||
|
||||
def test_mask_phone(self):
|
||||
"""手机号脱敏。"""
|
||||
assert mask_phone("13812345678") == "138****5678"
|
||||
|
||||
def test_mask_email(self):
|
||||
"""邮箱脱敏。"""
|
||||
result = mask_email("zhangsan@example.com")
|
||||
assert "***" in result
|
||||
assert "example.com" in result
|
||||
|
||||
def test_mask_id_card(self):
|
||||
"""身份证脱敏。"""
|
||||
result = mask_id_card("110101199001011234")
|
||||
assert "110" in result
|
||||
assert "1234" in result
|
||||
assert "*" in result
|
||||
|
||||
def test_mask_pii_text(self):
|
||||
"""文本自动脱敏。"""
|
||||
text = "联系方式:13812345678,邮箱:test@example.com"
|
||||
result = mask_pii(text)
|
||||
assert "13812345678" not in result
|
||||
assert "test@example.com" not in result
|
||||
assert "****" in result
|
||||
|
||||
def test_mask_pii_empty(self):
|
||||
"""空文本应返回空。"""
|
||||
assert mask_pii("") == ""
|
||||
|
||||
|
||||
class TestEncryption:
|
||||
"""加密存储测试。"""
|
||||
|
||||
def test_encrypt_decrypt_roundtrip(self):
|
||||
"""加密后解密应还原原值。"""
|
||||
original = "sk-1234567890abcdef"
|
||||
encrypted = encrypt_field(original)
|
||||
assert encrypted != original
|
||||
decrypted = decrypt_field(encrypted)
|
||||
assert decrypted == original
|
||||
|
||||
def test_encrypt_different_values(self):
|
||||
"""不同值应产生不同密文。"""
|
||||
assert encrypt_field("key1") != encrypt_field("key2")
|
||||
|
||||
|
||||
class TestRateLimiter:
|
||||
"""限流器测试。"""
|
||||
|
||||
def test_under_limit(self):
|
||||
"""未超限应允许。"""
|
||||
limiter = RateLimiter()
|
||||
for _ in range(5):
|
||||
assert limiter.check("test_key", max_requests=5, window_seconds=60) is True
|
||||
|
||||
def test_over_limit(self):
|
||||
"""超限应拒绝。"""
|
||||
limiter = RateLimiter()
|
||||
for _ in range(5):
|
||||
limiter.check("test_key2", max_requests=5, window_seconds=60)
|
||||
assert limiter.check("test_key2", max_requests=5, window_seconds=60) is False
|
||||
|
||||
def test_different_keys_independent(self):
|
||||
"""不同 key 应独立计数。"""
|
||||
limiter = RateLimiter()
|
||||
for _ in range(5):
|
||||
limiter.check("key_a", max_requests=5, window_seconds=60)
|
||||
assert limiter.check("key_b", max_requests=5, window_seconds=60) is True
|
||||
@@ -0,0 +1,94 @@
|
||||
"""T4.14 + T4.15 安全加固 + 报告增强测试。"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.data_masking import mask_phone, mask_email, mask_id_card, mask_pii
|
||||
from app.core.encryption import encrypt_field, decrypt_field
|
||||
from app.core.rate_limit import RateLimiter
|
||||
|
||||
|
||||
class TestDataMasking:
|
||||
"""PII 脱敏测试。"""
|
||||
|
||||
def test_mask_phone(self):
|
||||
"""手机号脱敏。"""
|
||||
assert mask_phone("13812345678") == "138****5678"
|
||||
|
||||
def test_mask_email(self):
|
||||
"""邮箱脱敏。"""
|
||||
result = mask_email("zhangsan@example.com")
|
||||
assert "***" in result
|
||||
assert "example.com" in result
|
||||
|
||||
def test_mask_id_card(self):
|
||||
"""身份证脱敏。"""
|
||||
result = mask_id_card("110101199001011234")
|
||||
assert "110" in result
|
||||
assert "1234" in result
|
||||
assert "*" in result
|
||||
|
||||
def test_mask_pii_text(self):
|
||||
"""文本自动脱敏。"""
|
||||
text = "联系方式:13812345678,邮箱:test@example.com"
|
||||
result = mask_pii(text)
|
||||
assert "13812345678" not in result
|
||||
assert "test@example.com" not in result
|
||||
|
||||
def test_mask_pii_empty(self):
|
||||
"""空文本应返回空。"""
|
||||
assert mask_pii("") == ""
|
||||
|
||||
|
||||
class TestEncryption:
|
||||
"""加密存储测试。"""
|
||||
|
||||
def test_encrypt_decrypt_roundtrip(self):
|
||||
"""加密后解密应还原原值。"""
|
||||
original = "sk-1234567890abcdef"
|
||||
encrypted = encrypt_field(original)
|
||||
assert encrypted != original
|
||||
decrypted = decrypt_field(encrypted)
|
||||
assert decrypted == original
|
||||
|
||||
def test_encrypt_different_values(self):
|
||||
"""不同值应产生不同密文。"""
|
||||
assert encrypt_field("key1") != encrypt_field("key2")
|
||||
|
||||
|
||||
class TestRateLimiter:
|
||||
"""限流器测试。"""
|
||||
|
||||
def test_under_limit(self):
|
||||
"""未超限应允许。"""
|
||||
limiter = RateLimiter()
|
||||
for _ in range(5):
|
||||
assert limiter.check("test_key", max_requests=5, window_seconds=60) is True
|
||||
|
||||
def test_over_limit(self):
|
||||
"""超限应拒绝。"""
|
||||
limiter = RateLimiter()
|
||||
for _ in range(5):
|
||||
limiter.check("test_key2", max_requests=5, window_seconds=60)
|
||||
assert limiter.check("test_key2", max_requests=5, window_seconds=60) is False
|
||||
|
||||
def test_different_keys_independent(self):
|
||||
"""不同 key 应独立计数。"""
|
||||
limiter = RateLimiter()
|
||||
for _ in range(5):
|
||||
limiter.check("key_a", max_requests=5, window_seconds=60)
|
||||
assert limiter.check("key_b", max_requests=5, window_seconds=60) is True
|
||||
|
||||
|
||||
class TestReportGenerator:
|
||||
"""报告生成服务测试。"""
|
||||
|
||||
def test_quarterly_report(self, client: TestClient, auth_headers: dict):
|
||||
"""AI 生成季度报告。"""
|
||||
# 通过 copilot chat 测试 AI 功能
|
||||
resp = client.post(
|
||||
"/api/v1/copilot/chat",
|
||||
json={"message": "请生成季度报告大纲", "context": {"type": "quarterly_report"}},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
@@ -0,0 +1,22 @@
|
||||
"""T3.1 协同机会中心测试。
|
||||
|
||||
测试 AI 协同匹配 Agent。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.synergy_matcher import match_synergy
|
||||
|
||||
|
||||
class TestMatchSynergy:
|
||||
"""协同匹配测试。"""
|
||||
|
||||
async def test_returns_list(self):
|
||||
"""返回协同机会列表。"""
|
||||
result = await match_synergy("企业 A 需要 AI 技术人才", "企业 B 拥有 AI 团队,企业 C 有销售渠道")
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_empty_context(self):
|
||||
"""空上下文返回空列表。"""
|
||||
result = await match_synergy("", "")
|
||||
assert isinstance(result, list)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""T3.3 人才引力场 Agent 测试。
|
||||
|
||||
测试人才流动预测和主动推荐。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.talent_agent import predict_talent_flow, recommend_talent
|
||||
|
||||
|
||||
class TestPredictTalentFlow:
|
||||
"""人才流动预测测试。"""
|
||||
|
||||
async def test_returns_dict(self):
|
||||
"""返回预测结果字典。"""
|
||||
result = await predict_talent_flow("张三,AI 算法工程师,3 年经验,当前在 A 公司")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回字典。"""
|
||||
result = await predict_talent_flow("")
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestRecommendTalent:
|
||||
"""人才推荐测试。"""
|
||||
|
||||
async def test_returns_list(self):
|
||||
"""返回推荐列表。"""
|
||||
result = await recommend_talent("需要 AI 算法工程师 1 名", "张三:AI 工程师;李四:数据科学家")
|
||||
assert isinstance(result, list)
|
||||
|
||||
async def test_empty_input(self):
|
||||
"""空输入返回空列表。"""
|
||||
result = await recommend_talent("", "")
|
||||
assert isinstance(result, list)
|
||||
@@ -0,0 +1,126 @@
|
||||
"""T3.13 协作闭环 + 任务管理测试。
|
||||
|
||||
测试通知服务、任务 CRUD 和评论 CRUD。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.services.notification import send_email, send_in_app_notification, send_webhook
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers(client: TestClient):
|
||||
"""注册并登录,返回认证头。"""
|
||||
client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": "task_test@example.com",
|
||||
"password": "password123",
|
||||
"name": "任务测试用户",
|
||||
"tenant_name": "任务测试机构",
|
||||
"role": "investor",
|
||||
},
|
||||
)
|
||||
resp = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": "task_test@example.com", "password": "password123"},
|
||||
)
|
||||
token = resp.json()["data"]["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def company_id(client: TestClient, auth_headers: dict):
|
||||
"""创建测试企业,返回企业 ID。"""
|
||||
resp = client.post(
|
||||
"/api/v1/companies",
|
||||
json={"name": "任务测试公司", "industry": "AI"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
return resp.json()["data"]["id"]
|
||||
|
||||
|
||||
class TestNotificationService:
|
||||
"""通知服务测试。"""
|
||||
|
||||
async def test_send_email(self):
|
||||
"""发送邮件通知。"""
|
||||
result = await send_email("test@example.com", "测试邮件", "邮件内容")
|
||||
assert result is True
|
||||
|
||||
async def test_send_in_app_notification(self):
|
||||
"""发送站内信通知。"""
|
||||
result = await send_in_app_notification("user-1", "测试通知", "通知内容")
|
||||
assert result is True
|
||||
|
||||
async def test_send_webhook(self):
|
||||
"""发送 Webhook 通知。"""
|
||||
result = await send_webhook("https://example.com/webhook", {"event": "test"})
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestTaskCRUD:
|
||||
"""任务 CRUD 测试。"""
|
||||
|
||||
def test_create_task(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""创建任务。"""
|
||||
resp = client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "跟进融资进度", "company_id": company_id, "priority": "high"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_list_tasks(self, client: TestClient, auth_headers: dict):
|
||||
"""查询任务列表。"""
|
||||
resp = client.get("/api/v1/tasks", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert isinstance(resp.json()["data"], list)
|
||||
|
||||
def test_update_task(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""更新任务状态。"""
|
||||
create_resp = client.post(
|
||||
"/api/v1/tasks",
|
||||
json={"title": "待更新任务", "company_id": company_id},
|
||||
headers=auth_headers,
|
||||
)
|
||||
task_id = create_resp.json()["data"]["id"]
|
||||
resp = client.put(
|
||||
f"/api/v1/tasks/{task_id}",
|
||||
json={"status": "done"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["code"] == 0
|
||||
|
||||
def test_no_auth(self, client: TestClient):
|
||||
"""无认证返回 401。"""
|
||||
resp = client.get("/api/v1/tasks")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
class TestCommentCRUD:
|
||||
"""评论 CRUD 测试。"""
|
||||
|
||||
def test_create_and_list_comment(self, client: TestClient, auth_headers: dict, company_id: str):
|
||||
"""创建并查询评论。"""
|
||||
client.post(
|
||||
"/api/v1/comments",
|
||||
json={"target_type": "company", "target_id": company_id, "content": "测试评论"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
resp = client.get(
|
||||
f"/api/v1/comments?target_type=company&target_id={company_id}",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) >= 1
|
||||
assert data[0]["content"] == "测试评论"
|
||||
|
||||
def test_no_auth(self, client: TestClient):
|
||||
"""无认证返回 401。"""
|
||||
resp = client.get("/api/v1/comments?target_type=company&target_id=xxx")
|
||||
assert resp.status_code == 401
|
||||
@@ -0,0 +1,68 @@
|
||||
"""T2.5 弱信号采集与关联测试。
|
||||
|
||||
测试弱信号采集器的四类信号采集和关联引擎的跨维度关联分析。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.signal_correlator import correlate_signals
|
||||
from app.services.weak_signal_collector import collect_weak_signals
|
||||
|
||||
|
||||
class TestCollectWeakSignals:
|
||||
"""弱信号采集测试。"""
|
||||
|
||||
async def test_collect_returns_four_types(self):
|
||||
"""采集返回四类信号。"""
|
||||
signals = await collect_weak_signals("company-1", "测试公司")
|
||||
assert len(signals) == 4
|
||||
types = {s["signal_type"] for s in signals}
|
||||
assert types == {"technical", "sentiment", "org", "market"}
|
||||
|
||||
async def test_signal_has_confidence(self):
|
||||
"""每个信号包含置信度。"""
|
||||
signals = await collect_weak_signals("company-1", "测试公司")
|
||||
for s in signals:
|
||||
assert "confidence" in s
|
||||
assert 0 <= s["confidence"] <= 1
|
||||
|
||||
async def test_signal_has_company_id(self):
|
||||
"""信号包含企业 ID。"""
|
||||
signals = await collect_weak_signals("company-abc", "测试公司")
|
||||
for s in signals:
|
||||
assert s["company_id"] == "company-abc"
|
||||
|
||||
|
||||
class TestCorrelateSignals:
|
||||
"""弱信号关联测试。"""
|
||||
|
||||
async def test_single_signal_no_correlation(self):
|
||||
"""单个信号不产生关联。"""
|
||||
signals = [{"id": "1", "signal_type": "technical", "confidence": 0.8}]
|
||||
result = await correlate_signals(signals)
|
||||
assert result == []
|
||||
|
||||
async def test_cross_dimension_correlation(self):
|
||||
"""跨维度信号产生关联。"""
|
||||
signals = [
|
||||
{"id": "1", "signal_type": "technical", "confidence": 0.7},
|
||||
{"id": "2", "signal_type": "sentiment", "confidence": 0.6},
|
||||
]
|
||||
result = await correlate_signals(signals)
|
||||
assert len(result) == 1
|
||||
assert result[0]["correlation_type"] == "cross_dimension"
|
||||
assert result[0]["risk_probability"] > 0
|
||||
|
||||
async def test_same_type_no_cross_dimension(self):
|
||||
"""同类型信号不产生跨维度关联。"""
|
||||
signals = [
|
||||
{"id": "1", "signal_type": "technical", "confidence": 0.7},
|
||||
{"id": "2", "signal_type": "technical", "confidence": 0.6},
|
||||
]
|
||||
result = await correlate_signals(signals)
|
||||
assert len(result) == 0
|
||||
|
||||
async def test_empty_signals(self):
|
||||
"""空信号列表返回空。"""
|
||||
result = await correlate_signals([])
|
||||
assert result == []
|
||||
Reference in New Issue
Block a user