zhiyi/tests/test_graph.py

248 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Tests for graph module.
"""
import pytest
import tempfile
from pathlib import Path
from datetime import datetime
import networkx as nx
from src.graph.models import GraphNode, GraphEdge, NodeType, EdgeType
from src.graph.builder import GraphBuilder
from src.graph.query import GraphQuery
from src.graph.indexer import LazyIndexer
from src.models.distilled import Distilled
class TestGraphNode:
def test_create_entity_node(self):
node = GraphNode.from_entity(
entity="牧尘",
distilled_id="test-123",
episode_id="ep-456",
confidence=0.8,
importance=2
)
assert node.type == NodeType.ENTITY
assert node.label == "牧尘"
assert node.distilled_id == "test-123"
def test_create_fact_node(self):
node = GraphNode.from_fact(
fact="牧尘喜欢简洁的回答",
distilled_id="test-123",
episode_id="ep-456",
confidence=0.9
)
assert node.type == NodeType.FACT
assert "简洁" in node.label
def test_to_dict_roundtrip(self):
node = GraphNode.from_entity("测试", "d1", "e1")
d = node.to_dict()
node2 = GraphNode.from_dict(d)
assert node.id == node2.id
assert node.label == node2.label
assert node.type == node2.type
def test_node_importance_assignment(self):
node = GraphNode.from_entity("重要人物", "d1", "e1", importance=3)
assert node.importance == 3
class TestGraphEdge:
def test_create_edge(self):
node_a = GraphNode.from_entity("A", "d1", "e1")
node_b = GraphNode.from_entity("B", "d2", "e2")
edge = GraphEdge.create_relation(
source_id=node_a.id,
target_id=node_b.id,
relation=EdgeType.RELATES_TO,
weight=0.9
)
assert edge.source == node_a.id
assert edge.target == node_b.id
assert edge.weight == 0.9
def test_to_dict_roundtrip(self):
node_a = GraphNode.from_entity("A", "d1", "e1")
node_b = GraphNode.from_entity("B", "d2", "e2")
edge = GraphEdge.create_relation(node_a.id, node_b.id, EdgeType.SIMILAR_TO, weight=0.8)
d = edge.to_dict()
edge2 = GraphEdge.from_dict(d)
assert edge.id == edge2.id
assert edge.relation == edge2.relation
class TestGraphBuilder:
def test_build_graph_from_distilled(self):
"""Test building a graph from a Distilled record."""
distilled = Distilled(
id="distilled-1",
episode_id="ep-1",
type="fact",
summary="测试总结",
facts=[
"牧尘喜欢简洁",
"牧尘偏好Python",
"牧尘用Arch Linux"
],
entities=["牧尘"],
importance=2
)
builder = GraphBuilder()
builder.add_distilled(distilled)
graph = builder.graph
# Should create entity nodes + fact nodes
assert len(graph.nodes) >= 4 # 至少entity "牧尘" + 3 facts
def test_entity_extraction_from_facts(self):
"""Test that entities are properly extracted from facts."""
distilled = Distilled(
id="distilled-2",
episode_id="ep-2",
type="fact",
summary="",
facts=["牧尘使用ComfyUI"],
entities=["牧尘", "ComfyUI"],
importance=0.7
)
builder = GraphBuilder()
builder.add_distilled(distilled)
graph = builder.graph
# Both entity and fact should create nodes
assert len(graph.nodes) >= 2
class TestGraphQuery:
def setup_method(self):
"""Set up a test graph using NetworkX directly."""
# Create a simple graph: A -> B -> C, A -> D
self.graph = nx.DiGraph()
self.node_a = GraphNode.from_entity("A", "d1", "e1")
self.node_b = GraphNode.from_entity("B", "d2", "e2")
self.node_c = GraphNode.from_entity("C", "d3", "e3")
self.node_d = GraphNode.from_entity("D", "d4", "e4")
# Add nodes to graph with data
self.graph.add_node(self.node_a.id, **self.node_a.to_dict())
self.graph.add_node(self.node_b.id, **self.node_b.to_dict())
self.graph.add_node(self.node_c.id, **self.node_c.to_dict())
self.graph.add_node(self.node_d.id, **self.node_d.to_dict())
# A -> B -> C (2 hops), A -> D (1 hop)
self.graph.add_edge(self.node_a.id, self.node_b.id, **GraphEdge.create_relation(
self.node_a.id, self.node_b.id, EdgeType.RELATES_TO, weight=0.9
).to_dict())
self.graph.add_edge(self.node_b.id, self.node_c.id, **GraphEdge.create_relation(
self.node_b.id, self.node_c.id, EdgeType.RELATES_TO, weight=0.9
).to_dict())
self.graph.add_edge(self.node_a.id, self.node_d.id, **GraphEdge.create_relation(
self.node_a.id, self.node_d.id, EdgeType.RELATES_TO, weight=0.8
).to_dict())
def test_bfs_1_hop(self):
"""Test BFS with 1 hop."""
query = GraphQuery(self.graph)
result = query.bfs_n_hops(self.node_a.id, max_hops=1)
# 1 hop should reach B and D (not C which is 2 hops away)
assert 1 in result
found_1hop = result.get(1, set())
assert self.node_b.id in found_1hop
assert self.node_d.id in found_1hop
assert self.node_c.id not in found_1hop
def test_bfs_2_hops(self):
"""Test BFS with 2 hops."""
query = GraphQuery(self.graph)
result = query.bfs_n_hops(self.node_a.id, max_hops=2)
# 2 hops should reach C (through B)
assert 2 in result
found_2hops = result.get(2, set())
assert self.node_c.id in found_2hops
def test_degree_centrality(self):
"""Test degree centrality calculation."""
query = GraphQuery(self.graph)
centrality = query.degree_centrality()
# Graph has 4 nodes, n-1 = 3
# A: out-degree=2, degree=2 → 2/3
# B: out-degree=1, degree=2 → 2/3 (in+out)
# C: out-degree=0, degree=1 → 1/3
# D: out-degree=0, degree=1 → 1/3
assert centrality[self.node_a.id] == 2.0 / 3.0
assert centrality[self.node_b.id] == 2.0 / 3.0
assert centrality[self.node_c.id] == 1.0 / 3.0
assert centrality[self.node_d.id] == 1.0 / 3.0
def test_jaccard_similarity(self):
"""Test Jaccard similarity between nodes."""
# Add shared neighbor for A and B
node_e = GraphNode.from_entity("E", "d5", "e5")
self.graph.add_node(node_e.id, **node_e.to_dict())
# A -> E, B -> E (both connect to E)
self.graph.add_edge(self.node_a.id, node_e.id, **GraphEdge.create_relation(
self.node_a.id, node_e.id, EdgeType.RELATES_TO, weight=0.9
).to_dict())
self.graph.add_edge(self.node_b.id, node_e.id, **GraphEdge.create_relation(
self.node_b.id, node_e.id, EdgeType.RELATES_TO, weight=0.9
).to_dict())
query = GraphQuery(self.graph)
sim = query.jaccard_similarity(self.node_a.id, self.node_b.id)
# A has neighbors {B, D, E}, B has neighbors {C, E}
# Jaccard = |{E}| / |{B, D, E, C}| = 1/4
assert 0 < sim <= 1
class TestLazyIndexer:
def test_indexer_creation(self):
"""Test lazy indexer initialization."""
with tempfile.TemporaryDirectory() as tmpdir:
indexer = LazyIndexer(storage_dir=Path(tmpdir))
assert indexer.storage_dir.exists()
def test_build_and_lookup(self):
"""Test building indexes and looking up."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a simple graph
graph = nx.DiGraph()
node = GraphNode.from_entity("测试节点", "d1", "e1")
graph.add_node(node.id, **node.to_dict())
indexer = LazyIndexer(storage_dir=Path(tmpdir))
indexer.build_indexes(graph)
# Should find the node by label
results = indexer.lookup_by_label("测试节点")
assert len(results) == 1
def test_save_and_load_index(self):
"""Test saving and loading indexes."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a graph
graph = nx.DiGraph()
node = GraphNode.from_entity("测试节点", "d1", "e1")
graph.add_node(node.id, **node.to_dict())
# Build and save
indexer1 = LazyIndexer(storage_dir=Path(tmpdir))
indexer1.build_indexes(graph)
indexer1.save_index()
# Load in new indexer
indexer2 = LazyIndexer(storage_dir=Path(tmpdir))
indexer2.load_index()
results = indexer2.lookup_by_label("测试节点")
assert len(results) == 1