Test AI-Written Pagination Against a Tied-Timestamp Contract

AI-generated illustration. A plausible first page is not enough: acceptance depends on whether the full traversal follows the independently defined contract.
To test AI-generated pagination against requirements, define the expected sequence before inspecting the implementation. Then check traversal across page boundaries, not just page sizes.
For tied timestamps, the key question is: does the change preserve the required sequence, or merely return plausible-looking pages? A six-record fixture makes that difference visible.
Turn the requirement into a contract
Start with this requirement:
For an unchanged dataset, return every matching record exactly once, newest first.
“Newest first” leaves ties unresolved. Decide how to order records with equal timestamps before the implementation becomes the reference behavior for its own tests.
| Contract element | Choice for this example |
|---|---|
| Dataset | Fixed matching records throughout traversal |
| Timestamp | Non-null created_at |
| Tie-breaker | Unique integer id |
| Ordering | created_at DESC, id DESC |
| Cursor position | Timestamp and ID of the last returned record |
| Advancement | Records strictly after that pair in the declared order |
| Completion | Continuing past the final record returns no further results |
| Observable result | Concatenated pages equal the expected sequence, without omissions or duplicates |
Descending ID is a chosen tie-breaker, not evidence that a higher ID means a later creation time. Another stable, unique tie-breaker could work, but the requirement, query, cursor, and test must agree.
For this ordering, a continuation query has this shape:
SELECT id, created_at
FROM records
WHERE created_at < :cursor_created_at
OR (created_at = :cursor_created_at AND id < :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT :page_size;
This is illustrative SQL. The first page omits the continuation predicate. If the endpoint also filters records, preserve those filters and group the continuation predicate appropriately.
An older timestamp comes after the cursor; at the same timestamp, a smaller ID comes after it. Strict comparisons exclude the cursor record itself.
A full first page can still lose four records
Illustrative fixture: In an isolated test database, create six matching records with IDs 1 through 6, all with created_at = 2025-01-01T00:00:00Z. Keep the dataset unchanged and set the page size to two.
The expected pages are:
Page 1: [6, 5]
Page 2: [4, 3]
Page 3: [2, 1]
Page 4: []
Now consider an implementation that orders by both fields but stores only the timestamp in its cursor. After returning [6, 5], it continues with:
WHERE created_at < :cursor_created_at
Every remaining record has the cursor’s timestamp. None satisfies the predicate, so page two is empty. Four records disappear from the traversal even though the first page has the right size and order.
Changing < to <= does not fix this. With the same ordering and no additional advancement mechanism, the query returns [6, 5] again.
The cursor must identify a position in the ordering, not just a timestamp bucket.

AI-generated illustration. In the article’s fixed fixture, equal timestamps require a unique tie-breaker. The cursor carries both timestamp and ID so traversal can continue within the tie.
Exercise the changed continuation path
Run the fixture through the changed pagination path, including its cursor encoder and decoder. A mock that already returns the expected pages would check the harness, not the change.
The following Python example is an untested test harness. Its read_page adapter is a proposed interface, not a supplied library API:
read_page(limit=2, after=None)returns the first page.- Each response exposes
rowsandnext_cursor. - Each row exposes integer
id. - Every nonempty page supplies a continuation cursor, allowing an explicit terminal empty-page request.
def assert_tied_timestamp_contract(read_page):
expected_pages = ([6, 5], [4, 3], [2, 1], [])
cursor = None
seen = []
for expected_ids in expected_pages:
page = read_page(limit=2, after=cursor)
actual_ids = [row["id"] for row in page["rows"]]
assert actual_ids == expected_ids, (
f"Expected {expected_ids}, got {actual_ids}"
)
seen.extend(actual_ids)
if expected_ids:
next_cursor = page["next_cursor"]
assert next_cursor is not None
assert next_cursor != cursor
cursor = next_cursor
assert seen == [6, 5, 4, 3, 2, 1]
assert len(seen) == len(set(seen))
If the API signals completion on the last nonempty page, adapt the terminal assertion to that documented behavior. Do not change the public API to fit the harness. The essential check remains: all six records arrive in order, and traversal ends without additional records.
The page boundary falls inside a timestamp tie. A strict timestamp-only continuation fails at page two; an inclusive one repeats page one. An ascending-ID tie-breaker fails the first-page expectation.
Derive expected pages from the requirement. Computing them with the cursor logic under review can reproduce the same defect on both sides of an assertion.
What passing does and does not establish
A pass shows that this implementation traversed this fixed fixture in the required order. It does not establish correctness across all inputs.
Keep these limits explicit:
- Mixed timestamps: All six timestamps are equal, so even an incorrect ID-only implementation could pass. Add mixed-timestamp fixtures to check ordering and advancement across different timestamps.
- Concurrent writes: Inserts ahead of an already-passed cursor may be missed. Changes to ordering fields can move records across the cursor boundary, causing omissions or repeats.
- Snapshot consistency: A timestamp-and-ID cursor identifies a position. It does not freeze the dataset across requests.
- Cursor handling: This fixture does not cover all timestamp precision, serialization, or invalid-cursor cases. Add tests where the API contract requires them.
- Performance: Six records do not establish query efficiency or index suitability at scale.
For changing data, first decide whether the API promises a fixed snapshot, live results with documented limitations, or another consistency model. That requires separate tests.
Make the contract the acceptance gate
Before accepting the change, compare the declared ordering, continuation predicate, and decoded cursor fields. They should describe the same position: (created_at, id) under descending order.
Then run the fixture through the changed path. A passing test alone cannot confirm that all three elements agree. Keep the worksheet and expected pages beside it so reviewers can see both the intended behavior and the limits of the evidence.
Next, use ThunderLang’s getting-started guide to apply an explicit acceptance contract to your first AI-assisted change.