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

129 lines
4.3 KiB
TypeScript

import type { Kysely } from "kysely";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { handleContentCreate } from "../../src/api/index.js";
import type { Database } from "../../src/database/types.js";
import { emdashLoader } from "../../src/loader.js";
import { runWithContext } from "../../src/request-context.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../utils/test-db.js";
describe("Cache hints", () => {
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;
}
describe("loadCollection cacheHint", () => {
it("should tag collection with type name", async () => {
await createPublishedPost("First Post");
await createPublishedPost("Second Post");
const loader = emdashLoader();
const result = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post" } }),
);
expect(result.cacheHint).toBeDefined();
expect(result.cacheHint!.tags).toEqual(["post"]);
});
it("should include lastModified from most recent entry", async () => {
await createPublishedPost("First Post");
const second = await createPublishedPost("Second Post");
const loader = emdashLoader();
const result = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post" } }),
);
expect(result.cacheHint!.lastModified).toBeInstanceOf(Date);
// lastModified should be >= the second post's updated_at
const secondUpdated = new Date(second.updatedAt);
expect(result.cacheHint!.lastModified!.getTime()).toBeGreaterThanOrEqual(
secondUpdated.getTime(),
);
});
});
describe("entry-level cacheHint", () => {
it("should tag each entry with its database ID", async () => {
const post = await createPublishedPost("Test Post");
const loader = emdashLoader();
const result = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post" } }),
);
expect(result.entries).toHaveLength(1);
const entry = result.entries![0];
expect(entry.cacheHint).toBeDefined();
expect(entry.cacheHint!.tags).toEqual([post.id]);
});
it("should include lastModified on each entry", async () => {
await createPublishedPost("Test Post");
const loader = emdashLoader();
const result = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post" } }),
);
const entry = result.entries![0];
expect(entry.cacheHint!.lastModified).toBeInstanceOf(Date);
});
});
describe("loadEntry cacheHint", () => {
it("should tag entry with its database ID", async () => {
const post = await createPublishedPost("Test Post");
const loader = emdashLoader();
const result = await runWithContext({ editMode: false, db }, () =>
loader.loadEntry!({ filter: { type: "post", id: post.slug } }),
);
// loadEntry returns the entry directly (LiveDataEntry), not { entry, cacheHint }
expect(result).toBeDefined();
expect(result!.cacheHint).toBeDefined();
expect(result!.cacheHint!.tags).toEqual([post.id]);
});
});
describe("invalidation tag alignment", () => {
it("should produce tags that match the invalidation pattern", async () => {
const post = await createPublishedPost("Test Post");
const loader = emdashLoader();
const collectionResult = await runWithContext({ editMode: false, db }, () =>
loader.loadCollection!({ filter: { type: "post" } }),
);
// The route invalidates with tags: [collection, id]
// Collection pages are tagged with [type] -> matches "collection" tag
// Entry pages are tagged with [entryId] -> matches "id" tag
const invalidationTags = ["post", post.id];
// Collection-level tag should be hit by invalidation
expect(invalidationTags).toContain(collectionResult.cacheHint!.tags![0]);
// Entry-level tag should be hit by invalidation
const entry = collectionResult.entries![0];
expect(invalidationTags).toContain(entry.cacheHint!.tags![0]);
});
});
});