Files
wehub-resource-sync 6b7e6b44f1
Python Build and Type Check / python-ci (ubuntu-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
gh-pages / build (push) Has been cancelled
Python Publish (pypi) / Upload release to PyPI (push) Has been cancelled
Spellcheck / spellcheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:31 +08:00

302 lines
10 KiB
Python

# Copyright (C) 2026 Microsoft Corporation.
# Licensed under the MIT License
"""Tests for extract_graph merge and orphan-filtering operations.
Validates that _merge_entities, _merge_relationships, and
filter_orphan_relationships correctly aggregate per-text-unit
extraction results and remove relationships whose source or
target has no corresponding entity.
"""
import pandas as pd
from graphrag.index.operations.extract_graph.extract_graph import (
_merge_entities,
_merge_relationships,
)
from graphrag.index.operations.extract_graph.utils import (
filter_orphan_relationships,
)
def _entity_row(
title: str,
entity_type: str = "THING",
description: str = "desc",
source_id: str = "tu1",
) -> dict:
"""Build a single raw entity row as produced by the graph extractor."""
return {
"title": title,
"type": entity_type,
"description": description,
"source_id": source_id,
}
def _relationship_row(
source: str,
target: str,
weight: float = 1.0,
description: str = "desc",
source_id: str = "tu1",
) -> dict:
"""Build a single raw relationship row as produced by the graph extractor."""
return {
"source": source,
"target": target,
"weight": weight,
"description": description,
"source_id": source_id,
}
class TestMergeEntities:
"""Tests for the _merge_entities aggregation helper."""
def test_groups_by_title_and_type(self):
"""Entities with the same title+type merge into one row."""
df1 = pd.DataFrame([_entity_row("A", "PERSON")])
df2 = pd.DataFrame([_entity_row("A", "PERSON", source_id="tu2")])
merged = _merge_entities([df1, df2])
assert len(merged) == 1
assert merged.iloc[0]["title"] == "A"
assert merged.iloc[0]["frequency"] == 2
def test_different_types_stay_separate(self):
"""Same title but different type should not merge."""
df = pd.DataFrame([
_entity_row("A", "PERSON"),
_entity_row("A", "ORG"),
])
merged = _merge_entities([df])
assert len(merged) == 2
def test_empty_input(self):
"""Empty entity list should produce an empty DataFrame."""
df = pd.DataFrame(columns=["title", "type", "description", "source_id"])
merged = _merge_entities([df])
assert len(merged) == 0
class TestMergeRelationships:
"""Tests for the _merge_relationships aggregation helper."""
def test_groups_by_source_target(self):
"""Relationships with same source+target merge and sum weight."""
df1 = pd.DataFrame([_relationship_row("A", "B", weight=2.0)])
df2 = pd.DataFrame([_relationship_row("A", "B", weight=3.0)])
merged = _merge_relationships([df1, df2])
assert len(merged) == 1
assert merged.iloc[0]["weight"] == 5.0
def test_distinct_pairs_stay_separate(self):
"""Different source-target pairs remain separate rows."""
df = pd.DataFrame([
_relationship_row("A", "B"),
_relationship_row("B", "C"),
])
merged = _merge_relationships([df])
assert len(merged) == 2
def test_empty_input(self):
"""Empty relationship list should produce an empty DataFrame."""
df = pd.DataFrame(
columns=["source", "target", "weight", "description", "source_id"]
)
merged = _merge_relationships([df])
assert len(merged) == 0
class TestFilterOrphanRelationships:
"""Tests for orphan relationship filtering.
After LLM graph extraction, relationships may reference entity
names that have no corresponding entity row. These must be
removed before downstream processing.
"""
def test_all_valid_relationships_kept(self):
"""Relationships whose endpoints all exist should be retained."""
entities = pd.DataFrame([
_entity_row("A"),
_entity_row("B"),
_entity_row("C"),
])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("A", "B"),
_relationship_row("B", "C"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 2
def test_removes_relationship_with_missing_source(self):
"""Relationship whose source has no entity entry is dropped."""
entities = pd.DataFrame([_entity_row("B")])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("PHANTOM", "B"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 0
def test_removes_relationship_with_missing_target(self):
"""Relationship whose target has no entity entry is dropped."""
entities = pd.DataFrame([_entity_row("A")])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("A", "PHANTOM"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 0
def test_removes_relationship_with_both_missing(self):
"""Relationship where both endpoints are missing is dropped."""
entities = pd.DataFrame([_entity_row("A")])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("GHOST_1", "GHOST_2"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 0
def test_keeps_valid_drops_orphan_mixed(self):
"""Valid and orphaned relationships coexist; only valid survive."""
entities = pd.DataFrame([
_entity_row("A"),
_entity_row("B"),
])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("A", "B"),
_relationship_row("A", "PHANTOM"),
_relationship_row("PHANTOM", "B"),
_relationship_row("GHOST_1", "GHOST_2"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 1
assert filtered.iloc[0]["source"] == "A"
assert filtered.iloc[0]["target"] == "B"
def test_empty_entities_drops_all_relationships(self):
"""If there are no entities, all relationships are orphaned."""
entities = pd.DataFrame(columns=["title", "type", "description", "source_id"])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("A", "B"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 0
def test_empty_relationships_returns_empty(self):
"""If there are no relationships, result is empty DataFrame."""
entities = pd.DataFrame([_entity_row("A")])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame(
columns=["source", "target", "weight", "description", "source_id"]
)
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 0
def test_preserves_all_columns(self):
"""Filtered DataFrame retains all original columns."""
entities = pd.DataFrame([
_entity_row("A"),
_entity_row("B"),
])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("A", "B", weight=5.0, description="linked"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert set(filtered.columns) == set(merged_rels.columns)
assert filtered.iloc[0]["weight"] == 5.0
assert filtered.iloc[0]["description"] == ["linked"]
def test_multi_text_unit_orphan(self):
"""Orphan detected across multiple text units after merge."""
df1 = pd.DataFrame([
_entity_row("A", source_id="tu1"),
_relationship_row("A", "HALLUCINATED", source_id="tu1"),
])
df2 = pd.DataFrame([
_entity_row("A", source_id="tu2"),
_relationship_row("A", "HALLUCINATED", source_id="tu2"),
])
entity_dfs = [
df1[["title", "type", "description", "source_id"]],
df2[["title", "type", "description", "source_id"]],
]
rel_dfs = [
df1[["source", "target", "weight", "description", "source_id"]],
df2[["source", "target", "weight", "description", "source_id"]],
]
merged_entities = _merge_entities(entity_dfs)
merged_rels = _merge_relationships(rel_dfs)
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert len(filtered) == 0
def test_resets_index_after_filter(self):
"""Filtered DataFrame should have a clean 0-based index."""
entities = pd.DataFrame([
_entity_row("A"),
_entity_row("B"),
_entity_row("C"),
])
merged_entities = _merge_entities([entities])
relationships = pd.DataFrame([
_relationship_row("PHANTOM", "B"),
_relationship_row("A", "B"),
_relationship_row("A", "PHANTOM"),
_relationship_row("B", "C"),
])
merged_rels = _merge_relationships([relationships])
filtered = filter_orphan_relationships(merged_rels, merged_entities)
assert list(filtered.index) == list(range(len(filtered)))