add prototype/test_cluster.py

This commit is contained in:
2026-09-07 17:45:34 +00:00
parent bfa3566882
commit 5fc41a0370
+119
View File
@@ -0,0 +1,119 @@
"""Tests for the story-layer clustering.
Acceptance (per plan): two reworded copies of the same story -> ONE cluster.
Also: exact-duplicate fast path, and distinct stories stay separate.
"""
import time
import pytest
from cluster import Clusterer, Item
def _item(title, url, source="src", summary="body", published=None):
return Item(guid=url, url=url, title=title, summary=summary,
source=source, published=published or time.time())
def _clustered(items):
return Clusterer().cluster(items)
def test_two_rewrites_merge_into_one_cluster():
"""The core accept: BBC + NPR reworded coverage of the same crash -> 1 story.
Wire-syndication pattern: divergent lede/headline, but the body largely
re-uses the same wire text (4 shared sentences).
"""
core = ("The two crew members on board were killed. "
"Investigators said the plane lost contact with the tower shortly "
"before impact. Authorities have opened an inquiry into the cause. "
"Air traffic around Miami Beach was halted for hours after the crash.")
items = [
_item("Cargo Plane Crashes Near Miami Beach Killing Two People",
"https://bbc.example/cargo-crash",
source="BBC",
summary="A cargo aircraft went down close to Miami Beach on Tuesday "
"morning. " + core),
_item("Two dead after cargo jet crashes outside Miami",
"https://npr.example/miami-crash",
source="NPR",
summary="Emergency crews responded to the wreckage near Miami Beach. "
+ core),
]
stories = _clustered(items)
assert len(stories) == 1, f"expected 1 cluster, got {len(stories)}"
assert stories[0].coverage_count == 2
def test_exact_duplicate_title_dedupes_without_similarity():
items = [
_item("OpenAI Sued Over Training Data", "https://tc.example/openai-suit",
source="TechCrunch"),
_item("OpenAI Sued Over Training Data", "https://verge.example/openai-suit-2",
source="The Verge"),
_item("OpenAI Sued Over Training Data", "https://mirror.example/openai-suit-3",
source="Mirror"),
]
stories = _clustered(items)
assert len(stories) == 1
assert stories[0].coverage_count == 3
def test_unrelated_stories_stay_separate():
items = [
_item("Apple unveils new iPhone at launch event", "https://a.example/1",
summary="Apple took the stage in Cupertino to reveal the next iPhone."),
_item("Hurricane forecast calls for record storm surge", "https://a.example/2",
summary="Meteorologists upgraded the storm to a category five system."),
_item("Local bakery wins national award for sourdough", "https://a.example/3",
summary="Judges praised the long fermentation and crust texture."),
]
stories = _clustered(items)
assert len(stories) == 3
def test_chain_merging_transitive():
"""A≈B and B≈C => all three one cluster, even if A≉C."""
core = ("The two crew members on board were killed. Authorities have opened "
"an inquiry into the cause. Air traffic around Miami Beach was halted "
"for hours after the crash.")
items = [
_item("Cargo plane crash near Miami leaves two dead", "https://bbc.example/x",
source="BBC", summary="The aircraft went down on Tuesday morning. " + core),
_item("Two dead as cargo aircraft crashes by Miami beach", "https://npr.example/x",
source="NPR", summary="Emergency crews reached the wreckage. " + core),
_item("Miami cargo crash kills both pilots on board", "https://wp.example/x",
source="WaPo", summary="The plane came down just off the beach. " + core),
]
stories = _clustered(items)
assert len(stories) == 1
assert stories[0].coverage_count == 3
def test_ranking_prefers_high_coverage_and_freshness():
from cluster import compute_scores
now = time.time()
items = [
_item("Viral story covered everywhere", "https://a.example/hot", published=now - 1000),
_item("Viral story covered everywhere", "https://b.example/hot-2", published=now - 900),
_item("Viral story covered everywhere", "https://c.example/hot-3", published=now - 800),
_item("Niche old post", "https://d.example/niche", published=now - 86400 * 3),
]
stories = _clustered(items)
assert len(stories) == 2
scored = compute_scores(stories, time.time())
# 3-source fresh story outranks the single-source old one
assert scored[0][1].coverage_count == 3
def test_summary_field_capped_input():
"""LLM lead text is headline + ~300 chars, never the long tail (defended in code)."""
from cluster import llm_summarize_topk
import os
os.environ.pop("LLM_BASE_URL", None)
# passes without error even with no LLM backend (network -> skipped/strange localhost)
# we only assert the display cap helper field exists on Story
items = [_item("Test", "https://a.example/t")]
s = _clustered(items)[0]
assert hasattr(s, "summary")