Skip to content

Commit 2dbd283

Browse files
committed
fix: prepare Jetpack Forms through canonical lifecycle
AI-assisted with openai/gpt-5.6-terra using OpenCode: implemented and tested the Jetpack Forms runtime preparation and provider adapter coverage.
1 parent 3b2595e commit 2dbd283

6 files changed

Lines changed: 181 additions & 64 deletions

homeboy-test-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"schema": "homeboy/test-manifest/v1",
33
"tests": {
44
"tests/form-materializer-smoke.php": { "environment": "standalone-php" },
5+
"tests/provider-adapter-runtime-smoke.php": { "environment": "standalone-php" },
56
"tests/smoke-ability-error-report-summary.php": { "environment": "standalone-php" },
67
"tests/smoke-ability-import-success-diagnostics.php": { "environment": "standalone-php" },
78
"tests/smoke-ability-registration-idempotent.php": { "environment": "standalone-php" },

includes/class-static-site-importer-entity-materializer-registry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ private static function adapters(): array {
581581
'preparation_callback' => array( 'Static_Site_Importer_Form_Seeder', 'prepare_jetpack_forms_runtime' ),
582582
'provider_readiness' => array(
583583
'required_block_types' => Static_Site_Importer_Form_Seeder::required_block_types(),
584-
'required_classes' => array( 'Automattic\\Jetpack\\Forms\\ContactForm\\Contact_Form' ),
584+
'required_classes' => Static_Site_Importer_Form_Seeder::required_runtime_apis(),
585585
),
586586
'missing_apis' => array(
587587
'Automattic\\Jetpack\\Forms\\ContactForm\\Contact_Form',

includes/class-static-site-importer-form-seeder.php

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
*/
2424
class Static_Site_Importer_Form_Seeder {
2525

26+
/** Whether this process has explicitly completed the late Jetpack Forms init. */
27+
private static bool $jetpack_forms_initialized = false;
28+
2629
/** Return the provider block emitted for one mapped form binding. */
2730
public static function binding_block_markup( array $entity, array $result ): string {
2831
unset( $entity );
@@ -61,6 +64,11 @@ public static function required_block_types(): array {
6164
return array_values( array_unique( array_merge( array( 'jetpack/contact-form', 'jetpack/field-checkbox-multiple', 'jetpack/input', 'jetpack/label', 'jetpack/option', 'jetpack/options', 'jetpack/phone-input' ), array_values( self::field_block_map() ) ) ) );
6265
}
6366

67+
/** @return array<int,string> Provider APIs required by the declared adapter. */
68+
public static function required_runtime_apis(): array {
69+
return array( 'Automattic\\Jetpack\\Forms\\ContactForm\\Contact_Form' );
70+
}
71+
6472
/**
6573
* Materialize Jetpack contact forms from a validated forms manifest.
6674
*
@@ -152,66 +160,56 @@ public static function jetpack_forms_available(): bool {
152160
return ! empty( $availability['available'] );
153161
}
154162

155-
/** Activate Jetpack's Forms module and register its server-side block types. */
163+
/** Activate and prepare Jetpack Forms through its canonical module lifecycle. */
156164
public static function prepare_jetpack_forms_runtime() {
157-
if ( ! class_exists( 'Jetpack' ) ) {
158-
return new WP_Error( 'static_site_importer_jetpack_class_missing', 'Jetpack activated without exposing its runtime class.' );
159-
}
160-
$modules_class = 'Automattic\\Jetpack\\Modules';
161-
if ( ! class_exists( $modules_class ) ) {
162-
return new WP_Error( 'static_site_importer_jetpack_modules_api_missing', 'Jetpack activated without exposing its modules API.' );
163-
}
164-
$modules = new $modules_class();
165-
$status_class = 'Automattic\\Jetpack\\Status';
166-
$cache_class = 'Automattic\\Jetpack\\Status\\Cache';
167-
$connection_ready_callback = self::optional_static_callback( 'Jetpack', 'is_connection_ready' );
168-
if (
169-
null !== $connection_ready_callback
170-
&& ! $connection_ready_callback()
171-
&& class_exists( $status_class )
172-
&& ! ( new $status_class() )->is_offline_mode()
173-
) {
174-
update_option( 'jetpack_offline_mode', true, false );
175-
$cache_clear_callback = self::optional_static_callback( $cache_class, 'clear' );
176-
if ( null !== $cache_clear_callback ) {
177-
$cache_clear_callback();
178-
}
165+
if ( ! class_exists( 'Jetpack' ) || ! class_exists( 'Automattic\\Jetpack\\Modules' ) ) {
166+
return self::jetpack_forms_runtime_error( 'static_site_importer_jetpack_forms_runtime_missing', array( 'Jetpack', 'Automattic\\Jetpack\\Modules' ) );
179167
}
168+
169+
$modules = new Automattic\Jetpack\Modules();
180170
if ( ! $modules->is_active( 'contact-form' ) ) {
181-
Jetpack::activate_module( 'contact-form', false, false );
171+
if ( ! method_exists( 'Jetpack', 'activate_module' ) ) {
172+
return self::jetpack_forms_runtime_error( 'static_site_importer_jetpack_forms_activation_missing', array( 'Jetpack::activate_module' ) );
173+
}
174+
$activated = Jetpack::activate_module( 'contact-form', false, false );
175+
if ( false === $activated || ( function_exists( 'is_wp_error' ) && is_wp_error( $activated ) ) ) {
176+
return self::jetpack_forms_runtime_error( 'static_site_importer_jetpack_forms_activation_failed', array( 'contact-form' ) );
177+
}
182178
}
183-
if ( ! $modules->is_active( 'contact-form' ) ) {
184-
return new WP_Error(
185-
'static_site_importer_jetpack_forms_module_inactive',
186-
'Jetpack did not activate its contact-form module.',
187-
array(
188-
'module_active' => false,
189-
'availability' => self::jetpack_forms_availability_details(),
190-
)
191-
);
179+
180+
$loader = 'Automattic\\Jetpack\\Forms\\Jetpack_Forms';
181+
if ( ! class_exists( $loader ) || ! method_exists( $loader, 'load_contact_form' ) ) {
182+
return self::jetpack_forms_runtime_error( 'static_site_importer_jetpack_forms_loader_missing', array( $loader . '::load_contact_form' ) );
192183
}
184+
$loader::load_contact_form();
193185

194-
$block_class = 'Automattic\\Jetpack\\Extensions\\Contact_Form\\Contact_Form_Block';
195-
$register_block_callback = self::optional_static_callback( $block_class, 'register_block' );
196-
$register_child_blocks_callback = self::optional_static_callback( $block_class, 'register_child_blocks' );
197-
if ( null !== $register_block_callback && null !== $register_child_blocks_callback ) {
198-
$register_block_callback();
199-
$register_child_blocks_callback();
186+
$initializer = 'Automattic\\Jetpack\\Forms\\ContactForm\\Contact_Form_Plugin';
187+
if ( function_exists( 'did_action' ) && did_action( 'init' ) && ! self::$jetpack_forms_initialized ) {
188+
if ( ! class_exists( $initializer ) || ! method_exists( $initializer, 'init' ) ) {
189+
return self::jetpack_forms_runtime_error( 'static_site_importer_jetpack_forms_init_missing', array( $initializer . '::init' ) );
190+
}
191+
$initializer::init();
192+
self::$jetpack_forms_initialized = true;
200193
}
201194

202195
$availability = self::jetpack_forms_availability_details();
203-
return ! empty( $availability['available'] )
204-
? true
205-
: new WP_Error( 'static_site_importer_jetpack_forms_blocks_missing', 'Jetpack Forms activated without registering every required provider block.', array(
206-
'module_active' => true,
207-
'availability' => $availability,
208-
) );
196+
if ( ! empty( $availability['available'] ) ) {
197+
return true;
198+
}
199+
200+
return self::jetpack_forms_runtime_error(
201+
'static_site_importer_jetpack_forms_blocks_missing',
202+
array_keys( array_filter( $availability['required_blocks'], static fn ( bool $registered ): bool => ! $registered ) ),
203+
$availability
204+
);
209205
}
210206

211-
/** Resolve an optional provider API without coupling analysis to one installed version. */
212-
private static function optional_static_callback( string $class_name, string $method_name ): ?callable {
213-
$callback = array( $class_name, $method_name );
214-
return is_callable( $callback ) ? $callback : null;
207+
/** Build a bounded provider-readiness error. */
208+
private static function jetpack_forms_runtime_error( string $code, array $missing, array $details = array() ): WP_Error {
209+
return new WP_Error( $code, 'Jetpack Forms provider runtime is not ready.', array_filter( array(
210+
'missing' => array_slice( array_values( $missing ), 0, 20 ),
211+
'details' => $details,
212+
) ) );
215213
}
216214

217215
/**
@@ -220,7 +218,11 @@ private static function optional_static_callback( string $class_name, string $me
220218
* @return array<string,mixed>
221219
*/
222220
public static function jetpack_forms_availability_details(): array {
223-
$contact_form_class = class_exists( 'Automattic\\Jetpack\\Forms\\ContactForm\\Contact_Form' );
221+
$required_apis = array();
222+
foreach ( self::required_runtime_apis() as $api ) {
223+
$required_apis[ $api ] = class_exists( $api );
224+
}
225+
$contact_form_class = ! empty( $required_apis['Automattic\\Jetpack\\Forms\\ContactForm\\Contact_Form'] );
224226
$legacy_class = class_exists( 'Grunion_Contact_Form' ) || class_exists( 'Contact_Form' );
225227
$registered_blocks = array_fill_keys( self::required_block_types(), false );
226228

@@ -232,13 +234,17 @@ public static function jetpack_forms_availability_details(): array {
232234
}
233235
$contact_form_block = $registered_blocks['jetpack/contact-form'];
234236
$field_text_block = $registered_blocks['jetpack/field-text'];
237+
$required_blocks_available = ! empty( $registered_blocks ) && ! in_array( false, $registered_blocks, true );
238+
$required_apis_available = ! empty( $required_apis ) && ! in_array( false, $required_apis, true );
235239

236240
return array(
237-
'available' => ( $contact_form_class || $legacy_class ) && ! in_array( false, $registered_blocks, true ),
241+
'available' => $required_apis_available && $contact_form_block && $required_blocks_available,
238242
'contact_form_class' => $contact_form_class,
239243
'legacy_class' => $legacy_class,
240244
'contact_form_block' => $contact_form_block,
241245
'field_text_block' => $field_text_block,
246+
'required_apis' => $required_apis,
247+
'required_blocks' => $registered_blocks,
242248
'registered_blocks' => $registered_blocks,
243249
);
244250
}

test-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
},
77
"tests": [
88
{ "path": "tests/form-materializer-smoke.php", "environment": "standalone-php" },
9+
{ "path": "tests/provider-adapter-runtime-smoke.php", "environment": "standalone-php" },
910
{ "path": "tests/host-dependency-resolver.test.mjs", "environment": "node" },
1011
{ "path": "tests/smoke-ability-error-report-summary.php", "environment": "standalone-php" },
1112
{ "path": "tests/smoke-ability-import-success-diagnostics.php", "environment": "standalone-php" },

tests/form-materializer-smoke.php

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
* @package StaticSiteImporter
99
*/
1010

11+
namespace Automattic\Jetpack\Forms\ContactForm {
12+
class Contact_Form {}
13+
}
14+
1115
namespace {
1216
if ( ! defined( 'ABSPATH' ) ) {
1317
define( 'ABSPATH', dirname( __DIR__ ) . '/' );
@@ -193,18 +197,9 @@ public function is_registered( string $name ): bool {
193197
$GLOBALS['ssi_jetpack_registered_form_blocks'] = array( 'jetpack/contact-form', 'jetpack/field-text' );
194198
$assert( ! Static_Site_Importer_Form_Seeder::jetpack_forms_available(), 'partial-provider-block-registration-is-unavailable' );
195199
$GLOBALS['ssi_jetpack_registered_form_blocks'] = $all_jetpack_blocks;
196-
$GLOBALS['ssi_test_options'] = array();
197-
$GLOBALS['ssi_test_jetpack_active_modules'] = array();
198-
Jetpack::$connection_ready = false;
199-
$assert( true === Static_Site_Importer_Form_Seeder::prepare_jetpack_forms_runtime(), 'disconnected-provider-runtime-prepared' );
200-
$assert( true === get_option( 'jetpack_offline_mode' ), 'disconnected-provider-persists-offline-mode' );
201-
$assert( in_array( 'contact-form', $GLOBALS['ssi_test_jetpack_active_modules'], true ), 'disconnected-provider-activates-forms' );
202-
203-
$GLOBALS['ssi_test_options'] = array();
204-
$GLOBALS['ssi_test_jetpack_active_modules'] = array();
205-
Jetpack::$connection_ready = true;
206-
$assert( true === Static_Site_Importer_Form_Seeder::prepare_jetpack_forms_runtime(), 'connected-provider-runtime-prepared' );
207-
$assert( false === get_option( 'jetpack_offline_mode', false ), 'connected-provider-preserves-online-mode' );
200+
$jetpack_dependency = $form_adapter['dependencies'][0] ?? array();
201+
$assert( in_array( 'jetpack/option', $jetpack_dependency['missing_apis'] ?? array(), true ), 'form-adapter-declares-field-children' );
202+
$assert( Static_Site_Importer_Form_Seeder::required_block_types() === ( $jetpack_dependency['provider_readiness']['required_block_types'] ?? array() ), 'form-adapter-declares-every-emitted-block' );
208203

209204
// --- Woo path unaffected -------------------------------------------------
210205
$product_adapter = Static_Site_Importer_Entity_Materializer_Registry::product_adapter();
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
/**
3+
* Provider adapter lifecycle coverage for the Jetpack Forms runtime.
4+
*
5+
* @package StaticSiteImporter
6+
*/
7+
8+
namespace {
9+
$case = $argv[1] ?? '';
10+
if ( '' === $case ) {
11+
$failures = array();
12+
foreach ( array( 'init-before-after', 'missing-loader', 'missing-init', 'partial-blocks' ) as $child_case ) {
13+
$command = escapeshellarg( PHP_BINARY ) . ' ' . escapeshellarg( __FILE__ ) . ' ' . escapeshellarg( $child_case );
14+
exec( $command, $output, $status );
15+
if ( 0 !== $status ) {
16+
$failures[] = $child_case . ': ' . implode( "\n", $output );
17+
}
18+
$output = array();
19+
}
20+
if ( empty( $failures ) ) {
21+
echo "PASS provider-adapter-runtime-smoke.php\n";
22+
exit( 0 );
23+
}
24+
echo "FAILURES:\n" . implode( "\n", $failures ) . "\n";
25+
exit( 1 );
26+
}
27+
28+
define( 'ABSPATH', dirname( __DIR__ ) . '/' );
29+
$GLOBALS['ssi_provider_adapter_case'] = $case;
30+
$GLOBALS['ssi_init_actions'] = 'init-before-after' === $case ? 0 : 1;
31+
$GLOBALS['ssi_missing_blocks'] = 'partial-blocks' === $case ? array( 'jetpack/field-email' ) : array();
32+
33+
class WP_Error {
34+
public function __construct( private string $code, private string $message = '', private array $data = array() ) {}
35+
public function get_error_code(): string { return $this->code; }
36+
public function get_error_data(): array { return $this->data; }
37+
}
38+
function is_wp_error( $value ): bool { return $value instanceof WP_Error; }
39+
function did_action( string $hook ): int { return 'init' === $hook ? $GLOBALS['ssi_init_actions'] : 0; }
40+
41+
class Jetpack {
42+
public static int $activations = 0;
43+
public static function activate_module( string $module, bool $redirect, bool $options ): bool {
44+
unset( $redirect, $options );
45+
++self::$activations;
46+
$GLOBALS['ssi_contact_form_active'] = 'contact-form' === $module;
47+
return true;
48+
}
49+
}
50+
class WP_Block_Type_Registry {
51+
public static function get_instance(): self { return new self(); }
52+
public function is_registered( string $name ): bool { return ! in_array( $name, $GLOBALS['ssi_missing_blocks'], true ); }
53+
}
54+
}
55+
56+
namespace Automattic\Jetpack {
57+
class Modules {
58+
public function is_active( string $module ): bool { return 'contact-form' === $module && ! empty( $GLOBALS['ssi_contact_form_active'] ); }
59+
}
60+
}
61+
62+
namespace Automattic\Jetpack\Forms {
63+
if ( 'missing-loader' !== $GLOBALS['ssi_provider_adapter_case'] ) {
64+
class Jetpack_Forms {
65+
public static int $loads = 0;
66+
public static function load_contact_form(): void { ++self::$loads; }
67+
}
68+
}
69+
}
70+
71+
namespace Automattic\Jetpack\Forms\ContactForm {
72+
class Contact_Form {}
73+
if ( 'missing-init' !== $GLOBALS['ssi_provider_adapter_case'] ) {
74+
class Contact_Form_Plugin {
75+
public static int $initializations = 0;
76+
public static function init(): void { ++self::$initializations; }
77+
}
78+
}
79+
}
80+
81+
namespace {
82+
require_once dirname( __DIR__ ) . '/includes/class-static-site-importer-form-seeder.php';
83+
84+
$result = Static_Site_Importer_Form_Seeder::prepare_jetpack_forms_runtime();
85+
$assert = static function ( bool $condition, string $message ): void {
86+
if ( ! $condition ) {
87+
echo 'FAIL ' . $message . "\n";
88+
exit( 1 );
89+
}
90+
};
91+
92+
if ( 'init-before-after' === $case ) {
93+
$assert( true === $result, 'initial preparation succeeds' );
94+
$assert( 1 === Jetpack::$activations, 'contact-form activates once without a connection' );
95+
$assert( 1 === \Automattic\Jetpack\Forms\Jetpack_Forms::$loads, 'canonical loader runs before init' );
96+
$assert( 0 === \Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin::$initializations, 'init hook remains deferred before init' );
97+
$GLOBALS['ssi_init_actions'] = 1;
98+
$assert( true === Static_Site_Importer_Form_Seeder::prepare_jetpack_forms_runtime(), 'late preparation succeeds' );
99+
$assert( 1 === \Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin::$initializations, 'late canonical init runs once' );
100+
$assert( true === Static_Site_Importer_Form_Seeder::prepare_jetpack_forms_runtime(), 'repeated preparation succeeds' );
101+
$assert( 1 === \Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin::$initializations, 'late canonical init is idempotent' );
102+
}
103+
104+
if ( 'missing-loader' === $case ) {
105+
$assert( is_wp_error( $result ) && 'static_site_importer_jetpack_forms_loader_missing' === $result->get_error_code(), 'missing loader is bounded' );
106+
}
107+
if ( 'missing-init' === $case ) {
108+
$assert( is_wp_error( $result ) && 'static_site_importer_jetpack_forms_init_missing' === $result->get_error_code(), 'missing init is bounded' );
109+
}
110+
if ( 'partial-blocks' === $case ) {
111+
$assert( is_wp_error( $result ) && 'static_site_importer_jetpack_forms_blocks_missing' === $result->get_error_code(), 'partial block registry fails readiness' );
112+
$assert( array( 'jetpack/field-email' ) === $result->get_error_data()['missing'], 'missing block diagnostic is bounded and specific' );
113+
}
114+
}

0 commit comments

Comments
 (0)