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

356 lines
10 KiB
TypeScript

import { sql } from "kysely";
import { afterEach, beforeEach, expect, it } from "vitest";
import { rewriteUrls } from "../../../src/astro/routes/api/import/wordpress/rewrite-urls.js";
import { ContentRepository } from "../../../src/database/repositories/content.js";
import { MediaUsageRepository } from "../../../src/database/repositories/media-usage.js";
import {
CONTENT_MEDIA_USAGE_ADAPTER_ID,
CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
markContentMediaUsageCollectionStaleSafely,
} from "../../../src/media/usage/content-refresh.js";
import {
buildContentMediaUsageSourceKey,
type MediaUsageContentSourceVariant,
} from "../../../src/media/usage/source-key.js";
import { createContentAccessWithWrite } from "../../../src/plugins/context.js";
import { SchemaRegistry } from "../../../src/schema/registry.js";
import { applySeed } from "../../../src/seed/apply.js";
import type { SeedFile } from "../../../src/seed/types.js";
import {
describeEachDialect,
setupForDialect,
teardownForDialect,
type DialectTestContext,
} from "../../utils/test-db.js";
describeEachDialect("media usage stale marking for bypass writes", (dialect) => {
let ctx: DialectTestContext;
let registry: SchemaRegistry;
let usageRepo: MediaUsageRepository;
beforeEach(async () => {
ctx = await setupForDialect(dialect);
registry = new SchemaRegistry(ctx.db);
usageRepo = new MediaUsageRepository(ctx.db);
await createCollectionWithFields("posts");
await createCollectionWithFields("pages");
});
afterEach(async () => {
await teardownForDialect(ctx);
});
it("marks touched collections stale after seed content writes", async () => {
await markComplete("posts");
const seed: SeedFile = {
version: "1",
content: {
posts: [
{
id: "seed-post",
slug: "seed-post",
data: {
title: "Seed Post",
hero: mediaRef("media-seed"),
},
},
],
},
};
await applySeed(ctx.db, seed, { includeContent: true });
await expectCollectionStatus("posts", "stale");
});
it("marks seed-touched collections stale even when a later content entry fails", async () => {
await markComplete("posts");
const seed: SeedFile = {
version: "1",
content: {
posts: [
{
id: "seed-created-before-failure",
slug: "duplicate-seed-slug",
data: { title: "Created Before Failure" },
},
{
id: "seed-conflict",
slug: "duplicate-seed-slug",
data: { title: "Conflict" },
},
],
},
};
await expect(
applySeed(ctx.db, seed, { includeContent: true, onConflict: "error" }),
).rejects.toThrow(/Conflict: content/);
await expectCollectionStatus("posts", "stale");
});
it("marks collections stale after plugin content direct writes", async () => {
await markComplete("posts");
const content = createContentAccessWithWrite(ctx.db);
await content.create("posts", {
title: "Plugin Post",
hero: mediaRef("media-plugin-create"),
});
await expectCollectionStatus("posts", "stale");
await markComplete("posts");
const repo = new ContentRepository(ctx.db);
const item = await repo.create({
type: "posts",
slug: "plugin-update",
data: { title: "Plugin Update", hero: mediaRef("media-plugin-old") },
});
await content.update("posts", item.id, { hero: mediaRef("media-plugin-new") });
await expectCollectionStatus("posts", "stale");
await markComplete("posts");
expect(await content.delete("posts", item.id)).toBe(true);
await expectCollectionStatus("posts", "stale");
});
it("marks collections stale after schema field mutations", async () => {
await markComplete("posts");
await registry.createField("posts", { slug: "deck", label: "Deck", type: "string" });
await expectCollectionStatus("posts", "stale");
await markComplete("posts");
await registry.updateField("posts", "hero", { label: "Hero Image" });
await expectCollectionStatus("posts", "stale");
await markComplete("posts");
await registry.deleteField("posts", "deck");
await expectCollectionStatus("posts", "stale");
});
it("marks registered orphaned tables stale", async () => {
await sql`CREATE TABLE ec_orphan_posts (id text primary key)`.execute(ctx.db);
await registry.registerOrphanedTable("orphan_posts");
await expectCollectionStatus("orphan_posts", "stale");
});
it("deletes collection usage sources after collection deletion", async () => {
await markComplete("posts");
await usageRepo.replaceSource(contentSource("posts", "entry-1", "columns"), [
occurrence("hero", "media-collection-delete"),
]);
expect(await usageRepo.findSource(sourceKey("posts", "entry-1", "columns"))).not.toBeNull();
await registry.deleteCollection("posts", { force: true });
expect(await usageRepo.findSource(sourceKey("posts", "entry-1", "columns"))).toBeNull();
expect(await usageRepo.findCurrentUsageByMediaId("media-collection-delete")).toEqual([]);
expect(await findCollectionStatus("posts")).toBeNull();
});
it("retries failed WordPress rewrite stale marks once after the rewrite pass", async () => {
const repo = new ContentRepository(ctx.db);
const oldUrl = "https://example.com/wp-content/uploads/2026/01/hero.jpg";
await repo.create({
type: "posts",
slug: "rewrite-retry-post",
data: { title: "Rewrite Retry Post", body: `<img src="${oldUrl}">` },
});
await markComplete("posts");
let attempts = 0;
const result = await rewriteUrls(
ctx.db,
{ [oldUrl]: "/_emdash/media/file/imported/hero.jpg" },
() => undefined,
["posts"],
async (db, collectionSlug, lastErrorCode) => {
attempts++;
if (attempts === 1) return false;
return markContentMediaUsageCollectionStaleSafely(db, collectionSlug, lastErrorCode);
},
);
expect(result.byCollection).toEqual({ posts: 1 });
expect(attempts).toBe(2);
await expectCollectionStatus("posts", "stale");
});
it("marks only rewritten WordPress URL collections stale", async () => {
const repo = new ContentRepository(ctx.db);
const oldUrl = "https://example.com/wp-content/uploads/2026/01/hero.jpg";
await repo.create({
type: "posts",
slug: "rewrite-post",
data: { title: "Rewrite Post", body: `<img src="${oldUrl}">` },
});
await repo.create({
type: "pages",
slug: "clean-page",
data: { title: "Clean Page", body: "No matching media URL" },
});
await markComplete("posts");
await markComplete("pages");
const result = await rewriteUrls(
ctx.db,
{ [oldUrl]: "/_emdash/media/file/imported/hero.jpg" },
() => undefined,
);
expect(result.byCollection).toEqual({ posts: 1 });
await expectCollectionStatus("posts", "stale");
await expectCollectionStatus("pages", "complete");
});
it("marks earlier WordPress rewrite collections stale when a later collection fails", async () => {
const repo = new ContentRepository(ctx.db);
const oldUrl = "https://example.com/wp-content/uploads/2026/01/hero.jpg";
await repo.create({
type: "posts",
slug: "rewrite-before-error",
data: { title: "Rewrite Before Error", body: `<img src="${oldUrl}">` },
});
await registry.createCollection({ slug: "zz_broken", label: "Broken" });
const broken = await registry.getCollection("zz_broken");
expect(broken).not.toBeNull();
await ctx.db
.insertInto("_emdash_fields")
.values({
id: "broken_field",
collection_id: broken!.id,
slug: "bad_repeater",
label: "Bad Repeater",
type: "repeater",
column_type: "JSON",
required: 0,
unique: 0,
default_value: null,
validation: "{",
widget: null,
options: null,
sort_order: 0,
searchable: 0,
translatable: 1,
})
.execute();
await markComplete("posts");
let staleMarkAttempts = 0;
await expect(
rewriteUrls(
ctx.db,
{ [oldUrl]: "/_emdash/media/file/imported/hero.jpg" },
() => undefined,
["posts", "zz_broken"],
async (db, collectionSlug, lastErrorCode) => {
if (collectionSlug !== "posts") {
return markContentMediaUsageCollectionStaleSafely(db, collectionSlug, lastErrorCode);
}
staleMarkAttempts++;
if (staleMarkAttempts === 1) return false;
return markContentMediaUsageCollectionStaleSafely(db, collectionSlug, lastErrorCode);
},
),
).rejects.toThrow();
expect(staleMarkAttempts).toBe(2);
await expectCollectionStatus("posts", "stale");
});
async function createCollectionWithFields(slug: string) {
await registry.createCollection({ slug, label: slug });
await registry.createField(slug, { slug: "title", label: "Title", type: "string" });
await registry.createField(slug, { slug: "body", label: "Body", type: "text" });
await registry.createField(slug, { slug: "hero", label: "Hero", type: "image" });
}
async function markComplete(collectionSlug: string) {
await usageRepo.upsertIndexStatus({
adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
scopeKey: collectionSlug,
status: "complete",
schemaVersion: 1,
indexedSourceCount: 1,
failedSourceCount: 0,
});
}
async function expectCollectionStatus(collectionSlug: string, status: string) {
await expect(findCollectionStatus(collectionSlug)).resolves.toEqual(
expect.objectContaining({ status }),
);
}
async function findCollectionStatus(collectionSlug: string) {
return usageRepo.findIndexStatus({
adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
scopeKey: collectionSlug,
});
}
});
function mediaRef(id: string): Record<string, unknown> {
return {
id,
provider: "local",
mimeType: "image/webp",
width: 100,
height: 100,
};
}
function sourceKey(
collectionSlug: string,
contentId: string,
sourceVariant: MediaUsageContentSourceVariant,
): string {
return buildContentMediaUsageSourceKey({ collectionSlug, contentId, sourceVariant });
}
function contentSource(
collectionSlug: string,
contentId: string,
sourceVariant: MediaUsageContentSourceVariant,
) {
return {
sourceKey: sourceKey(collectionSlug, contentId, sourceVariant),
sourceType: "content",
collectionSlug,
contentId,
sourceVariant,
contentSlug: "hello-world",
contentTitle: "Hello World",
contentStatus: "published",
schemaVersion: 1,
sourceCompleteness: "complete" as const,
};
}
function occurrence(fieldSlug: string, mediaId: string) {
return {
fieldSlug,
fieldPath: fieldSlug,
referenceType: "image_field" as const,
mediaId,
provider: "local",
providerAssetId: mediaId,
mediaKind: "image" as const,
mimeType: "image/webp",
};
}