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

372 lines
10 KiB
TypeScript

import { sql } from "kysely";
import { afterEach, beforeEach, expect, it } from "vitest";
import type { Database } from "../../../src/database/types.js";
import {
describeEachDialect,
setupForDialect,
teardownForDialect,
type DialectTestContext,
} from "../../utils/test-db.js";
const EXPECTED_SOURCE_COLUMNS = [
"source_updated_at",
"source_version",
"source_fingerprint",
"source_completeness",
"last_attempted_at",
"last_error_code",
] as const;
const EXPECTED_STATUS_COLUMNS = [
"adapter_id",
"scope_type",
"scope_key",
"status",
"schema_version",
"started_at",
"completed_at",
"cursor",
"indexed_source_count",
"failed_source_count",
"last_error_code",
"updated_at",
] as const;
const EXPECTED_INDEXES = [
"idx__emdash_media_usage_sources_completeness",
"idx__emdash_media_usage_sources_fingerprint",
"idx__emdash_media_usage_index_status_status",
] as const;
describeEachDialect("media usage index status migration", (dialect) => {
let ctx: DialectTestContext;
beforeEach(async () => {
ctx = await setupForDialect(dialect);
});
afterEach(async () => {
await teardownForDialect(ctx);
});
it("is registered and creates metadata columns plus status table", async () => {
const migrations = await ctx.db.selectFrom("_emdash_migrations").select("name").execute();
expect(migrations.map((row) => row.name)).toContain("050_media_usage_index_status");
const sourceColumns = await listColumnNames(ctx, "_emdash_media_usage_sources");
for (const columnName of EXPECTED_SOURCE_COLUMNS) {
expect(sourceColumns.has(columnName), `missing source column ${columnName}`).toBe(true);
}
const statusColumns = await listColumnNames(ctx, "_emdash_media_usage_index_status");
for (const columnName of EXPECTED_STATUS_COLUMNS) {
expect(statusColumns.has(columnName), `missing status column ${columnName}`).toBe(true);
}
});
it("applies defaults for source completeness and status counters", async () => {
await ctx.db
.insertInto("_emdash_media_usage_sources")
.values({
source_key: "content:posts:entry1:columns",
source_type: "content",
collection_slug: "posts",
content_id: "entry1",
source_variant: "columns",
locale: "en",
translation_group: "tg1",
content_slug: "hello-world",
content_title: "Hello World",
content_status: "published",
content_scheduled_at: null,
content_deleted_at: null,
revision_id: "rev1",
current_generation: "gen1",
})
.execute();
const source = await ctx.db
.selectFrom("_emdash_media_usage_sources")
.select([
"source_completeness",
"source_updated_at",
"source_version",
"source_fingerprint",
"last_attempted_at",
"last_error_code",
])
.where("source_key", "=", "content:posts:entry1:columns")
.executeTakeFirstOrThrow();
expect(source).toEqual({
source_completeness: "unknown",
source_updated_at: null,
source_version: null,
source_fingerprint: null,
last_attempted_at: null,
last_error_code: null,
});
await ctx.db
.insertInto("_emdash_media_usage_index_status")
.values({
adapter_id: "content-media",
scope_type: "collection",
scope_key: "posts",
status: "never",
})
.execute();
const status = await ctx.db
.selectFrom("_emdash_media_usage_index_status")
.select([
"schema_version",
"indexed_source_count",
"failed_source_count",
"started_at",
"completed_at",
"cursor",
"last_error_code",
"updated_at",
])
.where("adapter_id", "=", "content-media")
.where("scope_type", "=", "collection")
.where("scope_key", "=", "posts")
.executeTakeFirstOrThrow();
expect(status).toEqual({
schema_version: 1,
indexed_source_count: 0,
failed_source_count: 0,
started_at: null,
completed_at: null,
cursor: null,
last_error_code: null,
updated_at: expect.any(String),
});
});
it("rejects duplicate status rows for the same adapter scope", async () => {
await ctx.db
.insertInto("_emdash_media_usage_index_status")
.values({
adapter_id: "content-media",
scope_type: "collection",
scope_key: "posts",
status: "running",
})
.execute();
await expect(
ctx.db
.insertInto("_emdash_media_usage_index_status")
.values({
adapter_id: "content-media",
scope_type: "collection",
scope_key: "posts",
status: "complete",
})
.execute(),
).rejects.toThrow();
});
it("adds source completeness default for existing PR 1 source rows", async () => {
const migration =
await import("../../../src/database/migrations/050_media_usage_index_status.js");
const sourceKey = "content:posts:pre047:columns";
await migration.down(ctx.db);
await ctx.db
.insertInto("_emdash_media_usage_sources")
.values({
source_key: sourceKey,
source_type: "content",
collection_slug: "posts",
content_id: "pre047",
source_variant: "columns",
locale: "en",
translation_group: "tg-pre047",
content_slug: "pre047",
content_title: "Pre 047",
content_status: "published",
content_scheduled_at: null,
content_deleted_at: null,
revision_id: "rev-pre047",
current_generation: "gen-pre047",
})
.execute();
await migration.up(ctx.db);
const source = await ctx.db
.selectFrom("_emdash_media_usage_sources")
.select([
"source_completeness",
"source_updated_at",
"source_version",
"source_fingerprint",
"last_attempted_at",
"last_error_code",
])
.where("source_key", "=", sourceKey)
.executeTakeFirstOrThrow();
expect(source).toEqual({
source_completeness: "unknown",
source_updated_at: null,
source_version: null,
source_fingerprint: null,
last_attempted_at: null,
last_error_code: null,
});
});
it("creates expected indexes", async () => {
const indexNames = await listIndexNames(ctx);
for (const indexName of EXPECTED_INDEXES) {
expect(indexNames.has(indexName), `missing index ${indexName}`).toBe(true);
}
});
it("up() can run again after registered migrations complete", async () => {
const migration =
await import("../../../src/database/migrations/050_media_usage_index_status.js");
const sourceKey = "content:posts:entry-preserve:columns";
const sourceMetadata = {
source_completeness: "complete",
source_updated_at: "2026-01-01T00:00:00.000Z",
source_version: 7,
source_fingerprint: "fingerprint-preserve",
last_attempted_at: "2026-01-01T00:00:01.000Z",
last_error_code: "PREVIOUS_ERROR",
};
await ctx.db
.insertInto("_emdash_media_usage_sources")
.values({
source_key: sourceKey,
source_type: "content",
collection_slug: "posts",
content_id: "entry-preserve",
source_variant: "columns",
locale: "en",
translation_group: "tg-preserve",
content_slug: "preserve",
content_title: "Preserve",
content_status: "published",
content_scheduled_at: null,
content_deleted_at: null,
revision_id: "rev-preserve",
current_generation: "gen-preserve",
...sourceMetadata,
})
.execute();
const statusMetadata = {
schema_version: 3,
started_at: "2026-01-02T00:00:00.000Z",
completed_at: "2026-01-02T00:00:10.000Z",
cursor: "cursor-preserve",
indexed_source_count: 12,
failed_source_count: 2,
last_error_code: "STATUS_ERROR",
updated_at: "2026-01-02T00:00:11.000Z",
};
await ctx.db
.insertInto("_emdash_media_usage_index_status")
.values({
adapter_id: "content-media",
scope_type: "collection",
scope_key: "posts",
status: "partial",
...statusMetadata,
})
.execute();
await migration.up(ctx.db);
const statusColumns = await listColumnNames(ctx, "_emdash_media_usage_index_status");
expect(statusColumns.has("adapter_id")).toBe(true);
const source = await ctx.db
.selectFrom("_emdash_media_usage_sources")
.select([
"source_completeness",
"source_updated_at",
"source_version",
"source_fingerprint",
"last_attempted_at",
"last_error_code",
])
.where("source_key", "=", sourceKey)
.executeTakeFirstOrThrow();
expect(source).toEqual(sourceMetadata);
const status = await ctx.db
.selectFrom("_emdash_media_usage_index_status")
.select([
"status",
"schema_version",
"started_at",
"completed_at",
"cursor",
"indexed_source_count",
"failed_source_count",
"last_error_code",
"updated_at",
])
.where("adapter_id", "=", "content-media")
.where("scope_type", "=", "collection")
.where("scope_key", "=", "posts")
.executeTakeFirstOrThrow();
expect(status).toEqual({ status: "partial", ...statusMetadata });
});
it("down() drops metadata and up() recreates it", async () => {
const migration =
await import("../../../src/database/migrations/050_media_usage_index_status.js");
await migration.down(ctx.db);
await expect(
sql`SELECT 1 FROM _emdash_media_usage_index_status`.execute(ctx.db),
).rejects.toThrow();
const sourceColumnsAfterDown = await listColumnNames(ctx, "_emdash_media_usage_sources");
expect(sourceColumnsAfterDown.has("source_completeness")).toBe(false);
await migration.up(ctx.db);
const statusColumns = await listColumnNames(ctx, "_emdash_media_usage_index_status");
expect(statusColumns.has("adapter_id")).toBe(true);
const sourceColumnsAfterUp = await listColumnNames(ctx, "_emdash_media_usage_sources");
expect(sourceColumnsAfterUp.has("source_completeness")).toBe(true);
});
});
async function listColumnNames(
ctx: DialectTestContext,
tableName: keyof Database,
): Promise<Set<string>> {
const tables = await ctx.db.introspection.getTables();
const table = tables.find((candidate) => candidate.name === tableName);
return new Set(table?.columns.map((column) => column.name) ?? []);
}
async function listIndexNames(ctx: DialectTestContext): Promise<Set<string>> {
if (ctx.dialect === "sqlite") {
const result = await sql<{ name: string }>`
SELECT name FROM sqlite_master WHERE type = 'index'
`.execute(ctx.db);
return new Set(result.rows.map((row) => row.name));
}
const result = await sql<{ name: string }>`
SELECT indexname AS name FROM pg_indexes WHERE schemaname = current_schema()
`.execute(ctx.db);
return new Set(result.rows.map((row) => row.name));
}