""" Reading a chat backwards. `get_messages(since, limit)` pages *forward* from the oldest message, which is the right shape for "what happened since I last looked" and the wrong shape for opening a conversation. The browser asked it for `since=0, limit=200`, so a group with more than 200 messages showed its first 200 and never the exchange the reader came for. These tests pin the direction, because the bug was not a crash — it was a plausible-looking screen full of the wrong messages. The cursor is the row id rather than the timestamp. `timestamp` is a float from `time.time()`, so two messages saved in the same tick can share one, and a timestamp cursor would then skip a message or return it twice. """ from pathlib import Path import pytest import pytest_asyncio from meshbay_node.chat.store import ChatStore @pytest_asyncio.fixture async def store(tmp_path: Path): async with ChatStore(tmp_path / "chat.db") as s: yield s async def _fill(store, n: int) -> None: for i in range(n): await store.save_message(sender_id="u1", iteration=i, payload=f"msg-{i:03d}".encode()) def _texts(msgs) -> list[str]: return [m.payload.decode() for m in msgs] @pytest.mark.asyncio async def test_recent_returns_the_newest_not_the_oldest(store): await _fill(store, 300) got = _texts(await store.get_recent(limit=100)) assert len(got) == 100 assert got[-1] == "msg-299", "the newest message must be in the answer" assert got[0] == "msg-200" @pytest.mark.asyncio async def test_recent_is_ordered_oldest_first_for_rendering(store): await _fill(store, 10) got = _texts(await store.get_recent(limit=5)) assert got == ["msg-005", "msg-006", "msg-007", "msg-008", "msg-009"] @pytest.mark.asyncio async def test_recent_handles_fewer_messages_than_the_limit(store): await _fill(store, 3) assert _texts(await store.get_recent(limit=100)) == \ ["msg-000", "msg-001", "msg-002"] @pytest.mark.asyncio async def test_recent_on_an_empty_group(store): assert await store.get_recent(limit=100) == [] @pytest.mark.asyncio async def test_before_walks_backwards_without_gap_or_repeat(store): """Paging to the start must show every message exactly once.""" await _fill(store, 250) page = await store.get_recent(limit=100) seen = _texts(page) while await store.has_before(page[0].id): page = await store.get_before(page[0].id, limit=50) assert page, "has_before said there was more" seen = _texts(page) + seen assert seen == [f"msg-{i:03d}" for i in range(250)] assert len(seen) == len(set(seen)), "no message returned twice" @pytest.mark.asyncio async def test_before_is_ordered_oldest_first(store): await _fill(store, 20) recent = await store.get_recent(limit=5) older = _texts(await store.get_before(recent[0].id, limit=5)) assert older == ["msg-010", "msg-011", "msg-012", "msg-013", "msg-014"] @pytest.mark.asyncio async def test_before_excludes_the_cursor_message(store): await _fill(store, 10) recent = await store.get_recent(limit=3) older = await store.get_before(recent[0].id, limit=10) assert recent[0].id not in [m.id for m in older] @pytest.mark.asyncio async def test_has_before_is_false_at_the_start_of_history(store): await _fill(store, 5) page = await store.get_recent(limit=100) assert await store.has_before(page[0].id) is False @pytest.mark.asyncio async def test_has_before_is_true_when_older_messages_exist(store): await _fill(store, 200) page = await store.get_recent(limit=100) assert await store.has_before(page[0].id) is True @pytest.mark.asyncio async def test_messages_sharing_a_timestamp_are_still_paged_exactly_once(store): """The reason the cursor is the row id rather than the timestamp. Nothing makes `timestamp` unique — it is a float from `time.time()`. In practice `save_message` commits per row and they rarely tie, so the tie is forced here instead of hoped for: what matters is that a shared timestamp cannot make a page skip a message or repeat one, not how often it happens. """ for i in range(40): await store._db.execute( "INSERT INTO messages (sender_id, iteration, payload, timestamp, " "thread_id, sender_name) VALUES (?, ?, ?, ?, ?, ?)", ("u1", i, f"msg-{i:03d}".encode(), 1000.0, None, "")) await store._db.commit() rows = await store.get_recent(limit=40) assert len({m.timestamp for m in rows}) == 1, "the tie should be in place" page = await store.get_recent(limit=10) seen = _texts(page) while await store.has_before(page[0].id): page = await store.get_before(page[0].id, limit=10) seen = _texts(page) + seen assert seen == [f"msg-{i:03d}" for i in range(40)] @pytest.mark.asyncio async def test_forward_paging_still_works_for_callers_that_want_it(store): """`get_messages` keeps its meaning — the node UI reads "since" from it.""" await _fill(store, 10) all_msgs = await store.get_messages(limit=100) cutoff = all_msgs[4].timestamp after = await store.get_messages(since=cutoff, limit=100) assert all(m.timestamp > cutoff for m in after)