diff --git a/wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.php b/wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.php index 766e470..18db2c9 100644 --- a/wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.php +++ b/wordpress/themes/cdcf-headless/includes/admin/submission-lifecycle.php @@ -456,6 +456,18 @@ function cdcf_repend_submission_on_untrash($new_status, $old_status, $post): voi * Only acts on the English source post — when the worker promotes * a translation sibling to publish, this hook fires again, but * there's nothing more to enqueue at that point. + * + * The actual enqueue work is deferred to `shutdown` rather than run + * synchronously here. Running it inline puts our nested wp_insert_post + * + pll_save_post_translations calls inside Polylang's own save_post + * chain for the source post (and each freshly-inserted draft), where + * the multi-post group save silently fails to persist. Observed on + * production 2026-06-16: FamilyGraph submission 1381 was published + * with EN/IT/ES/FR/PT/DE siblings created and worker-translated, but + * the Polylang translation group came out empty on all six posts and + * Phase 0's attachment translation siblings were never created. The + * identical pll_save_post_translations call from outside the save + * chain (via /cdcf/v1/link-translations) persisted on the first try. */ function cdcf_enqueue_translations_on_publish($new_status, $old_status, $post): void { if ($new_status === $old_status) { @@ -480,7 +492,10 @@ function cdcf_enqueue_translations_on_publish($new_status, $old_status, $post): return; } - cdcf_enqueue_translations_for_submission($source_id, $post->post_type); + $post_type = $post->post_type; + add_action('shutdown', static function () use ($source_id, $post_type): void { + cdcf_enqueue_translations_for_submission($source_id, $post_type); + }); } /** diff --git a/wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php b/wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php index 5ecd7a2..9e9727f 100644 --- a/wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php +++ b/wordpress/themes/cdcf-headless/tests/SubmissionLifecycleTest.php @@ -886,8 +886,20 @@ public function test_publish_hook_skips_non_public_submission_posts(): void $this->assertTrue(true); } - public function test_publish_hook_enqueues_translations_for_public_submission(): void + public function test_publish_hook_defers_enqueue_to_shutdown_for_public_submission(): void { + // The publish hook MUST defer cdcf_enqueue_translations_for_submission to + // the `shutdown` action — running it synchronously inside + // transition_post_status puts our pll_save_post_translations call inside + // Polylang's own nested save_post chain for the source post + each + // freshly-inserted draft, which silently drops the multi-post group save. + // + // Observed in production 2026-06-16: FamilyGraph submission 1381 was + // published with EN/IT/ES/FR/PT/DE siblings created and worker-translated, + // but the Polylang group came out empty on all six posts AND Phase 0's + // attachment translation siblings were never created. Re-running the + // identical pll_save_post_translations call from outside the save chain + // (via /cdcf/v1/link-translations) persisted on the first attempt. $this->stubCommonFunctions(); Functions\when('pll_get_post')->justReturn(0); // self-source Functions\when('get_post_meta')->alias( @@ -896,6 +908,15 @@ public function test_publish_hook_enqueues_translations_for_public_submission(): : '' ); + $shutdownCallback = null; + Functions\when('add_action')->alias( + function (string $hook, callable $cb) use (&$shutdownCallback): void { + if ($hook === 'shutdown') { + $shutdownCallback = $cb; + } + } + ); + $enqueueCall = null; Functions\when('cdcf_enqueue_translations_for_submission')->alias( function (int $en_id, string $type) use (&$enqueueCall): void { @@ -906,6 +927,20 @@ function (int $en_id, string $type) use (&$enqueueCall): void { cdcf_enqueue_translations_on_publish('publish', 'draft', $this->fakePost()); + // Synchronously: NOT called. + $this->assertNull( + $enqueueCall, + 'cdcf_enqueue_translations_for_submission must NOT be invoked synchronously inside transition_post_status — it must be deferred to the `shutdown` action so Polylang\'s save_post chain settles first.' + ); + + // A shutdown callback was registered. + $this->assertNotNull( + $shutdownCallback, + 'cdcf_enqueue_translations_on_publish must call add_action(\'shutdown\', $callback) to defer the enqueue out of the post-save chain.' + ); + + // Driving the deferred callback fires the enqueue with the source post's args. + $shutdownCallback(); $this->assertSame([100, 'project'], $enqueueCall); }