# retoor import copy from tests.conftest import run_async from devplacepy.services.community.discoverer import CommunityDiscoverer from devplacepy.services.community.learner import HtmlInferrer from devplacepy.services.community.runtime import MAX_POST_LENGTH, Collector from devplacepy.services.community.extract import parse_html, select_nodes from devplacepy.services.sync.records import RECORD_MODELS from tests.unit.services.community.fakes import ( FakeFetcher, RSS, detail_page, json_community, no_sleep, ) LINKED_ITEM = """Linked story {n} on testinghttps://elsewhere.test/a/{n} https://blog.test/s/{n}Tue, {n:02d} Oct 2026 10:00:00 GMT https://blog.test/s/{n} Alice<p>Body of story {n} with enough text.</p>""" def discussion_community() -> dict: items = "".join(LINKED_ITEM.format(n=n) for n in range(1, 7)) routes = { "https://blog.test/": '' "Blog Testhi", "https://blog.test/feed.xml": RSS.format(items=items), } for n in range(1, 7): routes[f"https://elsewhere.test/a/{n}"] = f"
{'External article text. ' * 20}
" routes[f"https://blog.test/s/{n}"] = detail_page("story", n) return routes def learn(routes: dict, query: str) -> dict: result = run_async(CommunityDiscoverer(FakeFetcher(routes)).discover(query)) assert result.schema, result.error return result.schema def test_rss_comments_are_learned_from_the_discussion_link_not_the_article(): routes = discussion_community() schema = learn(routes, "https://blog.test/") feed = schema["feeds"][0] assert feed["post"]["fields"]["comments_url"] == ["$.comments"] assert feed["comments"]["mode"] == "page" result = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run()) assert result["records"]["comments"] posts = {post["origin_uid"] for post in result["records"]["posts"]} assert {comment["post_uid"] for comment in result["records"]["comments"]} <= posts def test_post_body_still_comes_from_the_article_page(): routes = discussion_community() schema = learn(routes, "https://blog.test/") result = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run()) assert all("External article text." in post["content"] for post in result["records"]["posts"]) LISTING = """
1 Story {n} about shells {n} comments

Excerpt of story {n} that is long enough to count.

""" def listing_nodes() -> list: root = parse_html("" + "".join(LISTING.format(n=n) for n in range(1, 9)) + "") return select_nodes(root, "div.story") def test_constant_first_anchor_is_never_chosen_as_identifier(): fields = HtmlInferrer(listing_nodes(), "https://board.test/").post_fields() assert fields["id"] == ["a.u-url::attr(href)"] assert fields["title"] == ["a.u-url"] assert fields["comments_url"] == ["a.discuss::attr(href)"] def test_no_distinct_anchor_means_no_identifier(): page = "".join('
x

Some text that is long enough here.

' for _ in range(6)) nodes = select_nodes(parse_html(f"{page}"), "div.story") assert "id" not in HtmlInferrer(nodes, "https://board.test/").post_fields() def test_oversized_posts_and_titles_are_truncated_to_valid_sync_records(): routes = json_community() schema = learn(routes, "https://forum.test/api/posts") routes["https://forum.test/api/posts"]["items"][0]["content_html"] = "

" + "x" * (MAX_POST_LENGTH * 2) + "

" routes["https://forum.test/api/posts"]["items"][0]["title"] = "t" * 900 result = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run()) for post in result["records"]["posts"]: assert len(post["content"]) <= MAX_POST_LENGTH RECORD_MODELS["posts"].model_validate(post) assert any(len(post["content"]) == MAX_POST_LENGTH for post in result["records"]["posts"]) def test_failed_article_fetch_is_a_warning_and_the_post_is_still_imported(): routes = discussion_community() schema = learn(routes, "https://blog.test/") assert schema["feeds"][0]["post"].get("detail") routes["https://elsewhere.test/a/1"] = (403, "forbidden") result = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run()) assert result["errors"] == [] assert any("elsewhere.test/a/1" in line for line in result["warnings"]) assert len(result["records"]["posts"]) == 6 def test_same_story_from_two_feeds_is_one_post_and_comments_follow_it(): routes = discussion_community() schema = learn(routes, "https://blog.test/") second = copy.deepcopy(schema["feeds"][0]) second["id"] = "feed2" second["post"]["fields"]["id"] = ["$.link"] schema["feeds"].append(second) result = run_async(Collector(FakeFetcher(routes), schema, sleep=no_sleep).run()) assert len(result["records"]["posts"]) == 6 posts = {post["origin_uid"] for post in result["records"]["posts"]} assert {comment["post_uid"] for comment in result["records"]["comments"]} <= posts