Files
topoteretes--cognee/cognee/tests/test_shared_node_preservation.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

498 lines
19 KiB
Python

"""
CRITICAL Test: Shared Node Preservation and Bug Fixes
Tests for critical bugs in the delete feature:
1. Shared node preservation - shared entities should not be deleted when one document is removed
2. Permission checking - "delete" permission should be checked, not "read"
3. File cleanup - files should be removed from storage when dataset is deleted
Test Coverage:
- test_shared_entity_preserved_across_documents: Shared node (Germany) preserved when one doc deleted
- test_delete_permission_checks_delete_not_read: Verify "delete" permission required
- test_dataset_deletion_removes_files: Verify files cleaned from storage
"""
import os
import pathlib
import pytest
from uuid import UUID, uuid4, NAMESPACE_OID, uuid5
from pydantic import BaseModel
from unittest.mock import AsyncMock, patch
import cognee
from cognee.api.v1.datasets import datasets
from cognee.context_global_variables import set_database_global_context_variables
from cognee.infrastructure.databases.graph import get_graph_engine
from cognee.infrastructure.databases.vector import get_vector_engine_async
from cognee.infrastructure.engine import DataPoint
from cognee.infrastructure.llm import LLMGateway
from cognee.modules.chunking.models.DocumentChunk import DocumentChunk
from cognee.modules.data.exceptions.exceptions import UnauthorizedDataAccessError
from cognee.modules.data.methods import create_authorized_dataset
from cognee.modules.data.processing.document_types.TextDocument import TextDocument
from cognee.modules.engine.models import Entity
from cognee.modules.engine.operations.setup import setup
from cognee.modules.users.exceptions import PermissionDeniedError
from cognee.modules.users.methods import create_user, get_default_user
from cognee.modules.users.models import User
from cognee.modules.users.permissions.methods import authorized_give_permission_on_datasets
from cognee.shared.data_models import KnowledgeGraph, Node, Edge, SummarizedContent
from cognee.shared.logging_utils import get_logger
from cognee.tests.utils.assert_graph_nodes_present import assert_graph_nodes_present
from cognee.tests.utils.assert_graph_nodes_not_present import assert_graph_nodes_not_present
from cognee.tests.utils.extract_entities import extract_entities
from cognee.tests.utils.extract_summary import extract_summary
logger = get_logger()
@pytest.mark.asyncio
@patch.object(LLMGateway, "acreate_structured_output", new_callable=AsyncMock)
async def test_shared_entity_preserved_across_documents(mock_create_structured_output: AsyncMock):
"""
Test that shared entities remain when one document referencing them is deleted.
Setup:
- Document 1: "BMW is a german car manufacturer" (creates BMW, Germany entities)
- Document 2: "Germany is located next to the Netherlands" (creates Germany, Netherlands)
- Germany is SHARED between both documents
Operation:
- Delete Document 2
Expected:
- Germany node: REMAINS (shared between doc1 and doc2)
- Netherlands node: DELETED (only in doc2)
- BMW node: REMAINS (only in doc1)
- BMW→Germany edge: REMAINS
- Germany→Netherlands edge: DELETED
"""
import os
data_directory_path = os.path.join(
pathlib.Path(__file__).parent, ".data_storage/test_shared_node_preservation"
)
cognee.config.data_root_directory(data_directory_path)
cognee_directory_path = os.path.join(
pathlib.Path(__file__).parent, ".cognee_system/test_shared_node_preservation"
)
cognee.config.system_root_directory(cognee_directory_path)
await cognee.prune.prune_data()
await cognee.prune.prune_system(metadata=True)
await setup()
def mock_llm_output(text_input: str, system_prompt: str, response_model):
if text_input == "test": # LLM connection test
return "test"
if "BMW" in text_input and response_model == SummarizedContent:
return SummarizedContent(
summary="BMW is a German car manufacturer.",
description="BMW is a German car manufacturer.",
)
if "Netherlands" in text_input and response_model == SummarizedContent:
return SummarizedContent(
summary="Germany is next to the Netherlands.",
description="Germany is next to the Netherlands.",
)
if "BMW" in text_input and response_model == KnowledgeGraph:
return KnowledgeGraph(
nodes=[
Node(
id="BMW",
name="BMW",
type="Company",
description="BMW is a German car manufacturer",
),
Node(
id="Germany",
name="Germany",
type="Country",
description="Germany is a country",
),
],
edges=[
Edge(
source_node_id="BMW",
target_node_id="Germany",
relationship_name="produced_in",
),
],
)
if "Netherlands" in text_input and response_model == KnowledgeGraph:
return KnowledgeGraph(
nodes=[
Node(
id="Germany",
name="Germany",
type="Country",
description="Germany is a country",
),
Node(
id="Netherlands",
name="Netherlands",
type="Country",
description="Netherlands is a country",
),
],
edges=[
Edge(
source_node_id="Germany",
target_node_id="Netherlands",
relationship_name="located_next_to",
),
],
)
mock_create_structured_output.side_effect = mock_llm_output
user = await get_default_user()
await set_database_global_context_variables("main_dataset", user.id)
# Add and cognify first document (BMW)
bmw_text = "BMW is a german car manufacturer"
add_bmw_result = await cognee.add(bmw_text)
bmw_data_id = add_bmw_result.data_ingestion_info[0]["data_id"]
# Add and cognify second document (Netherlands)
netherlands_text = "Germany is located next to the Netherlands"
add_netherlands_result = await cognee.add(netherlands_text)
netherlands_data_id = add_netherlands_result.data_ingestion_info[0]["data_id"]
# Cognify both documents
cognify_result: dict = await cognee.cognify()
dataset_id = list(cognify_result.keys())[0]
# Extract expected entities
bmw_document = TextDocument(
id=bmw_data_id,
name="BMW Document",
raw_data_location="bmw_location",
external_metadata="",
)
bmw_chunk = DocumentChunk(
id=uuid5(NAMESPACE_OID, f"{str(bmw_data_id)}-0"),
text=bmw_text,
chunk_size=14,
chunk_index=0,
cut_type="sentence_end",
is_part_of=bmw_document,
)
extract_summary(bmw_chunk, mock_llm_output("BMW", "", SummarizedContent))
netherlands_document = TextDocument(
id=netherlands_data_id,
name="Netherlands Document",
raw_data_location="netherlands_location",
external_metadata="",
)
netherlands_chunk = DocumentChunk(
id=uuid5(NAMESPACE_OID, f"{str(netherlands_data_id)}-0"),
text=netherlands_text,
chunk_size=14,
chunk_index=0,
cut_type="sentence_end",
is_part_of=netherlands_document,
)
extract_summary(netherlands_chunk, mock_llm_output("Netherlands", "", SummarizedContent))
# Extract entities from both documents
bmw_kg = mock_llm_output("BMW", "", KnowledgeGraph)
netherlands_kg = mock_llm_output("Netherlands", "", KnowledgeGraph)
bmw_entities = extract_entities(bmw_kg)
netherlands_entities = extract_entities(netherlands_kg)
# Find the shared Germany entity
[e for e in bmw_entities if e.name == "Germany"][0]
[e for e in netherlands_entities if e.name == "Germany"][0]
[e for e in bmw_entities if e.name == "BMW"][0]
[e for e in netherlands_entities if e.name == "Netherlands"][0]
# Verify both documents created nodes in the graph
graph_engine = await get_graph_engine()
nodes_before, edges_before = await graph_engine.get_graph_data()
logger.info(f"Before deletion: {len(nodes_before)} nodes, {len(edges_before)} edges")
# Verify Germany exists (shared node)
germany_nodes = [n for n in nodes_before if "Germany" in str(n)]
assert len(germany_nodes) > 0, "Germany node should exist before deletion"
logger.info(f"Found {len(germany_nodes)} Germany nodes before deletion")
# Delete the Netherlands document
logger.info(f"Deleting Netherlands document (data_id={netherlands_data_id})...")
await datasets.delete_data(dataset_id, netherlands_data_id, user)
# Check graph after deletion
nodes_after, edges_after = await graph_engine.get_graph_data()
logger.info(f"After deletion: {len(nodes_after)} nodes, {len(edges_after)} edges")
# CRITICAL ASSERTION: Germany node should still exist (shared with BMW document)
germany_nodes_after = [n for n in nodes_after if "Germany" in str(n)]
assert len(germany_nodes_after) > 0, (
"CRITICAL BUG: Germany node was deleted but it should remain (shared with BMW document)"
)
logger.info(f"✅ Germany node preserved: {len(germany_nodes_after)} nodes found")
# Verify Netherlands node is deleted
netherlands_nodes_after = [n for n in nodes_after if "Netherlands" in str(n)]
assert len(netherlands_nodes_after) == 0, "Netherlands node should be deleted"
logger.info("✅ Netherlands node deleted as expected")
# Verify BMW node still exists
bmw_nodes_after = [n for n in nodes_after if "BMW" in str(n)]
assert len(bmw_nodes_after) > 0, "BMW node should still exist"
logger.info("✅ BMW node preserved")
# Verify BMW→Germany edge still exists
germany_edges = [e for e in edges_after if "Germany" in str(e) and "BMW" in str(e)]
assert len(germany_edges) > 0, "BMW→Germany edge should still exist"
logger.info("✅ BMW→Germany edge preserved")
# Verify Germany→Netherlands edge is deleted
netherlands_edges = [e for e in edges_after if "Netherlands" in str(e)]
assert len(netherlands_edges) == 0, "Germany→Netherlands edge should be deleted"
logger.info("✅ Germany→Netherlands edge deleted")
# Verify vector indices match graph state
vector_engine = await get_vector_engine_async()
# Check if Germany is still in vector index
if await vector_engine.has_collection("Entity_name"):
# Note: We can't easily query by entity name in vector index,
# but we can verify the collection still exists and has items
logger.info("✅ Entity vector collection still exists")
logger.info("✅ test_shared_entity_preserved_across_documents PASSED")
@pytest.mark.asyncio
async def test_delete_permission_checks_delete_not_read():
"""
Test that delete operations check "delete" permission, not "read" permission.
Setup:
- Create user C
- Create dataset owned by other user
- Grant user C only "read" permission (NOT delete)
- Add data to dataset
Operation:
- Attempt to delete data with user C (who has only read permission)
Expected:
- Delete operation FAILS with PermissionDeniedError or UnauthorizedDataAccessError
- Data remains in database
- Graph nodes remain
"""
os.environ["ENABLE_BACKEND_ACCESS_CONTROL"] = "True"
data_directory_path = os.path.join(
pathlib.Path(__file__).parent, ".data_storage/test_delete_permission_check"
)
cognee.config.data_root_directory(data_directory_path)
cognee_directory_path = os.path.join(
pathlib.Path(__file__).parent, ".cognee_system/test_delete_permission_check"
)
cognee.config.system_root_directory(cognee_directory_path)
await cognee.prune.prune_data()
await cognee.prune.prune_system(metadata=True)
await setup()
# Create test data model
class Organization(DataPoint):
name: str
metadata: dict = {"index_fields": ["name"]}
class CustomData(BaseModel):
id: UUID
# Create users
user_c: User = await create_user(email="user_c@test.com", password="password123")
owner_user: User = await create_user(email="owner_delete@test.com", password="password456")
# Create dataset
dataset_y = await create_authorized_dataset(dataset_name="dataset_y", user=owner_user)
# Grant user_c only READ permission (NOT delete)
await authorized_give_permission_on_datasets(user_c.id, [dataset_y.id], "read", owner_user.id)
# Add data to dataset
data_y = CustomData(id=uuid4())
org_y = Organization(name="Test Organization Y")
await set_database_global_context_variables(dataset_y.id, dataset_y.owner_id)
from cognee.tasks.storage import add_data_points
from cognee.modules.pipelines.models import PipelineContext
await add_data_points(
[org_y],
ctx=PipelineContext(user=owner_user, dataset=dataset_y, data_item=data_y),
)
# Verify data exists
graph_engine = await get_graph_engine()
nodes_before, _ = await graph_engine.get_graph_data()
assert len(nodes_before) > 0, "Should have nodes before delete attempt"
# Attempt to delete with only read permission
delete_succeeded = False
delete_error = None
try:
await datasets.delete_data(dataset_y.id, data_y.id, user=user_c)
delete_succeeded = True
except (PermissionDeniedError, UnauthorizedDataAccessError) as e:
delete_error = e
logger.info(f"✅ Delete correctly blocked: {type(e).__name__}")
# CRITICAL ASSERTION: Delete should have failed
assert not delete_succeeded, (
"CRITICAL BUG: Delete succeeded with only read permission! "
"The system should check 'delete' permission, not 'read'."
)
assert delete_error is not None, (
"Should have raised PermissionDeniedError or UnauthorizedDataAccessError"
)
# Verify data still exists
nodes_after, _ = await graph_engine.get_graph_data()
assert len(nodes_after) == len(nodes_before), (
"No nodes should be deleted when permission is denied"
)
# Verify we can still read the data (read permission works)
dataset_data = await datasets.list_data(dataset_y.id, user=user_c)
assert len(dataset_data) > 0, "Should still be able to read data with read permission"
logger.info("✅ test_delete_permission_checks_delete_not_read PASSED")
@pytest.mark.asyncio
async def test_dataset_deletion_removes_files():
"""
Test that deleting a dataset removes files from storage.
Setup:
- Create dataset
- Add files that will be stored
- Verify files exist in storage
Operation:
- Delete entire dataset
Expected:
- Dataset record: DELETED from database
- Data records: DELETED from database
- Graph nodes: DELETED
- FILES: DELETED from storage (THIS IS THE BUG)
"""
data_directory_path = os.path.join(
pathlib.Path(__file__).parent, ".data_storage/test_file_cleanup"
)
cognee.config.data_root_directory(data_directory_path)
cognee_directory_path = os.path.join(
pathlib.Path(__file__).parent, ".cognee_system/test_file_cleanup"
)
cognee.config.system_root_directory(cognee_directory_path)
await cognee.prune.prune_data()
await cognee.prune.prune_system(metadata=True)
await setup()
user = await get_default_user()
# Create dataset and add text data
dataset_name = "dataset_z"
text_1 = "This is the first document about technology."
text_2 = "This is the second document about science."
add_result_1 = await cognee.add([text_1], dataset_name=dataset_name, user=user)
data_1_id = add_result_1.data_ingestion_info[0]["data_id"]
add_result_2 = await cognee.add([text_2], dataset_name=dataset_name, user=user)
data_2_id = add_result_2.data_ingestion_info[0]["data_id"]
# Cognify to create graph
cognify_result = await cognee.cognify([dataset_name], user=user)
dataset_id = list(cognify_result.keys())[0]
# Check that files/data exist in storage
from cognee.modules.data.methods import get_data
data_1 = await get_data(data_1_id)
data_2 = await get_data(data_2_id)
assert data_1 is not None, "Data 1 should exist"
assert data_2 is not None, "Data 2 should exist"
# Note: For text data, files might not be stored the same way as uploaded files
# But we can still verify the data records exist
logger.info(f"Data 1 location: {data_1.raw_data_location}")
logger.info(f"Data 2 location: {data_2.raw_data_location}")
# Verify graph nodes exist
graph_engine = await get_graph_engine()
nodes_before, _ = await graph_engine.get_graph_data()
assert len(nodes_before) > 0, "Should have nodes before deletion"
logger.info(f"Graph has {len(nodes_before)} nodes before deletion")
# Delete the entire dataset
logger.info(f"Deleting dataset {dataset_id}...")
await datasets.empty_dataset(dataset_id, user=user)
# Verify dataset is deleted
from cognee.modules.data.methods import get_dataset
try:
deleted_dataset = await get_dataset(dataset_id)
assert deleted_dataset is None, "Dataset should be deleted"
except Exception as e:
logger.info(f"✅ Dataset correctly deleted: {type(e).__name__}")
# Verify data records are deleted
data_1_after = await get_data(data_1_id)
data_2_after = await get_data(data_2_id)
assert data_1_after is None, "Data 1 should be deleted"
assert data_2_after is None, "Data 2 should be deleted"
logger.info("✅ Data records deleted")
# Verify graph nodes are deleted
nodes_after, _ = await graph_engine.get_graph_data()
assert len(nodes_after) == 0, "All nodes should be deleted"
logger.info("✅ Graph nodes deleted")
# CRITICAL: Verify files are deleted from storage
# For text data added via cognee.add(), the files are stored in the data directory
# Check that the data directory for this dataset is cleaned up
dataset_dir = os.path.join(data_directory_path, str(dataset_id))
# Note: The exact storage structure may vary, but we should verify
# that there are no orphaned files
if os.path.exists(dataset_dir):
files_in_dir = os.listdir(dataset_dir)
logger.warning(
f"POTENTIAL BUG: Dataset directory still exists with {len(files_in_dir)} files"
)
# This is informational - the exact cleanup behavior may depend on storage engine
else:
logger.info("✅ Dataset directory cleaned up")
logger.info("✅ test_dataset_deletion_removes_files PASSED")
if __name__ == "__main__":
import asyncio
asyncio.run(test_shared_entity_preserved_across_documents())
asyncio.run(test_delete_permission_checks_delete_not_read())
asyncio.run(test_dataset_deletion_removes_files())