"""知识图谱穿透集成测试(需 PostgreSQL)。 验证 R2 关键能力:通过关系边的多跳穿透识别"疑似同一实控人", 以及本体约束对非法关系的拒绝。对应场景一(政企拆单+隐性实控人,R8)的图谱基础。 """ from __future__ import annotations import pytest from app.datahub.graph_repo import ( OntologyViolationError, add_relationship, find_related_entities, upsert_entity, ) from app.datahub.ontology import EntityType, RelationshipType def test_upsert_entity_is_idempotent(session): e1 = upsert_entity(session, EntityType.CUSTOMER, "CUST-001", "客户甲") e2 = upsert_entity(session, EntityType.CUSTOMER, "CUST-001", "客户甲") assert e1.id == e2.id def test_ontology_violation_rejected(session): contract = upsert_entity(session, EntityType.CONTRACT, "C-1") customer = upsert_entity(session, EntityType.CUSTOMER, "CUST-2") # 合同 —签约→ 客户 方向非法 with pytest.raises(OntologyViolationError): add_relationship(session, RelationshipType.SIGNED, contract, customer) def test_detect_shared_controller_across_customers(session): """模拟"8 个客户疑似同一实控人":多个客户经法人关联到同一实控自然人。 构图:每个客户 <-法定代表人- 各自法人;各法人 -关联-> 同一实控人。 从实控人出发,应能穿透到全部客户。 """ controller = upsert_entity(session, EntityType.LEGAL_PERSON, "PER-CTRL", "实控人") customers = [] for i in range(8): cust = upsert_entity(session, EntityType.CUSTOMER, f"CUST-{i}", f"政企客户{i}") rep = upsert_entity(session, EntityType.LEGAL_PERSON, f"PER-{i}", f"法人{i}") # 法人 —法定代表人→ 客户 add_relationship(session, RelationshipType.LEGAL_REP_OF, rep, cust) # 法人 —关联(亲属/实控)→ 实控人 add_relationship(session, RelationshipType.RELATED_TO, rep, controller) customers.append(cust) session.flush() related = find_related_entities(session, controller.id, max_depth=3) related_ids = {rid for rid, _ in related} # 从实控人 3 跳内应能穿透到全部 8 个客户 for cust in customers: assert cust.id in related_ids, f"未穿透到 {cust.business_key}" def test_traversal_respects_max_depth(session): a = upsert_entity(session, EntityType.LEGAL_PERSON, "A") b = upsert_entity(session, EntityType.LEGAL_PERSON, "B") c = upsert_entity(session, EntityType.CUSTOMER, "C") add_relationship(session, RelationshipType.RELATED_TO, a, b) add_relationship(session, RelationshipType.LEGAL_REP_OF, b, c) session.flush() # depth=1:从 A 只能到 B,到不了 C ids_d1 = {rid for rid, _ in find_related_entities(session, a.id, max_depth=1)} assert b.id in ids_d1 assert c.id not in ids_d1 # depth=2:能到 C ids_d2 = {rid for rid, _ in find_related_entities(session, a.id, max_depth=2)} assert c.id in ids_d2