topoteretes--cognee
c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
232 行
7.8 KiB
Python
232 行
7.8 KiB
Python
import logging
|
|
from uuid import UUID, uuid4
|
|
|
|
from cognee.infrastructure.engine.models.DataPoint import DataPoint
|
|
|
|
|
|
class PersonWithIdentity(DataPoint):
|
|
name: str
|
|
metadata: dict = {"index_fields": ["name"], "identity_fields": ["name"]}
|
|
|
|
|
|
class DepartmentWithIdentity(DataPoint):
|
|
name: str
|
|
metadata: dict = {"index_fields": ["name"], "identity_fields": ["name"]}
|
|
|
|
|
|
class MultiFieldIdentity(DataPoint):
|
|
first_name: str
|
|
last_name: str
|
|
metadata: dict = {
|
|
"index_fields": ["first_name"],
|
|
"identity_fields": ["first_name", "last_name"],
|
|
}
|
|
|
|
|
|
class NoIdentityFields(DataPoint):
|
|
name: str
|
|
metadata: dict = {"index_fields": ["name"]}
|
|
|
|
|
|
class PartialIdentity(DataPoint):
|
|
name: str
|
|
age: int = 0
|
|
metadata: dict = {"index_fields": ["name"], "identity_fields": ["name", "age"]}
|
|
|
|
|
|
class TestSameValuesSameUUID:
|
|
def test_same_name_produces_same_id(self):
|
|
p1 = PersonWithIdentity(name="John")
|
|
p2 = PersonWithIdentity(name="John")
|
|
assert p1.id == p2.id
|
|
|
|
def test_deterministic_across_calls(self):
|
|
ids = [PersonWithIdentity(name="Alice").id for _ in range(10)]
|
|
assert len(set(ids)) == 1
|
|
|
|
|
|
class TestDifferentValuesDifferentUUID:
|
|
def test_different_names_produce_different_ids(self):
|
|
p1 = PersonWithIdentity(name="John")
|
|
p2 = PersonWithIdentity(name="Jane")
|
|
assert p1.id != p2.id
|
|
|
|
|
|
class TestCrossTypeSafety:
|
|
def test_same_name_different_class_different_id(self):
|
|
person = PersonWithIdentity(name="Engineering")
|
|
department = DepartmentWithIdentity(name="Engineering")
|
|
assert person.id != department.id
|
|
|
|
|
|
class TestExplicitIdOverride:
|
|
def test_explicit_id_takes_precedence(self):
|
|
explicit_id = uuid4()
|
|
p = PersonWithIdentity(id=explicit_id, name="John")
|
|
assert p.id == explicit_id
|
|
|
|
def test_explicit_id_differs_from_generated(self):
|
|
explicit_id = uuid4()
|
|
p_explicit = PersonWithIdentity(id=explicit_id, name="John")
|
|
p_generated = PersonWithIdentity(name="John")
|
|
assert p_explicit.id != p_generated.id
|
|
|
|
|
|
class TestDefaultFieldsIncludedInIdentity:
|
|
def test_default_field_included_in_identity(self):
|
|
"""Fields with defaults are available via model_dump() and produce deterministic IDs."""
|
|
p1 = PartialIdentity(name="John")
|
|
p2 = PartialIdentity(name="John")
|
|
assert p1.id == p2.id
|
|
assert p1.id.version == 5
|
|
|
|
def test_different_default_override_produces_different_id(self):
|
|
p1 = PartialIdentity(name="John", age=25)
|
|
p2 = PartialIdentity(name="John", age=30)
|
|
assert p1.id != p2.id
|
|
|
|
def test_truly_missing_field(self):
|
|
"""A class where identity_fields references a non-existent field."""
|
|
|
|
class BadIdentity(DataPoint):
|
|
name: str
|
|
metadata: dict = {
|
|
"index_fields": ["name"],
|
|
"identity_fields": ["name", "nonexistent"],
|
|
}
|
|
|
|
b1 = BadIdentity(name="John")
|
|
b2 = BadIdentity(name="John")
|
|
# nonexistent is not in model_dump(), so falls back to UUID4 - different each time
|
|
assert b1.id != b2.id
|
|
|
|
|
|
class TestNoIdentityFieldsBackwardCompat:
|
|
def test_no_identity_fields_produces_random_uuid(self):
|
|
n1 = NoIdentityFields(name="John")
|
|
n2 = NoIdentityFields(name="John")
|
|
assert n1.id != n2.id
|
|
|
|
def test_base_datapoint_no_identity_fields(self):
|
|
dp1 = DataPoint()
|
|
dp2 = DataPoint()
|
|
assert dp1.id != dp2.id
|
|
|
|
|
|
class TestMultiFieldIdentity:
|
|
def test_same_multi_fields_same_id(self):
|
|
m1 = MultiFieldIdentity(first_name="John", last_name="Doe")
|
|
m2 = MultiFieldIdentity(first_name="John", last_name="Doe")
|
|
assert m1.id == m2.id
|
|
|
|
def test_different_multi_fields_different_id(self):
|
|
m1 = MultiFieldIdentity(first_name="John", last_name="Doe")
|
|
m2 = MultiFieldIdentity(first_name="John", last_name="Smith")
|
|
assert m1.id != m2.id
|
|
|
|
def test_field_order_matters(self):
|
|
"""first_name='A', last_name='B' should differ from first_name='B', last_name='A'."""
|
|
m1 = MultiFieldIdentity(first_name="A", last_name="B")
|
|
m2 = MultiFieldIdentity(first_name="B", last_name="A")
|
|
assert m1.id != m2.id
|
|
|
|
|
|
class TestStringNormalization:
|
|
def test_case_insensitive(self):
|
|
p1 = PersonWithIdentity(name="John")
|
|
p2 = PersonWithIdentity(name="JOHN")
|
|
assert p1.id == p2.id
|
|
|
|
def test_spaces_normalized(self):
|
|
p1 = PersonWithIdentity(name="John Doe")
|
|
p2 = PersonWithIdentity(name="John_Doe")
|
|
assert p1.id == p2.id
|
|
|
|
def test_apostrophes_removed(self):
|
|
p1 = PersonWithIdentity(name="O'Brien")
|
|
p2 = PersonWithIdentity(name="OBrien")
|
|
assert p1.id == p2.id
|
|
|
|
|
|
class TestIdIsUUID5:
|
|
def test_generated_id_is_valid_uuid(self):
|
|
p = PersonWithIdentity(name="Test")
|
|
assert isinstance(p.id, UUID)
|
|
# UUID5 has version == 5
|
|
assert p.id.version == 5
|
|
|
|
def test_no_identity_id_is_uuid4(self):
|
|
n = NoIdentityFields(name="Test")
|
|
assert isinstance(n.id, UUID)
|
|
assert n.id.version == 4
|
|
|
|
|
|
class TestInheritanceWarning:
|
|
def test_subclass_dropping_identity_fields_logs_warning(self, caplog):
|
|
"""Subclass that overrides metadata without identity_fields triggers a warning."""
|
|
|
|
class Parent(DataPoint):
|
|
name: str
|
|
metadata: dict = {"index_fields": ["name"], "identity_fields": ["name"]}
|
|
|
|
class Child(Parent):
|
|
name: str
|
|
metadata: dict = {"index_fields": ["name"]} # forgot identity_fields
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
c = Child(name="Test")
|
|
assert "drops identity_fields" in caplog.text
|
|
# Should fall back to UUID4 since no identity_fields on Child
|
|
assert c.id.version == 4
|
|
|
|
|
|
class TestIdForMatchesIdentityFields:
|
|
"""id_for(...) and the identity_fields-derived instance id must be the SAME
|
|
value — they share one implementation (id_for) precisely so they cannot drift.
|
|
"""
|
|
|
|
def test_id_for_matches_single_field_instance(self):
|
|
assert PersonWithIdentity.id_for("John") == PersonWithIdentity(name="John").id
|
|
|
|
def test_id_for_matches_after_normalization(self):
|
|
assert PersonWithIdentity.id_for("John Doe") == PersonWithIdentity(name="John_Doe").id
|
|
|
|
def test_id_for_matches_multi_field_in_declaration_order(self):
|
|
assert (
|
|
MultiFieldIdentity.id_for("John", "Doe")
|
|
== MultiFieldIdentity(first_name="John", last_name="Doe").id
|
|
)
|
|
|
|
def test_id_for_namespaced_by_class(self):
|
|
# Same input, different classes -> different ids (class supplies the namespace).
|
|
assert PersonWithIdentity.id_for("Engineering") != DepartmentWithIdentity.id_for(
|
|
"Engineering"
|
|
)
|
|
|
|
|
|
class TestNormalizationMatchesGenerateNodeId:
|
|
"""The identity normalization must stay byte-for-byte aligned with the legacy
|
|
``generate_node_id`` normalization, otherwise the graph id migration can no
|
|
longer recompute historical ids from a node's stored name.
|
|
"""
|
|
|
|
def test_normalization_equivalence(self):
|
|
from uuid import NAMESPACE_OID, uuid5
|
|
|
|
from cognee.infrastructure.engine.utils.generate_node_id import generate_node_id
|
|
|
|
for raw in ["John", "John Doe", "O'Brien", "New York City", "ALL CAPS", "mixed Case's"]:
|
|
normalized = DataPoint._normalize_identity_value(raw)
|
|
# generate_node_id hashes the bare normalized value (no class prefix).
|
|
assert generate_node_id(raw) == uuid5(NAMESPACE_OID, normalized)
|
|
|
|
|
|
class TestTypeFieldPreserved:
|
|
def test_type_is_class_name(self):
|
|
p = PersonWithIdentity(name="John")
|
|
assert p.type == "PersonWithIdentity"
|
|
|
|
def test_type_not_affected_by_identity_fields(self):
|
|
n = NoIdentityFields(name="John")
|
|
assert n.type == "NoIdentityFields"
|