Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions includes/class-static-site-importer-theme-generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ public static function import_website_artifact( array $artifact, array $args = a
'status' => 'dependencies_prepared',
'runtime_lifecycle' => $lifecycle,
'dependencies' => $dependencies,
'fresh_runtime' => array( 'request_id' => self::runtime_request_id() ),
'fresh_runtime' => array( 'request_id' => (string) ( $args['runtime_lifecycle_invocation_id'] ?? '' ) ),
);
}
if ( 'resume' === ( $args['runtime_lifecycle_phase'] ?? '' ) && (string) ( $args['runtime_lifecycle_request_id'] ?? '' ) === self::runtime_request_id() ) {
$prepared_invocation = (string) ( $args['runtime_lifecycle_request_id'] ?? '' );
$current_invocation = (string) ( $args['runtime_lifecycle_invocation_id'] ?? '' );
if ( 'resume' === ( $args['runtime_lifecycle_phase'] ?? '' ) && '' !== $prepared_invocation && $prepared_invocation === $current_invocation ) {
return new WP_Error( 'static_site_importer_fresh_runtime_required', 'Provider validation must resume in a fresh WordPress request after dependency preparation.' );
}

Expand Down Expand Up @@ -864,11 +866,6 @@ private static function prepare_wordpress_site_plan_lifecycle( array $plan, arra
return $lifecycle;
}

/** A CLI process id is sufficient to reject same-request phase resumes. */
private static function runtime_request_id(): string {
return (string) ( function_exists( 'getmypid' ) ? getmypid() : spl_object_id( new stdClass() ) );
}

private static function runtime_declaration_is_required( array $declaration, array $declarations ): bool {
$key = (string) ( $declaration['kind'] ?? '' ) . ':' . (string) ( $declaration['type'] ?? $declaration['capability'] ?? '' );
if ( ! empty( $declaration['required_for'] ) ) {
Expand Down
24 changes: 19 additions & 5 deletions includes/class-static-site-importer-validation-runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ public static function fixture_matrix_result( array $result ): array {
/**
* Validate a website artifact in the current runtime.
*
* @param array<string,mixed> $input Validation input.
* @param array<string,mixed> $input Validation input.
* @param string $invocation_id Server-owned identity for this invocation.
* @return array<string,mixed>|WP_Error
*/
public static function validate_artifact( array $input ) {
public static function validate_artifact( array $input, string $invocation_id = '' ) {
$artifact = isset( $input['artifact'] ) && is_array( $input['artifact'] ) ? $input['artifact'] : array();
if ( empty( $artifact ) ) {
return new WP_Error( 'static_site_importer_validation_artifact_missing', 'Validation requires an artifact JSON object.' );
Expand Down Expand Up @@ -103,6 +104,7 @@ public static function validate_artifact( array $input ) {
if ( isset( $input['runtime_lifecycle_request_id'] ) ) {
$import_args['runtime_lifecycle_request_id'] = (string) $input['runtime_lifecycle_request_id'];
}
$import_args['runtime_lifecycle_invocation_id'] = self::invocation_id( $invocation_id );

$result = Static_Site_Importer_Theme_Generator::import_website_artifact( $artifact, $import_args );
if ( is_wp_error( $result ) ) {
Expand All @@ -112,8 +114,14 @@ public static function validate_artifact( array $input ) {
return self::result_from_import( $result, $artifact_dir, $import_args );
}

/** Prepare plugin dependencies in this request; callers resume validation in another request. */
public static function prepare_artifact_dependencies( array $input ) {
/**
* Prepare plugin dependencies in this request; callers resume validation in another request.
*
* @param array<string,mixed> $input Validation input.
* @param string $invocation_id Server-owned identity for this invocation.
* @return array<string,mixed>|WP_Error
*/
public static function prepare_artifact_dependencies( array $input, string $invocation_id = '' ) {
$input['runtime_lifecycle_phase'] = 'prepare';
$input['materialize_dependencies'] = true;
$artifact = isset( $input['artifact'] ) && is_array( $input['artifact'] ) ? $input['artifact'] : array();
Expand All @@ -132,7 +140,8 @@ public static function prepare_artifact_dependencies( array $input ) {
)
);
$import_args['runtime_lifecycle_phase'] = 'prepare';
$result = Static_Site_Importer_Theme_Generator::import_website_artifact( $artifact, $import_args );
$import_args['runtime_lifecycle_invocation_id'] = self::invocation_id( $invocation_id );
$result = Static_Site_Importer_Theme_Generator::import_website_artifact( $artifact, $import_args );
if ( is_wp_error( $result ) ) {
return $result;
}
Expand All @@ -148,6 +157,11 @@ public static function prepare_artifact_dependencies( array $input ) {
);
}

/** Resolve the server-owned identity for one validation invocation. */
private static function invocation_id( string $invocation_id ): string {
return '' !== $invocation_id ? $invocation_id : wp_generate_uuid4();
}

/** Build a registry-derived dependency plan without installing packages. */
public static function plan_artifact_dependencies( array $input ) {
$artifact = isset( $input['artifact'] ) && is_array( $input['artifact'] ) ? $input['artifact'] : array();
Expand Down
80 changes: 78 additions & 2 deletions tests/smoke-validation-runtime-diagnostics.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,56 @@ function wp_mkdir_p( $directory ) {
}
}

if ( ! function_exists( 'wp_generate_uuid4' ) ) {
function wp_generate_uuid4() {
static $sequence = 0;
++$sequence;

return '00000000-0000-4000-8000-' . str_pad( (string) $sequence, 12, '0', STR_PAD_LEFT );
}
}

if ( ! function_exists( 'wp_json_encode' ) ) {
function wp_json_encode( $value ) {
return json_encode( $value );
}
}

if ( ! class_exists( 'WP_Error' ) ) {
class WP_Error {
private string $code;

public function __construct( string $code ) {
$this->code = $code;
}

public function get_error_code(): string {
return $this->code;
}
}
}

if ( ! function_exists( 'is_wp_error' ) ) {
function is_wp_error( $value ) {
return false;
return $value instanceof WP_Error;
}
}

if ( ! class_exists( 'Static_Site_Importer_Theme_Generator' ) ) {
class Static_Site_Importer_Theme_Generator {
public static array $last_args = array();

public static function import_website_artifact( array $artifact, array $args = array() ): array {
public static function import_website_artifact( array $artifact, array $args = array() ) {
self::$last_args = $args;
if ( 'prepare' === ( $args['runtime_lifecycle_phase'] ?? '' ) ) {
return array(
'status' => 'dependencies_prepared',
'fresh_runtime' => array( 'request_id' => $args['runtime_lifecycle_invocation_id'] ?? '' ),
);
}
if ( 'resume' === ( $args['runtime_lifecycle_phase'] ?? '' ) && ( $args['runtime_lifecycle_request_id'] ?? '' ) === ( $args['runtime_lifecycle_invocation_id'] ?? '' ) ) {
return new WP_Error( 'static_site_importer_fresh_runtime_required' );
}

return array(
'quality' => array( 'pass' => true ),
Expand Down Expand Up @@ -202,9 +240,47 @@ public static function import_website_artifact( array $artifact, array $args = a
$assert( false === ( Static_Site_Importer_Theme_Generator::$last_args['materialize_dependencies'] ?? null ), 'validation-honors-disabled-dependency-materialization' );
$assert( false === ( $override_result['request']['import_args']['materialize_dependencies'] ?? null ), 'validation-result-records-disabled-dependency-materialization' );

$lifecycle_artifact = array( 'schema' => 'test/website-artifact/v1' );
$prepare_receipt = Static_Site_Importer_Validation_Runtime::prepare_artifact_dependencies(
array(
'artifact' => $lifecycle_artifact,
'slug' => 'persistent-worker',
)
);
$prepared_invocation = (string) ( $prepare_receipt['fresh_runtime']['request_id'] ?? '' );
$assert( '' !== $prepared_invocation, 'prepare-receipt-carries-invocation' );

$resume_artifact_dir = $artifact_dir . '/persistent-worker-resume';
$resume_result = Static_Site_Importer_Validation_Runtime::validate_artifact(
array(
'artifact' => $lifecycle_artifact,
'artifact_dir' => $resume_artifact_dir,
'slug' => 'persistent-worker',
'runtime_lifecycle_phase' => 'resume',
'runtime_lifecycle_request_id' => $prepare_receipt['fresh_runtime']['request_id'],
)
);
$assert( ! is_wp_error( $resume_result ), 'distinct-invocation-resume-proceeds-in-one-process' );
$assert( $prepared_invocation !== ( Static_Site_Importer_Theme_Generator::$last_args['runtime_lifecycle_invocation_id'] ?? '' ), 'separate-runtime-calls-receive-distinct-invocations' );

$same_invocation_artifact_dir = $artifact_dir . '/same-invocation-resume';
$same_invocation_result = Static_Site_Importer_Validation_Runtime::validate_artifact(
array(
'artifact' => $lifecycle_artifact,
'artifact_dir' => $same_invocation_artifact_dir,
'slug' => 'same-invocation',
'runtime_lifecycle_phase' => 'resume',
'runtime_lifecycle_request_id' => $prepare_receipt['fresh_runtime']['request_id'],
),
$prepared_invocation
);
$assert( is_wp_error( $same_invocation_result ) && 'static_site_importer_fresh_runtime_required' === $same_invocation_result->get_error_code(), 'same-invocation-resume-rejected' );

unlink( $report_path );
rmdir( $default_artifact_dir );
rmdir( $override_artifact_dir );
rmdir( $resume_artifact_dir );
rmdir( $same_invocation_artifact_dir );
rmdir( $artifact_dir );

if ( $failures ) {
Expand Down
6 changes: 6 additions & 0 deletions tests/smoke-website-artifact-import-input.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ function wp_mkdir_p( $directory ) {
}
}

if ( ! function_exists( 'wp_generate_uuid4' ) ) {
function wp_generate_uuid4(): string {
return '00000000-0000-4000-8000-000000000001';
}
}

if ( ! function_exists( 'is_wp_error' ) ) {
function is_wp_error( $value ) {
return $value instanceof WP_Error;
Expand Down