@@ -5,7 +5,13 @@ import {
55 getDefaultCursorStore ,
66 buildCursorKey ,
77} from "../src/cursorTracker.js" ;
8- import { paginate , collectAll , HorizonPaginator } from "../src/horizonPaginator.js" ;
8+ import {
9+ paginate ,
10+ collectAll ,
11+ HorizonPaginator ,
12+ paginateArray ,
13+ } from "../src/horizonPaginator.js" ;
14+ import { SdkError } from "../src/errors.js" ;
915import type { CollectionPage } from "../src/types.js" ;
1016
1117describe ( "InMemoryCursorStore" , ( ) => {
@@ -249,3 +255,112 @@ describe("paginate – page-size negotiation integration (#692)", () => {
249255 expect ( results ) . toHaveLength ( 8 ) ;
250256 } ) ;
251257} ) ;
258+
259+ // ─────────────────────────────────────────────────────────────────────────────
260+ // #618 — paginateArray (local array pagination)
261+ // Spec + resolved anomalies: RESOLUCION_ANOMALIAS_618.md (experimento_autonomia).
262+
263+ describe ( "paginateArray (#618)" , ( ) => {
264+ // 1..10
265+ const TEN = Array . from ( { length : 10 } , ( _ , i ) => i + 1 ) ;
266+
267+ // ---- Spec: first page / last page / ranges ----
268+ it ( "returns the first page (1-indexed)" , ( ) => {
269+ const r = paginateArray ( TEN , { page : 1 , pageSize : 5 } ) ;
270+ expect ( r . data ) . toEqual ( [ 1 , 2 , 3 , 4 , 5 ] ) ;
271+ expect ( r . total ) . toBe ( 10 ) ;
272+ expect ( r . totalPages ) . toBe ( 2 ) ;
273+ expect ( r . hasNext ) . toBe ( true ) ;
274+ expect ( r . hasPrev ) . toBe ( false ) ;
275+ } ) ;
276+
277+ it ( "returns the last page" , ( ) => {
278+ const r = paginateArray ( TEN , { page : 2 , pageSize : 5 } ) ;
279+ expect ( r . data ) . toEqual ( [ 6 , 7 , 8 , 9 , 10 ] ) ;
280+ expect ( r . hasNext ) . toBe ( false ) ;
281+ expect ( r . hasPrev ) . toBe ( true ) ;
282+ } ) ;
283+
284+ it ( "handles a page smaller than pageSize on the last page" , ( ) => {
285+ const r = paginateArray ( [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] , { page : 2 , pageSize : 5 } ) ;
286+ expect ( r . data ) . toEqual ( [ 6 , 7 ] ) ;
287+ expect ( r . totalPages ) . toBe ( 2 ) ;
288+ expect ( r . hasNext ) . toBe ( false ) ;
289+ expect ( r . hasPrev ) . toBe ( true ) ;
290+ } ) ;
291+
292+ // ---- Spec: out-of-range page → data: [], no error ----
293+ it ( "returns data: [] for a page beyond totalPages" , ( ) => {
294+ const r = paginateArray ( TEN , { page : 99 , pageSize : 5 } ) ;
295+ expect ( r . data ) . toEqual ( [ ] ) ;
296+ expect ( r . total ) . toBe ( 10 ) ;
297+ expect ( r . totalPages ) . toBe ( 2 ) ;
298+ expect ( r . hasNext ) . toBe ( false ) ;
299+ expect ( r . hasPrev ) . toBe ( false ) ;
300+ } ) ;
301+
302+ it ( "returns data: [] for page 0 (1-indexed invariant, anomaly resolved)" , ( ) => {
303+ const r = paginateArray ( TEN , { page : 0 , pageSize : 5 } ) ;
304+ expect ( r . data ) . toEqual ( [ ] ) ;
305+ expect ( r . totalPages ) . toBe ( 2 ) ;
306+ expect ( r . hasNext ) . toBe ( false ) ;
307+ expect ( r . hasPrev ) . toBe ( false ) ;
308+ } ) ;
309+
310+ it ( "returns data: [] for a negative page (anomaly resolved)" , ( ) => {
311+ const r = paginateArray ( TEN , { page : - 3 , pageSize : 5 } ) ;
312+ expect ( r . data ) . toEqual ( [ ] ) ;
313+ } ) ;
314+
315+ // ---- Spec: pageSize bounds → throw SdkError ----
316+ it ( "throws SdkError for pageSize 0" , ( ) => {
317+ expect ( ( ) => paginateArray ( TEN , { page : 1 , pageSize : 0 } ) )
318+ . toThrowError ( SdkError ) ;
319+ } ) ;
320+
321+ it ( "throws SdkError for pageSize 201" , ( ) => {
322+ expect ( ( ) => paginateArray ( TEN , { page : 1 , pageSize : 201 } ) )
323+ . toThrowError ( SdkError ) ;
324+ } ) ;
325+
326+ it ( "throws SdkError with INVALID_RECIPIENT code (enum-closed decision)" , ( ) => {
327+ try {
328+ paginateArray ( TEN , { page : 1 , pageSize : 0 } ) ;
329+ expect . unreachable ( "should have thrown" ) ;
330+ } catch ( err ) {
331+ expect ( err ) . toBeInstanceOf ( SdkError ) ;
332+ expect ( ( err as SdkError ) . code ) . toBe ( "INVALID_RECIPIENT" ) ;
333+ }
334+ } ) ;
335+
336+ it ( "accepts pageSize 200 (upper bound inclusive)" , ( ) => {
337+ const r = paginateArray ( TEN , { page : 1 , pageSize : 200 } ) ;
338+ expect ( r . data ) . toEqual ( TEN ) ;
339+ expect ( r . totalPages ) . toBe ( 1 ) ;
340+ expect ( r . hasNext ) . toBe ( false ) ;
341+ } ) ;
342+
343+ // ---- Anomaly: empty array ----
344+ it ( "returns totalPages 0 and data [] for an empty array (anomaly resolved)" , ( ) => {
345+ const r = paginateArray ( [ ] , { page : 1 , pageSize : 5 } ) ;
346+ expect ( r . data ) . toEqual ( [ ] ) ;
347+ expect ( r . total ) . toBe ( 0 ) ;
348+ expect ( r . totalPages ) . toBe ( 0 ) ;
349+ expect ( r . hasNext ) . toBe ( false ) ;
350+ expect ( r . hasPrev ) . toBe ( false ) ;
351+ } ) ;
352+
353+ // ---- Purity: does not mutate input ----
354+ it ( "does not mutate the input array" , ( ) => {
355+ const src = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] ;
356+ const snapshot = [ ...src ] ;
357+ paginateArray ( src , { page : 2 , pageSize : 3 } ) ;
358+ expect ( src ) . toEqual ( snapshot ) ;
359+ } ) ;
360+
361+ // ---- Non-integer page/pageSize ----
362+ it ( "treats a non-integer page as out-of-range (data: [])" , ( ) => {
363+ const r = paginateArray ( TEN , { page : 1.5 , pageSize : 5 } ) ;
364+ expect ( r . data ) . toEqual ( [ ] ) ;
365+ } ) ;
366+ } ) ;
0 commit comments