Files
devplacepy/tests/unit/services/community/structured.py
T
retoorandClaude Sonnet 5.5 02ab747cd0 Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
Continuous Delivery could never be turned off once a crop was planted. Add a
one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool
game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant,
autoreplant claim or Refactor. Autoreplant now also respects crop locks through
the shared _crop_lock predicate. Plan and measurements in happyplanting.md.

Also includes the pending community sync, node network and related work in the
tree; the full suite passed (4999) with all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:23:43 +02:00

101 lines
5.2 KiB
Python

# retoor <retoor@molodetz.nl>
import json
import pytest
from devplacepy.services.community.discoverer import CommunityDiscoverer
from devplacepy.services.community.extract import extract_json_ld_posts, parse_html
from devplacepy.services.community.generator import CodeGenerator
from devplacepy.services.community.runtime import Collector
from devplacepy.services.community.schemacheck import validate_schema
from devplacepy.services.sync.records import RECORD_MODELS
from tests.conftest import run_async
from tests.unit.services.community.fakes import FakeFetcher
from tests.unit.services.community.generator import load, no_sleep
def thread(number: int, comments: int = 2) -> dict:
return {
"@type": "DiscussionForumPosting",
"@id": f"https://ld.test/t/{number}",
"headline": f"Thread number {number} about parsing",
"text": f"This is the body of thread {number}, written long enough to sync as a post.",
"author": {"@type": "Person", "name": f"writer{number % 2}", "url": f"https://ld.test/u/{number % 2}"},
"datePublished": f"2026-10-{number:02d}T10:00:00Z",
"dateModified": f"2026-10-{number:02d}T12:00:00Z",
"url": f"https://ld.test/t/{number}",
"comment": [
{
"@type": "Comment",
"@id": f"https://ld.test/t/{number}#c{index}",
"text": f"Comment {index} on thread {number}",
"author": {"@type": "Person", "name": f"replier{index}"},
"dateCreated": f"2026-10-{number:02d}T11:0{index}:00Z",
}
for index in range(comments)
],
}
def page(blobs: list) -> str:
scripts = "".join(f'<script type="application/ld+json">{json.dumps(blob)}</script>' for blob in blobs)
return f"<html><head><title>LD Forum</title>{scripts}</head><body><div id=app>Loading</div></body></html>"
def ld_site(blobs=None) -> dict:
blobs = blobs or [{"@context": "https://schema.org", "@graph": [thread(number) for number in range(1, 7)]}]
return {"https://ld.test/": page(blobs), "https://ld.test/about": "<html><body><p>About: 100 members.</p></body></html>"}
def test_the_extractor_normalizes_graphs_lists_and_nested_comments():
root = parse_html(page([{"@graph": [thread(1), {"@type": "WebSite", "name": "x"}]}, thread(2), [thread(3)], {"@type": "ItemList", "itemListElement": [{"item": thread(4)}]}]))
posts = extract_json_ld_posts(root)
assert [item["id"] for item in posts] == [f"https://ld.test/t/{n}" for n in (1, 2, 3, 4)]
first = posts[0]
assert first["title"] == "Thread number 1 about parsing" and first["author"] == {"username": "writer1", "id": "https://ld.test/u/1"}
assert first["created_at"].startswith("2026-10-01") and len(first["comments"]) == 2
assert first["comments"][0]["content"] == "Comment 0 on thread 1" and first["comments"][0]["author"] == {"username": "replier0"}
def test_garbage_and_non_posts_are_ignored():
root = parse_html('<script type="application/ld+json">{not json</script><script type="application/ld+json">{"@type":"Organization","name":"x"}</script><script>var x=1</script>')
assert extract_json_ld_posts(root) == []
nameless = parse_html(page([{"@type": "Article", "headline": "No identity at all"}]))
assert extract_json_ld_posts(nameless) == []
def test_a_site_with_only_structured_data_is_learned_end_to_end():
result = run_async(CommunityDiscoverer(FakeFetcher(ld_site())).discover("https://ld.test/"))
assert result.status == "ready", result.error
feed = result.schema["feeds"][0]
assert feed["type"] == "json_ld" and feed["post"]["fields"]["id"] and feed["comments"]["mode"] == "inline"
assert validate_schema(result.schema) == []
records = result.sample["records"]
assert len(records["posts"]) == 6 and len(records["comments"]) == 6 and records["users"]
full = run_async(Collector(FakeFetcher(ld_site()), result.schema, sleep=no_sleep).run())
assert len(full["records"]["comments"]) == 12
for kind, rows in records.items():
for row in rows:
RECORD_MODELS[kind].model_validate(row)
assert {user["username"] for user in records["users"]} >= {"writer0", "writer1", "replier0"}
def test_the_collector_and_the_generated_file_agree_on_structured_data(tmp_path):
schema = run_async(CommunityDiscoverer(FakeFetcher(ld_site())).discover("https://ld.test/")).schema
direct = run_async(Collector(FakeFetcher(ld_site()), schema, sleep=no_sleep).run())
module = load(CodeGenerator(schema).write(tmp_path))
generated = run_async(module.collect_all(FakeFetcher(ld_site()), sleep=no_sleep))
assert {kind: len(rows) for kind, rows in direct["records"].items()} == {kind: len(rows) for kind, rows in generated["records"].items()}
def test_a_single_structured_post_is_not_a_feed():
result = run_async(CommunityDiscoverer(FakeFetcher(ld_site([thread(1)]))).discover("https://ld.test/"))
assert result.status != "ready"
@pytest.mark.parametrize("count", [2, 5])
def test_few_posts_still_make_a_feed(count):
result = run_async(CommunityDiscoverer(FakeFetcher(ld_site([{"@graph": [thread(number, 0) for number in range(1, count + 1)]}]))).discover("https://ld.test/"))
assert result.status == "ready", result.error