import unittest from structs.headline import Headline from structs.story import Story from services.stories import cluster_stories def h(text, tokens, url=""): return Headline(text, tokens, url) class TestHeadlineSourceTracking(unittest.TestCase): def test_domain_extraction_strips_www_and_lowercases(self): headline = h("A story", ["a"], "https://www.CNN.com/us") self.assertEqual(headline.domain, "cnn.com") def test_domain_extraction_section_paths_collapse(self): us = h("A story", ["a"], "https://www.cnn.com/us") politics = h("A story", ["a"], "https://cnn.com/politics") self.assertEqual(us.domain, politics.domain) def test_domain_is_none_when_source_missing(self): self.assertIsNone(h("A story", ["a"]).domain) self.assertIsNone(h("A story", ["a"], "").domain) self.assertIsNone(h("A story", ["a"], " ").domain) class TestStory(unittest.TestCase): def test_sources_dedup_preserving_order(self): story = Story([ h("Story A", ["a"], "https://www.cnn.com/us"), h("Story A", ["a"], "https://foxnews.com/politics"), h("Story A", ["a"], "https://www.cnn.com/politics"), ]) self.assertEqual(story.sources, ["cnn.com", "foxnews.com"]) self.assertEqual(story.source_count, 2) def test_representative_is_longest_headline(self): story = Story([ h("Short", ["short"], "https://a.com"), h("A much longer descriptive headline", ["long"], "https://b.com"), ]) self.assertEqual(story.representative, "A much longer descriptive headline") class TestClusterStories(unittest.TestCase): def test_transitive_clustering_merges_chain(self): # Three headlines sharing tokens pairwise but not as a triple. headlines = [ h("alpha beta", ["alpha", "beta"], "https://a.com"), h("beta gamma", ["beta", "gamma"], "https://b.com"), h("delta epsilon", ["delta", "epsilon"], "https://c.com"), ] stories = cluster_stories(headlines, threshold=0.4) texts = {s.representative for s in stories} # alpha/beta and beta/gamma share "beta" -> one cluster; delta/epsilon separate. self.assertEqual(len(stories), 2) self.assertIn("alpha beta", texts) self.assertIn("delta epsilon", texts) def test_non_similar_headlines_stay_separate(self): headlines = [ h("apple pie", ["apple", "pie"], "https://a.com"), h("quantum physics", ["quantum", "physics"], "https://b.com"), ] stories = cluster_stories(headlines, threshold=0.6) self.assertEqual(len(stories), 2) def test_empty_input(self): self.assertEqual(cluster_stories([], 0.75), []) if __name__ == "__main__": unittest.main()