Files
2026-07-13 13:12:00 +08:00

174 lines
6.3 KiB
Python

"""Tests for pagination entity and paginate_in_memory helper."""
from __future__ import annotations
from omnigent.entities.pagination import PagedList, paginate_in_memory
# ── PagedList ─────────────────────────────────────────
def test_paged_list_defaults() -> None:
page = PagedList()
assert page.data == []
assert page.first_id is None
assert page.last_id is None
assert page.has_more is False
def test_paged_list_independent_defaults() -> None:
"""Each PagedList gets its own data list (no mutable-default footgun)."""
a = PagedList()
b = PagedList()
a.data.append("x")
assert b.data == []
# ── paginate_in_memory ────────────────────────────────
_ITEMS = [
{"id": "1", "name": "first"},
{"id": "2", "name": "second"},
{"id": "3", "name": "third"},
{"id": "4", "name": "fourth"},
{"id": "5", "name": "fifth"},
]
def _id_fn(item: dict) -> str:
return item["id"]
def test_paginate_asc_no_cursor() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=3, order="asc")
assert len(result.data) == 3
assert result.first_id == "1"
assert result.last_id == "3"
assert result.has_more is True
def test_paginate_asc_all_fit() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, order="asc")
assert len(result.data) == 5
assert result.has_more is False
assert result.first_id == "1"
assert result.last_id == "5"
def test_paginate_desc_no_cursor() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=3, order="desc")
assert len(result.data) == 3
assert result.first_id == "5"
assert result.last_id == "3"
assert result.has_more is True
def test_paginate_after_cursor_asc() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, after="2", order="asc")
assert [_id_fn(i) for i in result.data] == ["3", "4"]
assert result.has_more is True
def test_paginate_after_cursor_exhausts() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, after="3", order="asc")
assert [_id_fn(i) for i in result.data] == ["4", "5"]
assert result.has_more is False
def test_paginate_before_cursor_asc() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, before="4", order="asc")
assert [_id_fn(i) for i in result.data] == ["1", "2", "3"]
assert result.has_more is False
def test_paginate_after_and_before() -> None:
"""Both cursors narrow the window."""
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, after="1", before="5", order="asc")
assert [_id_fn(i) for i in result.data] == ["2", "3", "4"]
def test_paginate_empty_list() -> None:
result = paginate_in_memory([], _id_fn, limit=10, order="asc")
assert result.data == []
assert result.first_id is None
assert result.last_id is None
assert result.has_more is False
def test_paginate_cursor_not_found() -> None:
"""Unknown cursor id is silently ignored (no items skipped)."""
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, after="999", order="asc")
assert len(result.data) == 5
def test_paginate_limit_one() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=1, order="asc")
assert len(result.data) == 1
assert result.first_id == "1"
assert result.last_id == "1"
assert result.has_more is True
def test_paginate_desc_after_cursor() -> None:
"""After cursor in desc order — items appear reversed, cursor still works."""
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, after="4", order="desc")
# desc reverses to [5,4,3,2,1]; after "4" gives [3,2,1]; limit 2 = [3,2]
assert [_id_fn(i) for i in result.data] == ["3", "2"]
assert result.has_more is True
def test_paginate_desc_before_cursor() -> None:
result = paginate_in_memory(_ITEMS, _id_fn, limit=10, before="3", order="desc")
# desc reverses to [5,4,3,2,1]; before "3" gives [5,4]
assert [_id_fn(i) for i in result.data] == ["5", "4"]
# ── backward (before-cursor) pagination ───────────────
#
# A `before` cursor must return the page immediately *preceding* the
# cursor (anchored to the end of the range), not the first page. When
# more than `limit` items precede the cursor the page should be the last
# `limit` of them, and `has_more` should report whether still-earlier
# items remain — matching host._paginate_list_dir's semantics.
def test_paginate_before_cursor_returns_preceding_page() -> None:
"""`before` returns the `limit` items just before the cursor."""
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, before="5", order="asc")
assert [_id_fn(i) for i in result.data] == ["3", "4"]
assert result.first_id == "3"
assert result.last_id == "4"
# Items "1" and "2" still precede this page.
assert result.has_more is True
def test_paginate_before_cursor_walks_to_start() -> None:
"""Paging back again reaches the first items with has_more False."""
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, before="3", order="asc")
assert [_id_fn(i) for i in result.data] == ["1", "2"]
assert result.has_more is False
def test_paginate_desc_before_cursor_returns_preceding_page() -> None:
"""Backward paging in desc order anchors to the cursor, not page 1."""
# desc reverses to [5,4,3,2,1]; the two items before "1" are [3,2].
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, before="1", order="desc")
assert [_id_fn(i) for i in result.data] == ["3", "2"]
assert result.has_more is True
def test_paginate_after_and_before_anchor_to_before() -> None:
"""With both cursors, a short page anchors to the `before` end."""
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, after="1", before="5", order="asc")
# Window is (1, 5) -> [2,3,4]; the last `limit` before "5" are [3,4].
assert [_id_fn(i) for i in result.data] == ["3", "4"]
# Item "2" still precedes this page ("1" is excluded by `after`).
assert result.has_more is True
def test_paginate_before_cursor_unknown_is_ignored() -> None:
"""An unknown `before` cursor is ignored (returns the first page)."""
result = paginate_in_memory(_ITEMS, _id_fn, limit=2, before="999", order="asc")
assert [_id_fn(i) for i in result.data] == ["1", "2"]
assert result.has_more is True