test: add realistic WordPress migration acceptance test#1858
Conversation
The WP migration was validated only as isolated importer units (plus a parser/converter-only pass over the theme-unit-test dataset). Nothing exercised the runtime pipeline the admin wizard drives, end to end, against one realistic export. Adds a committed WXR fixture (posts with Gutenberg + classic content, pages, a CPT with a custom taxonomy, hierarchical categories, tags, drafts/pending, two authors, attachments with size-variant URLs used in content, featured images, Yoast/ACF meta, a reusable block) and an acceptance test that runs parse -> taxonomy plan -> importContent -> sections -> media import (network stubbed) -> URL rewriting, then asserts the resulting database state: slugs preserved 1:1, statuses and original dates, Portable Text structure, bylines, taxonomy terms and assignments, sections, media enrichment, content/featured-image URLs rewritten to imported media, and idempotent re-runs. Out-of-scope paths (custom-field/SEO values via the plugin importer, comments/menus, the CLI redirects map, multilingual imports) are documented in the test header with pointers to their existing suites. Fixes emdash-cms#1691
🦋 Changeset detectedLatest commit: 9b6d2a1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 780 lines across 2 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
This PR adds a focused, committed-fixture acceptance test for the WordPress WXR migration pipeline (issue #1691). The approach is sound: it drives the same runtime functions the admin wizard uses, stubs network/storage deterministically, and asserts on concrete database outcomes (counts, slugs, dates, statuses, taxonomy assignments, media URL rewriting, idempotency) rather than brittle snapshots.
I read the full diff, the fixture, the new test file, and the source implementations it exercises (execute.ts, media.ts, rewrite-urls.ts, wxr-taxonomies.ts, parser.ts, sections.ts, byline/taxonomy helpers, and the test harness). Test-only PRs don't need a changeset, and there are no admin UI strings or SQL-injection concerns here. No logic errors or regressions jumped out.
One optional tightening: the “skipExisting is a no-op” assertion only guards ec_post, so a rerun that accidentally duplicates bylines or other mapped collections would not be caught. A line-anchored suggestion expands the guard to the other mapped collections and sections.
| .selectFrom("ec_post" as keyof Database) | ||
| .select((eb) => eb.fn.countAll().as("count")) | ||
| .executeTakeFirstOrThrow(); | ||
| expect(Number(rows.count)).toBe(5); |
There was a problem hiding this comment.
[suggestion] The skip-existing idempotency check currently only guards ec_post. A rerun that creates duplicate rows in ec_page, ec_book, or _emdash_sections (or fails to keep counts stable) would slip through. Tighten the assertion to cover the other mapped collections and the imported section:
| expect(Number(rows.count)).toBe(5); | |
| expect(Number(rows.count)).toBe(5); | |
| const pages = await db | |
| .selectFrom("ec_page" as keyof Database) | |
| .select((eb) => eb.fn.countAll().as("count")) | |
| .executeTakeFirstOrThrow(); | |
| expect(Number(pages.count)).toBe(2); | |
| const books = await db | |
| .selectFrom("ec_book" as keyof Database) | |
| .select((eb) => eb.fn.countAll().as("count")) | |
| .executeTakeFirstOrThrow(); | |
| expect(Number(books.count)).toBe(2); | |
| const sections = await db | |
| .selectFrom("_emdash_sections") | |
| .select((eb) => eb.fn.countAll().as("count")) | |
| .executeTakeFirstOrThrow(); | |
| expect(Number(sections.count)).toBe(1); |
There was a problem hiding this comment.
Done in 9b6d2a1 — the rerun now asserts row counts for all mapped collections (ec_post, ec_page, ec_book), _emdash_bylines, and _emdash_sections. I also went one step further than the suggestion: the test re-runs importReusableBlocksAsSections itself, so the sections count guard is backed by an actual second pass through its skip-by-slug path (asserting sectionsCreated: 0, sectionsSkipped: 1) rather than just checking that importContent didn't touch the table.
The skipExisting rerun only asserted the ec_post row count; duplicated pages, books, bylines, or sections would have slipped through. Also reruns the sections import so its skip-by-slug path is exercised.
What does this PR do?
Fixes #1691 (milestone 1.0): validates the WordPress migration as an end-to-end workflow instead of only as isolated importer units.
The fixture (
tests/integration/wordpress-migration/fixtures/realistic-export.xml, committed — no network download) models a real small-site export:pending)bookcustom post type with a customgenretaxonomyguides→howto), tags, two authorshero-1024x683.jpg) are used inside content_thumbnail_id, Yoast SEO meta, ACF meta, internal links, a reusable block (wp_block)The test drives the runtime pipeline in the same order as the admin wizard —
parseWxrString→preImportWxrTaxonomies→importContent→importReusableBlocksAsSections→importMediaWithProgress(SSRF-safe fetch stubbed, distinct bytes per attachment so content-hash dedupe stays honest) →rewriteUrls— and then asserts on the resulting database state:wp-content/uploadsleft behindpublish→published,draft/pending→draft) and originalpost_date_gmtpreserved ascreated_at/published_atgenrescoped to the CPT),missingTaxonomiesemptyskipExistingis a no-op (idempotency)Coverage documentation (per the issue's "clearly documents what it covers"): the test header spells out what is intentionally out of scope for the WXR runtime path and where it is tested — custom-field/SEO values (plugin importer,
plugin-execute-fields.test.ts), comments/menus (plugin-execute-chunked.test.ts), the redirects map (CLI two-phase import,wordpress-import.test.ts), multilingual imports (wxr-i18n-taxonomies.test.ts). The fixture still carries the Yoast/ACF meta, and the test pins that the parser preserves it for the analyze step's field suggestions.Failures point at actionable importer problems: each assertion names the feature (slugs, dates, taxonomy assignment, URL rewriting, …) rather than a generic snapshot diff.
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output