Skip to content

feat(plugins): support translation-aware content creation - #3182

Merged
ascorbic merged 4 commits into
feat/plugin-content-discoveryfrom
feat/plugin-translation-create
Sep 19, 2026
Merged

ascorbic merged 4 commits into
feat/plugin-content-discoveryfrom
feat/plugin-translation-create

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds translation-aware content creation for sandboxed plugins as the next slice stacked on #3171.

Plugins can pass { locale, translationOf } to ctx.content.create(). EmDash validates the active source in the same collection, atomically joins its translation group, preserves non-translatable fields, byline credits, and taxonomy assignments, and runs the normal content validation and save-hook path across native, Cloudflare Worker Loader, and Node.js workerd execution.

The database permits one active row per case-insensitive locale in a translation group. The forward-only migration repairs historical duplicates before adding the unique index, and stable CONFLICT, NOT_FOUND, VALIDATION_ERROR, and SAVE_REJECTED codes cross both sandbox transports. Save-hook-originated creates do not re-enter save hooks.

Closes: n/a

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: n/a — this is the next slice of the maintainer-provided sandboxed capability expansion plan used by feat(plugins): add content discovery capabilities #3171.
  • I have included screenshots below if this PR changes the UI

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5)

Screenshots / test output

Not applicable. This PR has no admin UI changes.

Validated with:

  • pnpm build
  • pnpm typecheck
  • pnpm format:check
  • pnpm lint:json (zero diagnostics)
  • pnpm --dir docs build
  • focused native, SQLite migration/concurrency, Cloudflare Worker Loader, Node.js workerd, plugin-test, and plugin CLI suites
  • full @emdash-cms/plugin-test runtime-host suite (15/15)
  • independent adversarial review of the exact stacked diff, including material-fix rechecks and a final clean pass

One full workerd run encountered a transient local port collision; the affected wall-time test passed immediately in isolation, and the complete bridge suite plus the translation/restart real-workerd journey passed.

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 50c8141

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-test Minor
@emdash-cms/plugin-cli Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
create-emdash Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,461 lines across 42 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.

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet cla: signed labels Sep 17, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 50c8141 Sep 19 2026, 10:31 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://feat-plugin-translation-create.try.emdashcms.com, https://feat-plugin-translation-create-emdash-playground.emdash-cms.workers.dev (commit 50c8141)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://bbd2b41b.try.emdashcms.com, https://bbd2b41b-emdash-playground.emdash-cms.workers.dev 50c8141 2026-09-19T10:31:18.758Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://c9038b90.try.emdashcms.com, https://c9038b90-emdash-playground.emdash-cms.workers.dev a6d6a7b 2026-09-19T08:11:18.293Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://10387a67.try.emdashcms.com, https://10387a67-emdash-playground.emdash-cms.workers.dev 77d7ab5 2026-09-19T06:52:52.044Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://69d58500.try.emdashcms.com, https://69d58500-emdash-playground.emdash-cms.workers.dev f262c2e 2026-09-18T05:50:48.615Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://47935264.try.emdashcms.com, https://47935264-emdash-playground.emdash-cms.workers.dev 3457fef 2026-09-17T17:35:34.369Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://1c883c21.try.emdashcms.com, https://1c883c21-emdash-playground.emdash-cms.workers.dev 6deae76 2026-09-17T16:55:45.572Z Visit the dashboard ↗

@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Sep 17, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the expected next slice of the maintainer-provided sandboxed capability expansion (stacked on #3171), so the Discussion requirement is reasonably satisfied. The approach is sound and idiomatic for EmDash: a runtime-owned contentCreate callback wires the sandbox transports back to EmDashRuntime.handleContentCreate; the translation insert reads the source atomically in the same SQL statement; a partial unique index on (translation_group, lower(locale)) enforces one active row per locale; and hook re-entry is avoided with a combination of AsyncLocalStorage and the sandbox originHook marker.

I read the full diff, traced the runtime, Cloudflare, and workerd paths, checked the migration against dialect-helpers, and reviewed the tests, docs, and changeset. The implementation aligns with the PR description and the docs/changeset accurately reflect the behavior. Tests cover concurrency conflicts, missing sources, locale casing, custom configured locales, source mutation during hooks, save-hook re-entry, and cross-transport error codes.

The only concrete issue is a migration portability bug in the down path: up() correctly truncates the Postgres index name to 63 bytes before checking existence, but down() drops the untruncated name, so it silently misses the index on tables with long collection slugs and will fail tests or rollbacks that exercise it. There is no SQL-injection risk, no missing locale filter, no logged-out query-count regression, and no material doc/changeset problem.

Comment thread packages/core/src/database/migrations/080_content_translation_locale_unique.ts Outdated
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 17, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Sep 17, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Checked this before changing the migration. PostgreSQL truncates identifiers written in commands to NAMEDATALEN - 1 bytes; that applies when the identifier is referenced by DROP INDEX as well as when it is created, including quoted identifiers. The existing full generated name in down() therefore resolves to the same stored 63-byte identifier. SQLite stores and resolves the full name consistently.

I also drafted the suggested truncated-name helper and a long-slug rollback assertion. Independent adversarial review found that the assertion passes against the existing implementation too, so it does not reproduce a regression. I discarded that non-fix and left the current green head unchanged.

PostgreSQL reference: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS

~ 🤖 Codex

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@pkg-pr-new

pkg-pr-new Bot commented Sep 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@3182

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@3182

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@3182

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@3182

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@3182

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@3182

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@3182

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@3182

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@3182

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@3182

@emdash-cms/plugin-test

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-test@3182

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@3182

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@3182

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@3182

@emdash-cms/registry-loader

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-loader@3182

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@3182

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@3182

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@3182

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@3182

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@3182

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@3182

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@3182

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@3182

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@3182

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@3182

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@3182

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@3182

commit: 50c8141

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 18, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This re-review confirms the prior migration-down issue is fixed and finds no new blockers. The PR is the expected next slice of the maintainer-provided sandboxed capability expansion, so the approved-Discussion requirement remains reasonably satisfied.

What changed since the last review:

  • packages/core/src/database/migrations/080_content_translation_locale_unique.ts now truncates the Postgres index name to 63 bytes in down() before dropping it, matching the up() path. The new content-translation-index-name.test.ts assertions also cover this.

What I re-checked:

  • Runtime translation creation (EmDashRuntime.handleContentCreate) atomically resolves the source, enforces the one-active-locale-per-group rule with a pre-flight sibling check plus the unique index as a race guard, copies non-translatable fields after beforeSave hooks, and excludes the creating plugin’s own content:afterSave.
  • ContentRepository.create rewrites the translation create as an INSERT ... SELECT against the source row, so shared-field values are read at insert time and a source that is trashed during hooks turns into a NOT_FOUND.
  • Cloudflare Worker Loader and Node/workerd transports both wire the runtime-owned contentCreate callback, forward the originHook marker, and map CONFLICT/NOT_FOUND/VALIDATION_ERROR/SAVE_REJECTED back to plugin-visible errors.
  • The hook re-entry guard combines AsyncLocalStorage for native plugins with the sandbox originHook marker for isolated hooks, while normal route-created content still runs save hooks.
  • The partial unique index on (translation_group, lower(locale)) is added both in migration 080 and in SchemaRegistry.createContentTable.
  • Docs (capabilities.mdx, testing.mdx, SKILL files) and the changeset accurately describe the behavior.

No logic bugs, regressions, SQL-injection risks, missing locale filters, logged-out query-count regressions, or material documentation inaccuracies remain. Approve.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 18, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 18, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Review of f262c2e45, scoped to this PR's own diff (origin/feat/plugin-content-discovery...origin/feat/plugin-translation-create). One blocker. #3171 underneath it came back clean.

Blocker: on Postgres the restore conflict mapping never matches, so 409 becomes 500

function isTranslationLocaleConflict(error: unknown): boolean {
	if (!(error instanceof Error)) return false;
	const message = error.message.toLowerCase();
	return (
		message.includes("active_tg_locale") ||
		(message.includes("unique constraint failed") &&
			message.includes("translation_group") &&
			message.includes("locale"))
	);
}

packages/core/src/api/handlers/content.ts:67-76

Postgres raises duplicate key value violates unique constraint "<name>", which contains neither unique constraint failed nor the column names — so on Postgres the entire check rests on the index name containing active_tg_locale.

That name gets truncated. The migration itself already accounts for this:

const indexName = `uidx_${tableName}_active_tg_locale`;
const storedIndexName = isPostgres(db) ? indexName.slice(0, 63) : indexName;

080_content_translation_locale_unique.ts:70-71, and again at :84-85

uidx_ec_{slug}_active_tg_locale is 25 characters plus the slug, and Postgres truncates identifiers at 63 bytes. Any collection slug of 39+ characters (well inside the 63-char slug limit) stores a name ending _active_tg_local, _active_t, and so on — the substring test fails.

Failure scenario: Postgres site, collection slug ≥ 39 chars. Trash the fr translation, create a replacement fr translation, then restore the trashed one. handleContentRestore falls through to CONTENT_RESTORE_ERROR → HTTP 500 with a console.error, instead of the documented 409 CONFLICT ("An active translation already exists in this locale"). The operator gets an unexplained server error from an ordinary Restore click.

The branch's own content-translation-index-name.test.ts asserts the truncated names via expectedName(), so the truncation is already understood here — it just isn't reflected in the matcher.

Fix: match the name the migration actually computes instead of a suffix. isTranslationLocaleConflict already receives the collection at both call sites:

const expected = `uidx_ec_${collection}_active_tg_locale`.slice(0, 63).toLowerCase();
// ... message.includes(expected) || (existing SQLite clause)

Checked and clean: translation_group is taken atomically via COALESCE(translation_source.translation_group, translation_source.id) inside INSERT … SELECT … WHERE id = ? AND deleted_at IS NULL, so the group is reused and a source trashed mid-save yields zero rows → NOT_FOUND. Cross-collection sources are impossible (the SELECT is scoped to the same ec_* table). (translation_group, lower(locale)) uniqueness is enforced by migration 080 and mirrored in SchemaRegistry.createContentTable, closing the read-then-insert race. Slug uniqueness stays per-locale via generateUniqueSlug(collection, source, effectiveLocale). Locale is re-validated host-side by resolveContentCreateLocale even though both bridges validate first, content:write is re-checked in each bridge, and originHook comes from the host invocation path rather than plugin data, so save-hook suppression isn't spoofable. Migration 080's split loop provably terminates and doesn't collide with main (at 079).

One non-blocking note, since this changes a shared host path: runtime.handleContentCreate also serves admin POST /_emdash/api/content/[collection], so admin translation creation now also rejects unconfigured locales with VALIDATION_ERROR, force-inherits non-translatable fields from the source, and can return CONFLICT. Consistent with the group-shared-field model, but the emdash changeset frames these as plugin-only.

~ 🤖 Codex

@ascorbic

Copy link
Copy Markdown
Collaborator Author

Cross-PR blocker: three open PRs all add migration 080

origin/main is at 079_datetime_normalization, and three open PRs each add a different migration numbered 080:

PR migration file sorts
#3182 080_content_translation_locale_unique.ts 1st
#3184 080_redirect_write_guards.ts 2nd
#3169 080_taxonomy_translation_locale_unique.ts 3rd

Individually each is fine. Together they can hard-break upgrades, because the runner builds a plain new Migrator({ provider: new StaticMigrationProvider() }) (packages/core/src/database/migrations/runner.ts:227-229) with no allowUnorderedMigrations, so Kysely enforces strict alphabetical ordering:

throw new Error(`corrupted migrations: expected previously executed migration ${executedMigrations[i]} to be at index ${i} but ${migrations[i].name} was found in its place. New migrations must always have a name that comes alphabetically after the last executed migration.`);

kysely@0.29.2/dist/migration/migrator.js:503, #ensureMigrationsInOrder

Failure scenario: #3184 merges and ships, and a site upgrades and runs 080_redirect_write_guards. #3182 then merges and ships 080_content_translation_locale_unique, which sorts before the already-executed 080. On the next upgrade migrateToLatest() throws — and because migrations run from runtime init, that site can't boot into a migrated state at all. Recovery means hand-editing the migration table on production.

This only bites when the 080s land in different releases. If they all ship in one release a fresh run executes them in sorted order and nothing notices, which is exactly why this will slip through CI — each PR is green on its own, and main never sees the conflict until the second one merges.

Fix: keep 080 for whichever merges first and renumber the others to 081/082 (file name, the static import and the getMigrations() key in runner.ts) before they merge. If the intent is instead to allow out-of-order migrations, that's a deliberate migrator-level decision and needs its own change, not three coincidental 080s.

For this PR: 080_content_translation_locale_unique sorts first of the three, so it's the only one that is safe to merge at 080 without renumbering — but only if it merges before #3184 and #3169 ship. If either of those lands first, this one must be renumbered.

~ 🤖 Codex

@ascorbic

ascorbic commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both review points at 77d7ab559.

  • isTranslationLocaleConflict() now computes the collection-specific stored index name, including PostgreSQL's 63-byte truncation, while retaining the existing SQLite column-based clause.
  • Added a 39-character collection-slug restore regression to the dialect integration suite. The focused suite passes locally (8 tests); lint, full build, and typecheck also pass. An independent adversarial review found no further bugs.
  • Kept 080_content_translation_locale_unique as migration 080. This PR must land before feat(plugins): add sandboxed redirect access #3184's planned 081 and feat(plugins): add sandboxed taxonomy writes #3169's planned 082; no independent renumbering was made here.

~ 🤖 Codex (gpt-5.6-sol)

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Sep 19, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@ascorbic
ascorbic force-pushed the feat/plugin-translation-create branch from 77d7ab5 to a6d6a7b Compare September 19, 2026 08:06
@ascorbic ascorbic removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@ascorbic
ascorbic force-pushed the feat/plugin-translation-create branch from a6d6a7b to 50c8141 Compare September 19, 2026 10:27
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the expected next slice of the sandboxed capability expansion and the approach is sound: translation creates are routed through the runtime, source fields/attribution are resolved atomically by the repository, hook re-entry is guarded for both native and sandbox transports, and the unique partial index plus migration repair keep the invariant forward-compatible.

I re-read the full diff and checked the core runtime path, ContentRepository.create, migration 080 (including the fixed down() truncation), SchemaRegistry index creation, Cloudflare and workerd bridge/wrapper error envelopes and originHook wiring, AsyncLocalStorage hook-context plumbing, locale resolution changes, and the new test coverage.

No logic bugs, SQL-injection risks, missing locale filters, logged-out query-count regressions, or material documentation inaccuracies remain. The migration-down truncation fix is in place and covered by the updated index-name test.

I did find three small quality issues: a missing test for the documented "creating plugin's own afterSave is not re-entered" behavior, a dead __emdashOriginHook option in two test fixtures, and a new tautological string-in-template assertion in the plugin-cli tests. None block correctness, so the review is a comment rather than a request for changes.

Comment on lines +207 to +211
await expect(
runtimeHost.inspect.content.terms("posts", translated.id, "tags", "en"),
).resolves.toEqual([expect.objectContaining({ slug: "news" })]);
await expect(
runtimeHost.transport.invokeRoute("content-translation-error", {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The runtime-translation test covers shared fields, copied bylines, inherited taxonomy terms, and error codes, but it does not verify the documented behavior that the creating plugin's own content:afterSave hook is skipped for translation creates. The fixture's content:afterSave writes a storage event for every saved entry, so add an assertion that no events row exists for translated.id after any deferred hooks have had a chance to run.

Suggested change
await expect(
runtimeHost.inspect.content.terms("posts", translated.id, "tags", "en"),
).resolves.toEqual([expect.objectContaining({ slug: "news" })]);
await expect(
runtimeHost.transport.invokeRoute("content-translation-error", {
).resolves.toEqual([expect.objectContaining({ slug: "news" })]);
await vi.waitFor(async () => {
await expect(
runtimeHost!.inspect.storage.get("events", translated.id),
).resolves.toBeNull();
});
await expect(
runtimeHost.transport.invokeRoute("content-translation-error", {

const options = {
locale: route.input.locale,
translationOf: route.input.translationOf,
__emdashOriginHook: "content:beforeSave",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] __emdashOriginHook inside the ctx.content.create options is dead code. The sandbox wrappers and bridge handlers only read a dedicated originHook field supplied by the wrapper itself from the hook name; nothing consumes an extra key inside options. Leaving it here suggests a hook-skip mechanism that does not exist and will confuse future maintainers.

Suggested change
__emdashOriginHook: "content:beforeSave",
const options = {
locale: route.input.locale,
translationOf: route.input.translationOf,
};

{
locale: route.input.locale,
translationOf: route.input.translationOf,
__emdashOriginHook: "content:beforeSave"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Same as in the plugin-test fixture: __emdashOriginHook inside ctx.content.create options is never read by the workerd wrapper or bridge. It should be removed to avoid implying a non-existent API.

Suggested change
__emdashOriginHook: "content:beforeSave"
{
locale: route.input.locale,
translationOf: route.input.translationOf,
}

expect(skill).toContain("Node/workerd parity opt-in");
expect(skill).toContain("schema:read");
expect(skill).toContain("content:revisions:read");
expect(skill).toContain("{ locale, translationOf }");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This assertion only checks that the generated skill text contains the literal substring { locale, translationOf }. It cannot fail on a real functional regression — it only fails if the prose is intentionally removed. Per the repo's testing conventions, this is a config-pin / assert-the-diff test. Remove it and rely on the runtime tests that exercise ctx.content.create(..., { locale, translationOf }) instead.

Suggested change
expect(skill).toContain("{ locale, translationOf }");
expect(skill).toContain("createPluginRuntimeTestHost()");
expect(skill).toContain("Node/workerd parity opt-in");
expect(skill).toContain("schema:read");
expect(skill).toContain("content:revisions:read");
expect(skill).toContain("@<publisher-handle>/<slug>");

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Sep 19, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Sep 19, 2026
@ascorbic
ascorbic merged commit 70ab2f8 into main Sep 19, 2026
80 of 82 checks passed
@ascorbic
ascorbic deleted the feat/plugin-translation-create branch September 19, 2026 13:08
@emdashbot emdashbot Bot mentioned this pull request Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant