mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
- Robust ID extraction in CentralityCalculator, CommunityDetector, and ConnectivityAnalyzer - Support for direct Entity objects and dictionaries as node identifiers - Improved Entity hashability in utils/types.py - Added integration test to verify fix and prevent regression
232 lines
9.0 KiB
Python
232 lines
9.0 KiB
Python
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
from typing import Any, Dict, List, Optional
|
|
from semantica.graph_store.graph_store import GraphStore
|
|
|
|
class MockGraphStore:
|
|
def __init__(self, **config):
|
|
self.config = config
|
|
self.nodes = {}
|
|
self.relationships = {}
|
|
self.node_counter = 0
|
|
self.rel_counter = 0
|
|
self.connected = False
|
|
|
|
def connect(self, **options):
|
|
self.connected = True
|
|
return True
|
|
|
|
def close(self):
|
|
self.connected = False
|
|
|
|
def create_node(self, labels: List[str], properties: Dict[str, Any], **options) -> Dict[str, Any]:
|
|
self.node_counter += 1
|
|
node_id = self.node_counter
|
|
node = {
|
|
"id": node_id,
|
|
"labels": labels,
|
|
"properties": properties
|
|
}
|
|
self.nodes[node_id] = node
|
|
return node
|
|
|
|
def create_nodes(self, nodes: List[Dict[str, Any]], **options) -> List[Dict[str, Any]]:
|
|
created = []
|
|
for node_data in nodes:
|
|
created.append(self.create_node(node_data.get("labels", []), node_data.get("properties", {})))
|
|
return created
|
|
|
|
def get_node(self, node_id: int, **options) -> Optional[Dict[str, Any]]:
|
|
return self.nodes.get(node_id)
|
|
|
|
def get_nodes(self, labels: Optional[List[str]] = None, properties: Optional[Dict[str, Any]] = None, limit: int = 100, **options) -> List[Dict[str, Any]]:
|
|
result = []
|
|
for node in self.nodes.values():
|
|
if labels:
|
|
if not any(label in node["labels"] for label in labels):
|
|
continue
|
|
if properties:
|
|
match = True
|
|
for k, v in properties.items():
|
|
if node["properties"].get(k) != v:
|
|
match = False
|
|
break
|
|
if not match:
|
|
continue
|
|
result.append(node)
|
|
if len(result) >= limit:
|
|
break
|
|
return result
|
|
|
|
def update_node(self, node_id: int, properties: Dict[str, Any], merge: bool = True, **options) -> Dict[str, Any]:
|
|
if node_id not in self.nodes:
|
|
raise Exception(f"Node {node_id} not found")
|
|
|
|
if merge:
|
|
self.nodes[node_id]["properties"].update(properties)
|
|
else:
|
|
self.nodes[node_id]["properties"] = properties
|
|
return self.nodes[node_id]
|
|
|
|
def delete_node(self, node_id: int, detach: bool = True, **options) -> bool:
|
|
if node_id in self.nodes:
|
|
del self.nodes[node_id]
|
|
# Handle detach (delete relationships) if needed
|
|
if detach:
|
|
to_delete = []
|
|
for rel_id, rel in self.relationships.items():
|
|
if rel["start_node_id"] == node_id or rel["end_node_id"] == node_id:
|
|
to_delete.append(rel_id)
|
|
for rel_id in to_delete:
|
|
del self.relationships[rel_id]
|
|
return True
|
|
return False
|
|
|
|
def create_relationship(self, start_node_id: int, end_node_id: int, rel_type: str, properties: Optional[Dict[str, Any]] = None, **options) -> Dict[str, Any]:
|
|
if start_node_id not in self.nodes or end_node_id not in self.nodes:
|
|
raise Exception("Nodes not found")
|
|
|
|
self.rel_counter += 1
|
|
rel_id = self.rel_counter
|
|
rel = {
|
|
"id": rel_id,
|
|
"start_node_id": start_node_id,
|
|
"end_node_id": end_node_id,
|
|
"type": rel_type,
|
|
"properties": properties or {}
|
|
}
|
|
self.relationships[rel_id] = rel
|
|
return rel
|
|
|
|
def get_relationships(self, node_id: Optional[int] = None, rel_type: Optional[str] = None, direction: str = "both", limit: int = 100, **options) -> List[Dict[str, Any]]:
|
|
result = []
|
|
for rel in self.relationships.values():
|
|
if node_id is not None:
|
|
if direction == "out" and rel["start_node_id"] != node_id:
|
|
continue
|
|
elif direction == "in" and rel["end_node_id"] != node_id:
|
|
continue
|
|
elif direction == "both" and rel["start_node_id"] != node_id and rel["end_node_id"] != node_id:
|
|
continue
|
|
|
|
if rel_type and rel["type"] != rel_type:
|
|
continue
|
|
|
|
result.append(rel)
|
|
if len(result) >= limit:
|
|
break
|
|
return result
|
|
|
|
def delete_relationship(self, rel_id: int, **options) -> bool:
|
|
if rel_id in self.relationships:
|
|
del self.relationships[rel_id]
|
|
return True
|
|
return False
|
|
|
|
def execute_query(self, query: str, parameters: Optional[Dict[str, Any]] = None, **options) -> Dict[str, Any]:
|
|
return {"records": [], "summary": "Mock query executed"}
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
return {"nodes": len(self.nodes), "relationships": len(self.relationships)}
|
|
|
|
def create_index(self, label: str, property_name: str, index_type: str = "btree", **options) -> bool:
|
|
return True
|
|
|
|
def shortest_path(self, start_node_id: int, end_node_id: int, rel_type: Optional[str] = None, max_depth: int = 10, **options) -> Optional[Dict[str, Any]]:
|
|
return None # Simplified
|
|
|
|
def get_neighbors(self, node_id: int, rel_type: Optional[str] = None, direction: str = "both", depth: int = 1, **options) -> List[Dict[str, Any]]:
|
|
return [] # Simplified
|
|
|
|
class TestGraphStore(unittest.TestCase):
|
|
def setUp(self):
|
|
# Patch Neo4jStore to return our MockGraphStore
|
|
self.patcher = patch('semantica.graph_store.neo4j_store.Neo4jStore', side_effect=MockGraphStore)
|
|
self.mock_store_class = self.patcher.start()
|
|
|
|
# Initialize GraphStore with 'neo4j' backend (which will use our mock)
|
|
self.store = GraphStore(backend="neo4j")
|
|
self.store.connect()
|
|
|
|
def tearDown(self):
|
|
self.store.close()
|
|
self.patcher.stop()
|
|
|
|
def test_node_operations(self):
|
|
# Create
|
|
node = self.store.create_node(labels=["Person"], properties={"name": "Alice", "age": 30})
|
|
self.assertIsNotNone(node)
|
|
self.assertEqual(node["properties"]["name"], "Alice")
|
|
node_id = node["id"]
|
|
|
|
# Get
|
|
fetched_node = self.store.get_node(node_id)
|
|
self.assertEqual(fetched_node["id"], node_id)
|
|
self.assertEqual(fetched_node["properties"]["name"], "Alice")
|
|
|
|
# Get with filters
|
|
nodes = self.store.get_nodes(labels=["Person"], properties={"name": "Alice"})
|
|
self.assertEqual(len(nodes), 1)
|
|
self.assertEqual(nodes[0]["id"], node_id)
|
|
|
|
# Update
|
|
updated_node = self.store.update_node(node_id, properties={"age": 31})
|
|
self.assertEqual(updated_node["properties"]["age"], 31)
|
|
self.assertEqual(updated_node["properties"]["name"], "Alice") # Merge behavior
|
|
|
|
# Delete
|
|
result = self.store.delete_node(node_id)
|
|
self.assertTrue(result)
|
|
self.assertIsNone(self.store.get_node(node_id))
|
|
|
|
def test_relationship_operations(self):
|
|
node1 = self.store.create_node(["Person"], {"name": "Alice"})
|
|
node2 = self.store.create_node(["Person"], {"name": "Bob"})
|
|
|
|
# Create
|
|
rel = self.store.create_relationship(node1["id"], node2["id"], "KNOWS", {"since": 2023})
|
|
self.assertIsNotNone(rel)
|
|
self.assertEqual(rel["type"], "KNOWS")
|
|
rel_id = rel["id"]
|
|
|
|
# Get
|
|
rels = self.store.get_relationships(node_id=node1["id"], direction="out")
|
|
self.assertEqual(len(rels), 1)
|
|
self.assertEqual(rels[0]["id"], rel_id)
|
|
|
|
# Delete
|
|
result = self.store.delete_relationship(rel_id)
|
|
self.assertTrue(result)
|
|
rels = self.store.get_relationships(node_id=node1["id"])
|
|
self.assertEqual(len(rels), 0)
|
|
|
|
def test_batch_node_creation(self):
|
|
nodes_data = [
|
|
{"labels": ["Person"], "properties": {"name": "User1"}},
|
|
{"labels": ["Person"], "properties": {"name": "User2"}}
|
|
]
|
|
created_nodes = self.store.create_nodes(nodes_data)
|
|
self.assertEqual(len(created_nodes), 2)
|
|
self.assertEqual(created_nodes[0]["properties"]["name"], "User1")
|
|
self.assertEqual(created_nodes[1]["properties"]["name"], "User2")
|
|
|
|
def test_query_execution(self):
|
|
# Since MockGraphStore returns a fixed response
|
|
result = self.store.execute_query("MATCH (n) RETURN n")
|
|
self.assertEqual(result["summary"], "Mock query executed")
|
|
|
|
class TestGraphStoreInitialization(unittest.TestCase):
|
|
def test_falkordb_initialization(self):
|
|
with patch('semantica.graph_store.falkordb_store.FalkorDBStore', side_effect=MockGraphStore) as mock_falkor:
|
|
store = GraphStore(backend="falkordb")
|
|
self.assertIsInstance(store._store_backend, MockGraphStore)
|
|
mock_falkor.assert_called_once()
|
|
|
|
def test_invalid_backend(self):
|
|
from semantica.utils.exceptions import ValidationError
|
|
with self.assertRaises(ValidationError):
|
|
GraphStore(backend="invalid_backend")
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|