Files
ocas/packages/core/src/list-pagination.test.ts
T
xiaoju 190ae672a7 refactor: remove DEFAULT_LIST_LIMIT from core, add changeset
- Core layer: limit=undefined means no limit (return all)
- CLI layer: default limit 100 in parseListOptions()
- Remove Number.MAX_SAFE_INTEGER sentinel usages
- Add changeset for breaking list return type change

Addresses review feedback from 小墨
2026-06-01 15:08:44 +00:00

194 lines
6.7 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { BOOTSTRAP_STORE } from "./bootstrap-capable.js";
import { createMemoryStore } from "./store.js";
const HASH_RE = /^[0-9A-HJKMNP-TV-Z]{13}$/;
async function putN(
store: ReturnType<typeof createMemoryStore>,
type: string,
n: number,
delayMs = 2,
): Promise<string[]> {
const hashes: string[] = [];
for (let i = 0; i < n; i++) {
hashes.push(await store.put(type, { i }));
if (delayMs > 0 && i < n - 1) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
return hashes;
}
describe("listByType - pagination + sort + timestamps", () => {
test("A1. returns objects with hash/created/updated", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 3, 0);
const list = store.listByType(m);
for (const e of list) {
expect(e.hash).toMatch(HASH_RE);
expect(typeof e.created).toBe("number");
expect(typeof e.updated).toBe("number");
expect(e.created).toBe(e.updated);
}
});
test("A2. default sort is created ASC", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 4);
const list = store.listByType(m);
for (let i = 1; i < list.length; i++) {
expect((list[i] as { created: number }).created).toBeGreaterThanOrEqual(
(list[i - 1] as { created: number }).created,
);
}
});
test("A3. desc:true reverses order", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 4);
const list = store.listByType(m, { desc: true });
for (let i = 1; i < list.length; i++) {
expect((list[i] as { created: number }).created).toBeLessThanOrEqual(
(list[i - 1] as { created: number }).created,
);
}
});
test("A4. sort: 'updated' is equivalent to 'created' for CAS nodes", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 4);
const a = store.listByType(m, { sort: "created" });
const b = store.listByType(m, { sort: "updated" });
expect(a).toEqual(b);
});
test("A5. limit truncates", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 5, 0);
expect(store.listByType(m, { limit: 2 })).toHaveLength(2);
});
test("A6. offset skips", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 5);
const all = store.listByType(m);
const skip = store.listByType(m, { offset: 2, limit: 10 });
expect(skip).toHaveLength(all.length - 2);
expect(skip[0]).toEqual(all[2] as (typeof all)[number]);
});
test("A7. limit:0 returns empty array", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 3, 0);
expect(store.listByType(m, { limit: 0 })).toEqual([]);
});
test("A8. offset past end returns empty array", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 3, 0);
expect(store.listByType(m, { offset: 100 })).toEqual([]);
});
test("A9. core has no default limit (returns all)", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 150, 0);
// No CLI-layer cap; with 150 nodes of type m (plus m itself which is
// self-typed), the full set is returned.
expect(store.listByType(m)).toHaveLength(151);
});
test("A10. desc + offset + limit combined", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await putN(store, m, 5, 15);
const all = store.listByType(m);
const got = store.listByType(m, { desc: true, offset: 1, limit: 2 });
expect(got).toHaveLength(2);
// desc order is reverse of `all`; offset 1 + limit 2 → all[n-2], all[n-3]
const n = all.length;
expect(got[0]?.hash).toBe(all[n - 2]?.hash as string);
expect(got[1]?.hash).toBe(all[n - 3]?.hash as string);
});
});
describe("listMeta / listSchemas - pagination", () => {
test("B1. listMeta returns {hash,created,updated}", async () => {
const store = createMemoryStore();
const h = await store[BOOTSTRAP_STORE]({ type: "object" });
const list = store.listMeta();
expect(list).toHaveLength(1);
const e = list[0] as { hash: string; created: number; updated: number };
expect(e.hash).toBe(h);
expect(typeof e.created).toBe("number");
expect(typeof e.updated).toBe("number");
});
test("B2. listMeta has no default limit (returns all)", async () => {
const store = createMemoryStore();
for (let i = 0; i < 150; i++) {
await store[BOOTSTRAP_STORE]({ type: "object", i });
}
expect(store.listMeta()).toHaveLength(150);
});
test("B3. listMeta limit/offset/desc", async () => {
const store = createMemoryStore();
for (let i = 0; i < 5; i++) {
await store[BOOTSTRAP_STORE]({ type: "object", i });
await new Promise((r) => setTimeout(r, 2));
}
expect(store.listMeta({ limit: 2 })).toHaveLength(2);
const all = store.listMeta();
const desc = store.listMeta({ desc: true });
expect(desc[0]).toEqual(all[all.length - 1] as (typeof all)[number]);
});
test("B4. listSchemas returns objects, supports limit", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
await store.put(m, { type: "string" });
await store.put(m, { type: "number" });
const list = store.listSchemas();
for (const e of list) {
expect(e.hash).toMatch(HASH_RE);
expect(typeof e.created).toBe("number");
}
expect(store.listSchemas({ limit: 1 })).toHaveLength(1);
});
});
describe("Determinism / edge cases", () => {
test("I1. same-ms timestamps yield deterministic ordering across calls", async () => {
const store = createMemoryStore();
const m = await store[BOOTSTRAP_STORE]({ type: "object" });
// No delay → likely same millisecond
await putN(store, m, 5, 0);
const a = store.listByType(m);
const b = store.listByType(m);
expect(b).toEqual(a);
});
test("I2. empty store returns []", () => {
const store = createMemoryStore();
expect(store.listByType("0000000000000")).toEqual([]);
expect(store.listMeta()).toEqual([]);
expect(store.listSchemas()).toEqual([]);
});
});