Files
titlebot-ng-ng/test_countvotes.py
T
2026-10-31 01:12:01 -05:00

184 lines
7.9 KiB
Python

import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from nio import AsyncClient, RoomMessagesResponse
from nio.responses import RoomEventRelationsResponse
from room import Room
class Message(SimpleNamespace):
pass
class Response(SimpleNamespace):
pass
def message(body, event_id, sender="bot"):
return Message(body=body, event_id=event_id, sender=sender, source={})
def reaction(target, sender, event_id, key="👍"):
return SimpleNamespace(sender=sender, event_id=event_id, source={
"type": "m.reaction",
"content": {"m.relates_to": {
"rel_type": "m.annotation", "key": key, "event_id": target,
}},
})
class CountVotesTests(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.room = Room.__new__(Room)
self.room.config = SimpleNamespace(room_id="test", batch_size=100)
self.room.user_id = "bot"
self.room.client = SimpleNamespace(room_messages=AsyncMock())
self.enterContext(patch("room.RoomMessageText", Message))
self.enterContext(patch("room.RoomMessagesResponse", Response))
async def count(self, *pages, live_reactions=None):
self.room.client.room_messages.side_effect = pages
events = [event for page in pages for event in getattr(page, "chunk", [])]
removed = set()
for event in events:
source = getattr(event, "source", {})
if source.get("type") == "m.room.redaction":
removed.add(source.get("redacts") or source.get("content", {}).get("redacts"))
async def relations(room_id, event_id, **kwargs):
for event in events if live_reactions is None else live_reactions:
relation = event.source.get("content", {}).get("m.relates_to", {})
if relation.get("event_id") == event_id and event.event_id not in removed:
yield event
self.room.client.room_get_event_relations = relations
return await self.room.count_votes()
async def test_votes_absent_from_timeline_are_read_from_live_relations(self):
result = await self.count(Response(chunk=[
message("Suggestion 1: Author: Title", "$one"),
message("Time to vote!", "$start"),
], end=None), live_reactions=[
reaction("$one", "alice", "$vote", key="👍🏽"),
reaction("$one", "bob", "$vote2", key="👍\ufe0f"),
])
self.assertIn("Suggestion 1: 2 votes", result)
async def test_relation_failure_is_not_reported_as_zero_votes(self):
async def broken_relations(*args, **kwargs):
raise RuntimeError("Server unavailable")
yield
self.room.client.room_messages.return_value = Response(chunk=[
message("Suggestion 1: Author: Title", "$one"),
message("Time to vote!", "$start"),
], end=None)
self.room.client.room_get_event_relations = broken_relations
result = await self.room.count_votes()
self.assertIn("Unable to read emoji votes", result)
self.assertNotIn("0 votes", result)
async def test_latest_round_ranking_and_later_votes_refresh(self):
latest = [
reaction("$two", "alice", "$v2"),
reaction("$one", "alice", "$v1"),
reaction("$one", "alice", "$duplicate"),
reaction("$one", "bot", "$starter"),
reaction("$one", "charlie", "$wrong", key="👎"),
reaction("$old", "dana", "$oldvote"),
message("Suggestion 3: Author: Zero votes", "$three"),
message("Suggestion 2: Author: Second title", "$two"),
message("Suggestion 1: Author: First title", "$one"),
message("Time to vote! 🚢", "$announcement"),
message("Suggestion 1: Author: Old title", "$old"),
]
result = await self.count(Response(chunk=latest, end="older"))
self.assertIn("Suggestion 1: 1 vote", result)
self.assertIn("Suggestion 2: 1 vote", result)
self.assertIn("Suggestion 3: 0 votes", result)
self.assertNotIn("Old title", result)
self.assertLess(result.index("Suggestion 1:"), result.index("Suggestion 2:"))
updated = await self.count(Response(chunk=[
reaction("$two", "bob", "$later"), *latest,
], end="older"))
self.assertIn("Suggestion 2: 2 votes", updated)
self.assertLess(updated.index("Suggestion 2:"), updated.index("Suggestion 1:"))
async def test_removed_reactions_and_zero_vote_results(self):
redactions = [SimpleNamespace(source={
"type": "m.room.redaction", **target,
}) for target in ({"redacts": "$removed"}, {"content": {"redacts": "$removedv11"}})]
result = await self.count(Response(chunk=[
*redactions,
reaction("$one", "alice", "$removed"),
reaction("$one", "bob", "$removedv11"),
message("Suggestion 1: Author: Title", "$one"),
message("Time to vote!", "$start"),
], end=None))
self.assertIn("Suggestion 1: 0 votes", result)
async def test_round_older_than_fifteen_pages(self):
pages = [Response(chunk=[reaction("$one", "alice", "$vote")], end="1")]
pages.extend(Response(chunk=[message("Chat", f"$chat{i}", sender="alice")], end=str(i))
for i in range(2, 18))
pages.append(Response(chunk=[
message("Suggestion 1: Author: Title", "$one"),
message("Time to vote!", "$start"),
], end="old"))
result = await self.count(*pages)
self.assertIn("Suggestion 1: 1 vote", result)
self.assertEqual(self.room.client.room_messages.await_count, 18)
async def test_history_failure_does_not_report_partial_counts(self):
result = await self.count(Response(chunk=[
message("Suggestion 1: Author: Title", "$one"),
], end="older"), object())
self.assertIn("Unable to read voting history", result)
async def test_no_round(self):
self.assertIn("No suggestion posts found", await self.count(Response(chunk=[], end=None)))
class MatrixSDKVotesTests(unittest.IsolatedAsyncioTestCase):
async def test_real_sdk_parses_and_paginates_live_reactions(self):
room = Room.__new__(Room)
room.config = SimpleNamespace(room_id="!room:matrix.test", batch_size=2)
room.user_id = "@bot:matrix.test"
room.client = AsyncClient("https://matrix.test", room.user_id)
room.client.access_token = "test-token"
self.addAsyncCleanup(room.client.close)
def event(event_id, sender, event_type, content):
return {"event_id": event_id, "sender": sender, "type": event_type,
"origin_server_ts": 100000, "content": content,
"room_id": room.config.room_id}
history = RoomMessagesResponse.from_dict({
"start": "now", "chunk": [
event("$one", room.user_id, "m.room.message", {
"msgtype": "m.text", "body": "Suggestion 1: Author: Title"}),
event("$start", room.user_id, "m.room.message", {
"msgtype": "m.text", "body": "Time to vote!"}),
],
}, room.config.room_id)
def votes(sender, event_id, next_batch=None):
return RoomEventRelationsResponse.from_dict({
"chunk": [event(event_id, sender, "m.reaction", {"m.relates_to": {
"rel_type": "m.annotation", "key": "👍", "event_id": "$one",
}})], **({"next_batch": next_batch} if next_batch else {}),
}, room.config.room_id, "$one")
room.client._send = AsyncMock(side_effect=[
history, votes("@alice:matrix.test", "$v1", "page2"),
votes("@bob:matrix.test", "$v2"),
])
result = await room.count_votes()
self.assertIn("Suggestion 1: 2 votes", result)
self.assertIn("from=page2", room.client._send.await_args_list[-1].args[2])
if __name__ == "__main__":
unittest.main()