Files
wehub-resource-sync 2860fb5d18
Security / Dependency review (push) Has been skipped
Scorecard / Scorecard analysis (push) Failing after 0s
Validate / eval (push) Failing after 0s
Security / Dependency audit (push) Failing after 1s
Security / Secret scan (push) Failing after 1s
Validate / tests (push) Failing after 0s
Validate / mcp-tests (push) Failing after 1s
GitHub Actions Security Analysis with zizmor 🌈 / zizmor (push) Failing after 1s
Security / SAST scan (push) Failing after 13m52s
chore: import upstream snapshot with attribution
2026-07-13 12:05:33 +08:00

213 lines
7.4 KiB
Python

"""Unit tests for dedupe.py: text normalization, similarity metrics, and deduplication."""
import unittest
from lib import dedupe
from lib.schema import SourceItem
def _item(title: str, body: str = "", source: str = "reddit", item_id: str = "t1") -> SourceItem:
return SourceItem(
item_id=item_id, source=source, title=title, body=body,
url="https://example.com", engagement={}, metadata={},
)
# ---------------------------------------------------------------------------
# normalize_text
# ---------------------------------------------------------------------------
class TestNormalizeText(unittest.TestCase):
def test_lowercases(self):
self.assertEqual(dedupe.normalize_text("Hello World"), "hello world")
def test_strips_punctuation(self):
self.assertEqual(dedupe.normalize_text("it's a test!"), "it s a test")
def test_collapses_whitespace(self):
self.assertEqual(dedupe.normalize_text("a b\t\nc"), "a b c")
def test_empty_string(self):
self.assertEqual(dedupe.normalize_text(""), "")
# ---------------------------------------------------------------------------
# get_ngrams
# ---------------------------------------------------------------------------
class TestGetNgrams(unittest.TestCase):
def test_simple_trigrams(self):
ngrams = dedupe.get_ngrams("abcde")
self.assertEqual(ngrams, {"abc", "bcd", "cde"})
def test_short_text_returns_whole(self):
ngrams = dedupe.get_ngrams("ab")
self.assertEqual(ngrams, {"ab"})
def test_empty_returns_empty_set(self):
self.assertEqual(dedupe.get_ngrams(""), set())
def test_normalizes_before_ngrams(self):
# "A!B" -> "a b" after normalization -> {"a b"}
ngrams = dedupe.get_ngrams("A!B")
self.assertEqual(ngrams, {"a b"})
# ---------------------------------------------------------------------------
# jaccard_similarity
# ---------------------------------------------------------------------------
class TestJaccardSimilarity(unittest.TestCase):
def test_identical_sets(self):
self.assertAlmostEqual(dedupe.jaccard_similarity({"a", "b"}, {"a", "b"}), 1.0)
def test_disjoint_sets(self):
self.assertAlmostEqual(dedupe.jaccard_similarity({"a"}, {"b"}), 0.0)
def test_partial_overlap(self):
result = dedupe.jaccard_similarity({"a", "b", "c"}, {"b", "c", "d"})
self.assertAlmostEqual(result, 2.0 / 4.0)
def test_empty_left(self):
self.assertAlmostEqual(dedupe.jaccard_similarity(set(), {"a"}), 0.0)
def test_both_empty(self):
self.assertAlmostEqual(dedupe.jaccard_similarity(set(), set()), 0.0)
# ---------------------------------------------------------------------------
# token_jaccard
# ---------------------------------------------------------------------------
class TestTokenJaccard(unittest.TestCase):
def test_identical_texts(self):
self.assertAlmostEqual(dedupe.token_jaccard("hello world", "hello world"), 1.0)
def test_completely_different(self):
self.assertAlmostEqual(dedupe.token_jaccard("alpha beta", "gamma delta"), 0.0)
def test_filters_stopwords(self):
# "the" and "a" are stopwords, so "big cat" vs "big dog" should compare on {big, cat} vs {big, dog}
result = dedupe.token_jaccard("the big cat", "a big dog")
self.assertAlmostEqual(result, 1.0 / 3.0) # {big} / {big, cat, dog}
def test_filters_single_char_tokens(self):
# Single char tokens like "I" are filtered (len > 1)
result = dedupe.token_jaccard("I am great", "I am terrible")
# "am" is len 2, "great"/"terrible" are content
self.assertGreater(result, 0.0)
# ---------------------------------------------------------------------------
# hybrid_similarity
# ---------------------------------------------------------------------------
class TestHybridSimilarity(unittest.TestCase):
def test_identical_texts(self):
self.assertAlmostEqual(dedupe.hybrid_similarity("same text", "same text"), 1.0)
def test_completely_different(self):
result = dedupe.hybrid_similarity("aaaaaa", "zzzzzz")
self.assertLess(result, 0.1)
def test_takes_max_of_both_methods(self):
text_a = "OpenClaw security issues discussion"
text_b = "OpenClaw security issues thread"
ngram_sim = dedupe.jaccard_similarity(
dedupe.get_ngrams(text_a), dedupe.get_ngrams(text_b)
)
token_sim = dedupe.token_jaccard(text_a, text_b)
self.assertAlmostEqual(
dedupe.hybrid_similarity(text_a, text_b),
max(ngram_sim, token_sim),
)
# ---------------------------------------------------------------------------
# item_text
# ---------------------------------------------------------------------------
class TestItemText(unittest.TestCase):
def test_combines_fields(self):
item = _item("My Title", "My Body")
text = dedupe.item_text(item)
self.assertIn("My Title", text)
self.assertIn("My Body", text)
def test_skips_none_fields(self):
item = _item("Title", "")
item.author = None
item.container = None
text = dedupe.item_text(item)
self.assertEqual(text, "Title")
def test_includes_author_and_container(self):
item = _item("Title", "Body")
item.author = "john"
item.container = "r/python"
text = dedupe.item_text(item)
self.assertIn("john", text)
self.assertIn("r/python", text)
# ---------------------------------------------------------------------------
# dedupe_items
# ---------------------------------------------------------------------------
class TestDedupeItems(unittest.TestCase):
def test_keeps_unique_items(self):
items = [
_item("OpenClaw is amazing", item_id="a"),
_item("NanoClaw security review", item_id="b"),
_item("IronClaw Rust architecture", item_id="c"),
]
result = dedupe.dedupe_items(items)
self.assertEqual(len(result), 3)
def test_removes_near_duplicates(self):
items = [
_item("OpenClaw vs NanoClaw comparison review", item_id="a"),
_item("OpenClaw vs NanoClaw comparison review thread", item_id="b"),
]
result = dedupe.dedupe_items(items)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].item_id, "a") # keeps first
def test_keeps_first_of_duplicates(self):
items = [
_item("Same title here", item_id="first"),
_item("Same title here", item_id="second"),
]
result = dedupe.dedupe_items(items)
self.assertEqual(result[0].item_id, "first")
def test_empty_body_items_kept(self):
item = SourceItem(
item_id="empty", source="reddit", title="", body="",
url="", engagement={}, metadata={},
)
result = dedupe.dedupe_items([item])
self.assertEqual(len(result), 1)
def test_threshold_respected(self):
items = [
_item("OpenClaw security analysis", item_id="a"),
_item("OpenClaw security review", item_id="b"),
]
# With threshold=1.0, only exact matches are removed
result = dedupe.dedupe_items(items, threshold=1.0)
self.assertEqual(len(result), 2)
# With threshold=0.3, these similar items collapse
result_loose = dedupe.dedupe_items(items, threshold=0.3)
self.assertEqual(len(result_loose), 1)
if __name__ == "__main__":
unittest.main()