yusufcalisir's picture
deploy: Hugging Face space upload
73ba4f5
Raw History Blame Contribute Delete
4.64 kB
"""Tests for the graph engine."""
import pytest
from app.application.services.graph_engine import GraphEngine
from app.domain.enums import EntityType, RelationshipType
from app.domain.investigation_entities import Entity, Relationship
def _make_entity(eid: str, etype: EntityType = EntityType.CUSTOMER, bank: str = "bank_a") -> Entity:
return Entity(
id=eid,
entity_type=etype,
privacy_id=f"hash_{eid}",
bank_id=bank,
display_label=f"ENT-{eid[:4]}",
)
def _make_relationship(
rid: str,
src: str,
tgt: str,
rtype: RelationshipType = RelationshipType.TRANSACTS_WITH,
) -> Relationship:
return Relationship(
id=rid,
source_entity_id=src,
target_entity_id=tgt,
relationship_type=rtype,
)
@pytest.fixture
def graph() -> GraphEngine:
g = GraphEngine()
# Build a small graph: A -- B -- C -- D, plus A -- E
entities = [
_make_entity("A"),
_make_entity("B", EntityType.MERCHANT),
_make_entity("C", EntityType.DEVICE),
_make_entity("D"),
_make_entity("E", EntityType.CARD),
]
g.register_entities(entities)
g.add_relationship(_make_relationship("r1", "A", "B"))
g.add_relationship(_make_relationship("r2", "B", "C"))
g.add_relationship(_make_relationship("r3", "C", "D"))
g.add_relationship(_make_relationship("r4", "A", "E"))
return g
class TestNeighborTraversal:
def test_direct_neighbors(self, graph: GraphEngine) -> None:
neighbors = graph.find_neighbors("A", depth=1)
neighbor_ids = {n.id for n in neighbors}
assert neighbor_ids == {"B", "E"}
def test_depth_2(self, graph: GraphEngine) -> None:
neighbors = graph.find_neighbors("A", depth=2)
neighbor_ids = {n.id for n in neighbors}
assert neighbor_ids == {"B", "E", "C"}
def test_depth_3(self, graph: GraphEngine) -> None:
neighbors = graph.find_neighbors("A", depth=3)
neighbor_ids = {n.id for n in neighbors}
assert neighbor_ids == {"B", "E", "C", "D"}
def test_type_filter(self, graph: GraphEngine) -> None:
neighbors = graph.find_neighbors(
"B",
depth=2,
relationship_types={RelationshipType.TRANSACTS_WITH},
)
# All relationships in fixture are TRANSACTS_WITH, so should find all
assert len(neighbors) > 0
class TestClusterDetection:
def test_single_cluster(self, graph: GraphEngine) -> None:
# All nodes are connected — one cluster of size 5
clusters = graph.detect_clusters(min_size=3)
assert len(clusters) == 1
assert len(clusters[0]) == 5
def test_min_size_filter(self, graph: GraphEngine) -> None:
clusters = graph.detect_clusters(min_size=6)
assert len(clusters) == 0
def test_disconnected_clusters(self) -> None:
g = GraphEngine()
# Two disconnected components: {A, B, C} and {D, E, F}
for eid in ["A", "B", "C", "D", "E", "F"]:
g.register_entity(_make_entity(eid))
g.add_relationship(_make_relationship("r1", "A", "B"))
g.add_relationship(_make_relationship("r2", "B", "C"))
g.add_relationship(_make_relationship("r3", "D", "E"))
g.add_relationship(_make_relationship("r4", "E", "F"))
clusters = g.detect_clusters(min_size=3)
assert len(clusters) == 2
class TestSubgraph:
def test_subgraph_generation(self, graph: GraphEngine) -> None:
subgraph = graph.get_subgraph("A", radius=2)
assert len(subgraph.nodes) > 0
assert len(subgraph.edges) > 0
assert subgraph.center_entity_id == "A"
def test_subgraph_react_flow_format(self, graph: GraphEngine) -> None:
subgraph = graph.get_subgraph("A", radius=1)
# Check React Flow node format
for node in subgraph.nodes:
assert "id" in node
assert "position" in node
assert "data" in node
assert "x" in node["position"]
assert "y" in node["position"]
def test_nonexistent_entity(self, graph: GraphEngine) -> None:
subgraph = graph.get_subgraph("NONEXISTENT", radius=2)
assert len(subgraph.nodes) == 0
class TestSearchAndStats:
def test_search_by_label(self, graph: GraphEngine) -> None:
results = graph.search_nodes("ENT-A")
assert len(results) >= 1
def test_stats(self, graph: GraphEngine) -> None:
stats = graph.get_stats()
assert stats["total_nodes"] == 5
assert stats["total_edges"] == 4
assert "customer" in stats["nodes_by_type"]