Files
emdash-cms--emdash/packages/core/tests/unit/query-collection-bucketing.test.ts
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

193 lines
5.8 KiB
TypeScript

import type { Kysely } from "kysely";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { handleContentCreate } from "../../src/api/index.js";
import type { Database } from "../../src/database/types.js";
import { emdashLoader } from "../../src/loader.js";
import { bucketFilter, getEmDashEntry, sliceCollectionResult } from "../../src/query.js";
import { runWithContext } from "../../src/request-context.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../utils/test-db.js";
vi.mock("astro:content", () => ({
getLiveEntry: vi.fn(),
}));
import { getLiveEntry } from "astro:content";
describe("getEmDashCollection limit bucketing", () => {
let db: Kysely<Database>;
beforeEach(async () => {
db = await setupTestDatabaseWithCollections();
});
afterEach(async () => {
await teardownTestDatabase(db);
});
async function createPublishedPost(title: string) {
const result = await handleContentCreate(db, "post", {
data: { title },
status: "published",
});
if (!result.success) throw new Error("Failed to create post");
return result.data!.item;
}
it("a sliced bucket fetch produces the same entries and nextCursor as a direct loader call at the same limit", async () => {
for (let i = 1; i <= 7; i++) await createPublishedPost(`Post ${i}`);
const loader = emdashLoader();
const direct = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post", limit: 4 } }),
);
expect(direct.entries).toHaveLength(4);
expect(direct.nextCursor).toBeTruthy();
const bucketed = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post", limit: 10 } }),
);
expect(bucketed.entries).toHaveLength(7);
const sliced = sliceCollectionResult(bucketed, 4, undefined);
expect(sliced.entries.map((e) => e.id)).toEqual(direct.entries.map((e) => e.id));
expect(sliced.nextCursor).toBe(direct.nextCursor);
});
it("bucketFilter raises small limits and leaves large ones alone", () => {
expect(bucketFilter(undefined)).toEqual({ fetchFilter: undefined, requestedLimit: undefined });
expect(bucketFilter({ limit: 4 })).toEqual({ fetchFilter: { limit: 10 }, requestedLimit: 4 });
expect(bucketFilter({ limit: 9 })).toEqual({ fetchFilter: { limit: 10 }, requestedLimit: 9 });
expect(bucketFilter({ limit: 10 })).toEqual({
fetchFilter: { limit: 10 },
requestedLimit: undefined,
});
expect(bucketFilter({ limit: 50 })).toEqual({
fetchFilter: { limit: 50 },
requestedLimit: undefined,
});
// cursor-paginated calls bypass bucketing — pagination contract requires honouring the limit
expect(bucketFilter({ limit: 4, cursor: "abc" })).toEqual({
fetchFilter: { limit: 4, cursor: "abc" },
requestedLimit: undefined,
});
// offset-paginated calls bypass bucketing for the same reason
expect(bucketFilter({ limit: 4, offset: 8 })).toEqual({
fetchFilter: { limit: 4, offset: 8 },
requestedLimit: undefined,
});
});
it("paginating from a slice-produced cursor returns the correct next page", async () => {
for (let i = 1; i <= 7; i++) await createPublishedPost(`Post ${i}`);
const loader = emdashLoader();
const bucketed = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post", limit: 10 } }),
);
const firstPage = sliceCollectionResult(bucketed, 4, undefined);
expect(firstPage.entries).toHaveLength(4);
expect(firstPage.nextCursor).toBeTruthy();
const secondPage = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({
filter: { type: "post", limit: 4, cursor: firstPage.nextCursor },
}),
);
expect(secondPage.entries).toHaveLength(3);
const allIds = [...firstPage.entries, ...secondPage.entries].map((e) => e.id);
expect(new Set(allIds).size).toBe(7);
});
});
describe("getEmDashEntry scheduled visibility", () => {
let db: Kysely<Database>;
beforeEach(async () => {
db = await setupTestDatabaseWithCollections();
});
afterEach(async () => {
await teardownTestDatabase(db);
vi.mocked(getLiveEntry).mockReset();
});
function mockEntry(data: Record<string, unknown>) {
vi.mocked(getLiveEntry).mockResolvedValue({
entry: {
id: "scheduled-post",
data: {
id: "db-id-1",
title: "Scheduled post",
...data,
},
},
error: undefined,
cacheHint: {},
} as any);
}
async function loadEntry() {
return runWithContext({ editMode: false, db }, () => getEmDashEntry("post", "scheduled-post"));
}
it("returns scheduled entries whose Date scheduledAt has passed", async () => {
mockEntry({
status: "scheduled",
scheduledAt: new Date("2000-01-01T00:00:00.000Z"),
});
const { entry } = await loadEntry();
expect(entry?.id).toBe("scheduled-post");
});
it("keeps scheduled entries with a future Date scheduledAt hidden", async () => {
mockEntry({
status: "scheduled",
scheduledAt: new Date("2999-01-01T00:00:00.000Z"),
});
const { entry } = await loadEntry();
expect(entry).toBeNull();
});
it("returns scheduled entries whose string scheduledAt has passed", async () => {
mockEntry({
status: "scheduled",
scheduledAt: "2000-01-01T00:00:00.000Z",
});
const { entry } = await loadEntry();
expect(entry?.id).toBe("scheduled-post");
});
it("returns published entries regardless of scheduledAt", async () => {
mockEntry({
status: "published",
scheduledAt: new Date("2999-01-01T00:00:00.000Z"),
});
const { entry } = await loadEntry();
expect(entry?.id).toBe("scheduled-post");
});
it.each([
["missing", {}],
["invalid", { scheduledAt: "not-a-date" }],
])("keeps scheduled entries with %s scheduledAt hidden", async (_label, scheduledFields) => {
mockEntry({
status: "scheduled",
...scheduledFields,
});
const { entry } = await loadEntry();
expect(entry).toBeNull();
});
});