Improve Bluesky reply/quote accessibility and split test bootstrap workflow

- Fix Bluesky quoted post rendering across list output, screen-reader speech, and View Post by centralizing quote extraction.
- Add robust quote URL extraction (facets/embed/text), include quoted URLs in URL shortcuts, and append full quoted URLs when hidden/truncated.
- Improve reply context handling:
  - add and use `$reply_to` template variable,
  - hydrate missing reply target handles in home/feed items,
  - keep backward compatibility for templates that do not include `$reply_to`.
- Align Bluesky default/fallback post templates to include reply context (`$reply_to`).
- Add/extend focused Bluesky tests for quote text, quote URLs, reply context, and template fallback behavior.
- Refactor scripts:
  - add bootstrap-dev.ps1 for environment setup (submodules, venv, deps),
  - keep run-tests.ps1 focused on running tests only,
  - add PowerShell comment-based help in English.
- Update README with the new bootstrap/test workflow and examples.
This commit is contained in:
Juanjo M
2026-02-15 23:50:00 +00:00
parent abf4cb0df1
commit 6e56d94448
11 changed files with 919 additions and 35 deletions

View File

@@ -0,0 +1,212 @@
# -*- coding: utf-8 -*-
import builtins
import unittest
from sessions.blueski import compose, templates, utils
class TestBlueskyQuotedPosts(unittest.TestCase):
def setUp(self):
if not hasattr(builtins, "_"):
builtins._ = lambda s: s
def _build_quoted_post(self):
return {
"post": {
"author": {"handle": "alice.bsky.social", "displayName": "Alice"},
"record": {"text": "Main post text"},
"embed": {
"$type": "app.bsky.embed.recordWithMedia#view",
"record": {
"$type": "app.bsky.embed.record#view",
"record": {
"$type": "app.bsky.embed.record#viewRecord",
"author": {"handle": "bob.bsky.social"},
"value": {"text": "Quoted post text"},
},
},
"media": {
"$type": "app.bsky.embed.images#view",
"images": [],
},
},
"indexedAt": "2026-02-15T10:00:00Z",
}
}
def _with_reply_context(self, item, reply_to_handle="carol.bsky.social"):
if isinstance(item, dict):
item["reply"] = {
"parent": {
"author": {
"handle": reply_to_handle,
}
}
}
return item
def test_extract_quoted_post_info_with_text(self):
item = self._build_quoted_post()
info = utils.extract_quoted_post_info(item)
self.assertIsNotNone(info)
self.assertEqual(info["kind"], "post")
self.assertEqual(info["handle"], "bob.bsky.social")
self.assertEqual(info["text"], "Quoted post text")
def test_compose_post_includes_quoted_text(self):
item = self._build_quoted_post()
result = compose.compose_post(
item,
db={},
settings={"general": {}},
relative_times=True,
show_screen_names=False,
safe=True,
)
self.assertIn("Quoting @bob.bsky.social: Quoted post text", result[1])
def test_template_render_post_includes_quoted_text(self):
item = self._build_quoted_post()
rendered = templates.render_post(
item,
template="$display_name, $safe_text $date.",
settings={"general": {}},
relative_times=True,
offset_hours=0,
)
self.assertIn("Quoting @bob.bsky.social: Quoted post text", rendered)
def test_extract_quoted_post_info_includes_urls(self):
item = self._build_quoted_post()
quoted = item["post"]["embed"]["record"]["record"]
quoted["value"]["facets"] = [
{
"features": [
{
"$type": "app.bsky.richtext.facet#link",
"uri": "https://example.com/full-link",
}
]
}
]
info = utils.extract_quoted_post_info(item)
self.assertIn("https://example.com/full-link", info["urls"])
def test_find_urls_includes_quoted_urls(self):
item = self._build_quoted_post()
quoted = item["post"]["embed"]["record"]["record"]
quoted["value"]["facets"] = [
{
"features": [
{
"$type": "app.bsky.richtext.facet#link",
"uri": "https://example.com/quoted-only",
}
]
}
]
urls = utils.find_urls(item)
self.assertIn("https://example.com/quoted-only", urls)
def test_compose_post_appends_full_quoted_url(self):
item = self._build_quoted_post()
quoted = item["post"]["embed"]["record"]["record"]
quoted["value"]["text"] = "Mira example.com/..."
quoted["value"]["facets"] = [
{
"features": [
{
"$type": "app.bsky.richtext.facet#link",
"uri": "https://example.com/full-target",
}
]
}
]
result = compose.compose_post(
item,
db={},
settings={"general": {}},
relative_times=True,
show_screen_names=False,
safe=True,
)
self.assertIn("[https://example.com/full-target]", result[1])
def test_extract_reply_to_handle_from_record_parent_author(self):
item = {
"post": {
"record": {
"text": "Reply text",
"reply": {
"parent": {
"uri": "at://did:plc:parent/app.bsky.feed.post/abc",
"author": {"handle": "parent.user"},
}
},
}
}
}
handle = utils.extract_reply_to_handle(item)
self.assertEqual(handle, "parent.user")
def test_template_render_post_reply_fallback_without_reply_to_variable(self):
item = {
"post": {
"author": {"handle": "alice.bsky.social", "displayName": "Alice"},
"record": {
"text": "Reply body",
"reply": {
"parent": {
"uri": "at://did:plc:parent/app.bsky.feed.post/abc",
"author": {"handle": "parent.user"},
}
},
},
"indexedAt": "2026-02-15T10:00:00Z",
}
}
rendered = templates.render_post(
item,
template="$display_name, $safe_text $date.",
settings={"general": {}},
relative_times=True,
offset_hours=0,
)
self.assertIn("Replying to @parent.user.", rendered)
def test_extract_reply_to_handle(self):
item = self._with_reply_context(self._build_quoted_post())
handle = utils.extract_reply_to_handle(item)
self.assertEqual(handle, "carol.bsky.social")
def test_compose_post_includes_reply_context(self):
item = self._with_reply_context(self._build_quoted_post())
result = compose.compose_post(
item,
db={},
settings={"general": {}},
relative_times=True,
show_screen_names=False,
safe=True,
)
self.assertIn("Replying to @carol.bsky.social:", result[1])
def test_template_render_post_exposes_reply_to_variable(self):
item = self._with_reply_context(self._build_quoted_post())
rendered = templates.render_post(
item,
template="$reply_to$text",
settings={"general": {}},
relative_times=True,
offset_hours=0,
)
self.assertIn("Replying to @carol.bsky.social.", rendered)
if __name__ == "__main__":
unittest.main()