From 51005768308450cdca6ab7d3447d2fc7636c124e Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 16 Jul 2026 08:13:45 -0400 Subject: [PATCH] fix: isolate post-runtime blueprint hydration --- package.json | 1 + .../class-wp-codebox-browser-task-builder.php | 27 +++++++++++++++++++ .../trait-wp-codebox-abilities-execution.php | 15 ++++++----- ...php-browser-preview-only-session-smoke.php | 23 ++++++++++++++-- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 468d15606..4d4344347 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "test:php-agent-runtime-execution": "php scripts/php-agent-runtime-execution-smoke.php", "test:php-runtime-provider-registry": "php -d zend.assertions=1 -d assert.exception=1 scripts/php-runtime-provider-registry-smoke.php", "test:php-browser-provider-auth-strategy": "php -d zend.assertions=1 -d assert.exception=1 scripts/php-browser-provider-auth-strategy-smoke.php", + "test:php-browser-preview-only-session": "php -d zend.assertions=1 -d assert.exception=1 scripts/php-browser-preview-only-session-smoke.php", "test:php-json-codec": "tsx tests/php-json-codec.test.ts", "test:php-managed-host-command": "tsx tests/php-managed-host-command.test.ts", "test:php-worker-runner": "tsx tests/php-worker-runner.test.ts", diff --git a/packages/wordpress-plugin/src/class-wp-codebox-browser-task-builder.php b/packages/wordpress-plugin/src/class-wp-codebox-browser-task-builder.php index 047de9316..139c889f6 100644 --- a/packages/wordpress-plugin/src/class-wp-codebox-browser-task-builder.php +++ b/packages/wordpress-plugin/src/class-wp-codebox-browser-task-builder.php @@ -579,6 +579,9 @@ public static function runtime_requirements( array $requirements ): array { /** @param array $prepared Prepared runtime descriptor. @param array $session Optional source session. @return array */ public static function browser_blueprint_ref( array $prepared, array $session = array() ): array { + if ( ! empty( $prepared['has_post_runtime_blueprint'] ) && is_array( $session['playground']['blueprint'] ?? null ) ) { + $prepared = self::browser_session_blueprint_ref_prepared( $prepared, $session, $session['playground']['blueprint'] ); + } $cache_key = self::safe_key( (string) ( $prepared['cache_key'] ?? $prepared['key'] ?? '' ) ); $input_hash = strtolower( trim( (string) ( $prepared['input_hash'] ?? $prepared['hash'] ?? '' ) ) ); $ref = '' !== $cache_key && preg_match( '/^[a-f0-9]{64}$/', $input_hash ) ? 'prepared:' . $cache_key . ':' . $input_hash : ''; @@ -605,6 +608,30 @@ public static function browser_blueprint_ref( array $prepared, array $session = ); } + /** @param array $prepared @param array $session @param array $blueprint @return array */ + private static function browser_session_blueprint_ref_prepared( array $prepared, array $session, array $blueprint ): array { + $cache_key = self::safe_key( (string) ( $prepared['cache_key'] ?? $prepared['key'] ?? '' ) ); + $input_hash = strtolower( trim( (string) ( $prepared['input_hash'] ?? $prepared['hash'] ?? '' ) ) ); + if ( '' === $cache_key || ! preg_match( '/^[a-f0-9]{64}$/', $input_hash ) || ! function_exists( 'set_transient' ) ) { + return $prepared; + } + + $session_id = (string) ( $session['session']['id'] ?? $session['session_id'] ?? '' ); + $payload = function_exists( 'wp_json_encode' ) ? wp_json_encode( array( 'prepared' => $input_hash, 'session' => $session_id, 'blueprint' => $blueprint ) ) : json_encode( array( 'prepared' => $input_hash, 'session' => $session_id, 'blueprint' => $blueprint ) ); + $ref_hash = hash( 'sha256', 'wp-codebox/browser-session-blueprint-ref/v1' . "\n" . (string) $payload ); + $artifact = array( + 'schema' => 'wp-codebox/browser-prepared-runtime-artifact/v1', + 'cache_key' => $cache_key, + 'input_hash' => $ref_hash, + 'blueprint' => $blueprint, + ); + $ttl = defined( 'WEEK_IN_SECONDS' ) ? WEEK_IN_SECONDS : 604800; + set_transient( self::prepared_runtime_transient_key( $cache_key, $ref_hash ), $artifact, $ttl ); + $prepared['input_hash'] = $ref_hash; + + return $prepared; + } + /** @param array $input Prepared runtime, full session, or executable ref request. @return array */ public static function executable_blueprint_ref( array $input ): array { if ( is_array( $input['blueprint_ref'] ?? null ) ) { diff --git a/packages/wordpress-plugin/src/trait-wp-codebox-abilities-execution.php b/packages/wordpress-plugin/src/trait-wp-codebox-abilities-execution.php index 62edb4470..e610a3b94 100644 --- a/packages/wordpress-plugin/src/trait-wp-codebox-abilities-execution.php +++ b/packages/wordpress-plugin/src/trait-wp-codebox-abilities-execution.php @@ -195,10 +195,13 @@ public static function create_browser_playground_session( array $input ): array| return $site_blueprint_artifact; } - $base_blueprint = self::browser_blueprint_with_site_artifact( is_array( $input['blueprint'] ?? null ) ? $input['blueprint'] : array(), $site_blueprint_artifact ); - $blueprint = self::browser_blueprint_with_runtime( $base_blueprint, $runtime, $playground ); - $blueprint = self::browser_blueprint_with_post_runtime( $blueprint, is_array( $input['post_runtime_blueprint'] ?? null ) ? $input['post_runtime_blueprint'] : array() ); - $prepared_runtime = self::browser_prepared_runtime_with_blueprints( is_array( $runtime['prepared_runtime'] ?? null ) ? $runtime['prepared_runtime'] : array(), $blueprint, $playground ); + $base_blueprint = self::browser_blueprint_with_site_artifact( is_array( $input['blueprint'] ?? null ) ? $input['blueprint'] : array(), $site_blueprint_artifact ); + $runtime_blueprint = self::browser_blueprint_with_runtime( $base_blueprint, $runtime, $playground ); + $post_runtime_blueprint = is_array( $input['post_runtime_blueprint'] ?? null ) ? $input['post_runtime_blueprint'] : array(); + $prepared_runtime = self::browser_prepared_runtime_with_blueprints( is_array( $runtime['prepared_runtime'] ?? null ) ? $runtime['prepared_runtime'] : array(), $runtime_blueprint, $playground ); + $runtime_blueprint = self::browser_selected_prepared_runtime_blueprint( $prepared_runtime, $runtime_blueprint ); + $blueprint = self::browser_blueprint_with_post_runtime( $runtime_blueprint, $post_runtime_blueprint ); + $prepared_runtime['has_post_runtime_blueprint'] = ! empty( $post_runtime_blueprint ); $runtime['prepared_runtime'] = $prepared_runtime; $contained_site = self::browser_contained_site_envelope( $input, $session_id, $playground, $runtime, $prepared_runtime, 'ready' ); $artifacts = self::browser_artifact_files( $input ); @@ -214,7 +217,7 @@ public static function create_browser_playground_session( array $input ): array| $task_payload = array(); $recipe = array(); $materialization = array(); - $recipe_blueprint = self::browser_playground_blueprint( self::browser_selected_prepared_runtime_blueprint( $prepared_runtime, $blueprint ), $playground ); + $recipe_blueprint = self::browser_playground_blueprint( $blueprint, $playground ); if ( ! $preview_only ) { $task_payload = self::browser_task_payload( $input, $task_input, $session_id, $artifacts, $inheritance_payload['inheritance'], $dependency_plan ); $recipe = self::browser_agent_recipe( $task_input, $session_id, $browser_runner, $blueprint, $playground, $task_payload ); @@ -225,7 +228,7 @@ public static function create_browser_playground_session( array $input ): array| $materialization = self::browser_materialization_contract( $recipe ); } if ( is_array( $runtime['prepared_runtime'] ?? null ) ) { - self::browser_prepared_runtime_cache_store( $runtime['prepared_runtime'], $recipe_blueprint ); + self::browser_prepared_runtime_cache_store( $runtime['prepared_runtime'], $runtime_blueprint ); } $blueprint = $recipe_blueprint; diff --git a/scripts/php-browser-preview-only-session-smoke.php b/scripts/php-browser-preview-only-session-smoke.php index 4d247a3fe..98f3619b3 100644 --- a/scripts/php-browser-preview-only-session-smoke.php +++ b/scripts/php-browser-preview-only-session-smoke.php @@ -131,6 +131,9 @@ function has_runner_step( array $steps ): bool { 'steps' => array( array( 'step' => 'setSiteOptions', 'options' => array( 'description' => 'Site artifact applied' ) ) ), ), ), + 'post_runtime_blueprint' => array( + 'steps' => array( array( 'step' => 'importWxr', 'file' => 'https://example.test/first-caller.xml' ) ), + ), 'include_internal_browser_session' => true, ); @@ -139,10 +142,17 @@ function has_runner_step( array $steps ): bool { fail( 'preview failed: ' . $preview->get_error_code() . ': ' . $preview->get_error_message() ); } $hydrated = WP_Codebox_Abilities::hydrate_browser_blueprint_ref( array( 'ref' => $preview['product']['preview_boot']['blueprint_ref'] ?? '' ) ); +$second_input = $input; +$second_input['sandbox_session_id'] = 'preview-only-session-second'; +$second_input['post_runtime_blueprint'] = array( + 'steps' => array( array( 'step' => 'importWxr', 'file' => 'https://example.test/second-caller.xml' ) ), +); +$second = WP_Codebox_Abilities::create_browser_playground_session( $second_input + array( 'preview_only' => true ) ); +$second_hydrated = WP_Codebox_Abilities::hydrate_browser_blueprint_ref( array( 'ref' => $second['product']['preview_boot']['blueprint_ref'] ?? '' ) ); $agentic = WP_Codebox_Abilities::create_browser_playground_session( $input ); $task = WP_Codebox_Abilities::create_browser_task_contract( $input + array( 'preview_only' => true, 'include_internal_browser_contract' => true ) ); -foreach ( array( 'hydrated' => $hydrated, 'agentic' => $agentic, 'task' => $task ) as $name => $result ) { +foreach ( array( 'hydrated' => $hydrated, 'second' => $second, 'second_hydrated' => $second_hydrated, 'agentic' => $agentic, 'task' => $task ) as $name => $result ) { if ( is_wp_error( $result ) ) { fail( $name . ' failed: ' . $result->get_error_code() . ': ' . $result->get_error_message() ); } @@ -170,7 +180,16 @@ function has_runner_step( array $steps ): bool { expect( true === ( $preview['product']['preview_boot']['blueprint_ref_dto']['hydratable'] ?? false ), 'Preview-only product DTO must expose a hydratable blueprint ref.' ); expect( ! is_wp_error( $hydrated ), 'Preview-only prepared runtime blueprint ref must hydrate.' ); -expect( $preview['playground']['blueprint'] === ( $hydrated['blueprint'] ?? null ), 'Preview-only boot blueprint must be the cached executable runtime blueprint.' ); +expect( 'miss' === ( $preview['runtime']['prepared_runtime']['status'] ?? '' ), 'First caller tail must materialize the shared runtime cache.' ); +expect( 'hit' === ( $second['runtime']['prepared_runtime']['status'] ?? '' ), 'Second caller tail must reuse the shared runtime cache.' ); +expect( $preview['playground']['blueprint'] === ( $hydrated['blueprint'] ?? null ), 'First caller hydration must return its executable blueprint.' ); +expect( $second['playground']['blueprint'] === ( $second_hydrated['blueprint'] ?? null ), 'Second caller hydration must return its executable blueprint.' ); +expect( str_contains( wp_json_encode( $hydrated['blueprint']['steps'] ?? array() ) ?: '', 'first-caller.xml' ), 'First caller hydration must retain its SSI import.' ); +expect( ! str_contains( wp_json_encode( $hydrated['blueprint']['steps'] ?? array() ) ?: '', 'second-caller.xml' ), 'First caller hydration must not contain the second caller import.' ); +expect( str_contains( wp_json_encode( $second_hydrated['blueprint']['steps'] ?? array() ) ?: '', 'second-caller.xml' ), 'Second caller hydration must retain its SSI import.' ); +expect( ! str_contains( wp_json_encode( $second_hydrated['blueprint']['steps'] ?? array() ) ?: '', 'first-caller.xml' ), 'Second caller hydration must not contain the first caller import.' ); +expect( 'importWxr' === ( $hydrated['blueprint']['steps'][ count( $hydrated['blueprint']['steps'] ?? array() ) - 1 ]['step'] ?? '' ), 'First caller import must run after shared runtime steps.' ); +expect( 'importWxr' === ( $second_hydrated['blueprint']['steps'][ count( $second_hydrated['blueprint']['steps'] ?? array() ) - 1 ]['step'] ?? '' ), 'Second caller import must run after shared runtime steps.' ); expect( false === ( $agentic['preview_only'] ?? true ), 'Default browser session must remain agentic.' ); expect( 'inherited-provider' === ( $agentic['provider'] ?? '' ), 'Default browser session must retain provider inheritance.' );