Files
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

140 lines
4.3 KiB
TypeScript

import Database from "better-sqlite3";
import { Kysely, SqliteDialect } from "kysely";
import { ulid } from "ulidx";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runMigrations } from "../../../src/database/migrations/runner.js";
import type { Database as DatabaseSchema } from "../../../src/database/types.js";
import { SchemaRegistry } from "../../../src/schema/registry.js";
// Mock loader.getDb so the runtime menu functions read from our test db.
vi.mock("../../../src/loader.js", () => ({
getDb: vi.fn(),
}));
import { prefetchLayoutData } from "../../../src/astro/prefetch.js";
import { getDb } from "../../../src/loader.js";
import { getMenu } from "../../../src/menus/index.js";
import { runWithContext } from "../../../src/request-context.js";
/** SQL of every query executed against the test database. */
let queries: string[] = [];
function collectionPatternQueries(): string[] {
return queries.filter((q) => q.includes("_emdash_collections") && q.includes("url_pattern"));
}
describe("getMenu collection-pattern request cache", () => {
let db: Kysely<DatabaseSchema>;
beforeEach(async () => {
queries = [];
db = new Kysely<DatabaseSchema>({
dialect: new SqliteDialect({ database: new Database(":memory:") }),
log(event) {
if (event.level === "query") queries.push(event.query.sql);
},
});
await runMigrations(db);
// A "post" collection with a url_pattern, plus one published entry
// that menu items can reference.
const registry = new SchemaRegistry(db);
await registry.createCollection({
slug: "post",
label: "Posts",
labelSingular: "Post",
});
await db
.updateTable("_emdash_collections")
.set({ url_pattern: "/blog/{slug}" })
.where("slug", "=", "post")
.execute();
const entryId = ulid();
await db
.insertInto("ec_post" as never)
.values({
id: entryId,
slug: "hello",
status: "published",
locale: "en",
translation_group: entryId,
} as never)
.execute();
// Two menus that both reference the same collection.
for (const name of ["primary", "footer"]) {
const menuId = ulid();
await db.insertInto("_emdash_menus").values({ id: menuId, name, label: name }).execute();
await db
.insertInto("_emdash_menu_items")
.values({
id: ulid(),
menu_id: menuId,
sort_order: 0,
type: "post",
reference_collection: "post",
reference_id: entryId,
label: "Hello",
})
.execute();
}
vi.mocked(getDb).mockResolvedValue(db);
});
afterEach(async () => {
await db.destroy();
vi.restoreAllMocks();
});
it("queries collection url_patterns once for multiple menus in one request", async () => {
queries = [];
const [primary, footer] = await runWithContext({ editMode: false }, async () => {
const first = await getMenu("primary");
const second = await getMenu("footer");
return [first, second];
});
expect(primary?.items.map((i) => i.url)).toEqual(["/blog/hello"]);
expect(footer?.items.map((i) => i.url)).toEqual(["/blog/hello"]);
expect(collectionPatternQueries()).toHaveLength(1);
});
it("returns identical results with and without a request context", async () => {
const cached = await runWithContext({ editMode: false }, () => getMenu("primary"));
const uncached = await getMenu("primary");
expect(cached).toEqual(uncached);
});
it("does not leak the cached patterns across requests", async () => {
await runWithContext({ editMode: false }, () => getMenu("primary"));
queries = [];
await runWithContext({ editMode: false }, () => getMenu("footer"));
expect(collectionPatternQueries()).toHaveLength(1);
});
it("prefetchLayoutData warms menus so layout getMenu calls hit the cache", async () => {
await runWithContext({ editMode: false }, async () => {
// Eager prefetch discovers every menu name and warms them up front.
await prefetchLayoutData();
// Anything the layout reads afterwards must be served from the
// request cache the prefetch populated -- zero further queries.
queries = [];
const primary = await getMenu("primary");
const footer = await getMenu("footer");
expect(primary?.items.map((i) => i.url)).toEqual(["/blog/hello"]);
expect(footer?.items.map((i) => i.url)).toEqual(["/blog/hello"]);
expect(queries).toHaveLength(0);
});
});
});