| … | … | @@ -79,9 +79,12 @@ test('getRecord CID == golden record CID + value intact', async (t) => { |
| 79 | 79 | // 4. listRecords: one post, correct uri/cid. |
| 80 | 80 | test('listRecords scopes + shapes correctly', async (t) => { |
| 81 | 81 | const { s } = await storeWithGolden(t) |
| 82 | | const recs = await listRecords(s, 'app.bsky.feed.post', { did: DID }) |
| 82 | // Shape is { records, cursor } per com.atproto.repo.listRecords — it used to return a bare array, |
| 83 | // which was one of the ways limit/reverse/cursor went unimplemented without anyone noticing. |
| 84 | const { records: recs, cursor } = await listRecords(s, 'app.bsky.feed.post', { did: DID }) |
| 83 | 85 | t.ok(recs.length === 1 && recs[0].uri.endsWith(POST_RKEY) && recs[0].cid.startsWith('bafyrei'), |
| 84 | 86 | 'listRecords scopes + shapes correctly') |
| 87 | t.absent(cursor, 'and no cursor on the last page — its ABSENCE is how a client knows to stop') |
| 85 | 88 | }) |
| 86 | 89 | |
| 87 | 90 | // 5. LIVE oracle (bonus, non-fatal): millipds getRecord CID must equal ours. |
| … | … | @@ -97,3 +100,38 @@ test('LIVE millipds getRecord CID matches (or skip if PDS down)', async (t) => { |
| 97 | 100 | t.comment(`live=${live.cid} ours=${rec.cid}`) |
| 98 | 101 | t.is(live.cid, rec.cid, 'LIVE millipds getRecord CID matches') |
| 99 | 102 | }) |
| 103 | |
| 104 | // 4b. PAGING. limit/reverse/cursor were accepted and IGNORED, so a client asking for the newest 10 got |
| 105 | // the oldest N thousand and a pager looped forever on a cursor that never advanced. Non-conformance that |
| 106 | // looks like success. |
| 107 | test('listRecords honours limit, reverse and cursor', async (t) => { |
| 108 | const dir = mkdtempSync(join(tmpdir(), 'readview-pager-')) |
| 109 | const store = await new RepoStore(dir).ready() |
| 110 | t.teardown(async () => { try { await store.close() } catch {} rmSync(dir, { recursive: true, force: true }) }) |
| 111 | const DID2 = 'did:plc:pager' |
| 112 | // Ascending rkeys, so order is unambiguous. |
| 113 | for (const k of ['aaa', 'bbb', 'ccc', 'ddd', 'eee']) { |
| 114 | await store.put('app.bsky.feed.post', k, { $type: 'app.bsky.feed.post', text: k, createdAt: '2026-01-01T00:00:00.000Z' }) |
| 115 | } |
| 116 | |
| 117 | const p1 = await listRecords(store, 'app.bsky.feed.post', { did: DID2, limit: 2 }) |
| 118 | t.is(p1.records.length, 2, 'limit is honoured') |
| 119 | t.ok(p1.records[0].uri.endsWith('aaa') && p1.records[1].uri.endsWith('bbb'), 'ascending by default') |
| 120 | t.is(p1.cursor, 'bbb', 'and a cursor is returned while more remain') |
| 121 | |
| 122 | // Resume STRICTLY after the cursor — `>=` would return the cursor row twice and stall a pager that |
| 123 | // dedupes by uri. |
| 124 | const p2 = await listRecords(store, 'app.bsky.feed.post', { did: DID2, limit: 2, cursor: p1.cursor }) |
| 125 | t.ok(p2.records[0].uri.endsWith('ccc'), 'paging resumes strictly AFTER the cursor') |
| 126 | |
| 127 | const p3 = await listRecords(store, 'app.bsky.feed.post', { did: DID2, limit: 2, cursor: p2.cursor }) |
| 128 | t.is(p3.records.length, 1, 'the final page is short') |
| 129 | t.absent(p3.cursor, 'and carries NO cursor — its absence is the stop signal, and returning one is what makes a pager loop') |
| 130 | |
| 131 | const rev = await listRecords(store, 'app.bsky.feed.post', { did: DID2, limit: 2, reverse: true }) |
| 132 | t.ok(rev.records[0].uri.endsWith('eee'), 'reverse gives DESCENDING order (newest first for TID rkeys)') |
| 133 | |
| 134 | // The lexicon caps limit at 100; a caller asking for more must not walk an unbounded collection. |
| 135 | const capped = await listRecords(store, 'app.bsky.feed.post', { did: DID2, limit: 9999 }) |
| 136 | t.is(capped.records.length, 5, 'an over-large limit is clamped, not obeyed') |
| 137 | }) |