diff --git a/packages/cli/src/commands/recipe-runtime-setup.ts b/packages/cli/src/commands/recipe-runtime-setup.ts index aa7fe3fe5..37b2b4569 100644 --- a/packages/cli/src/commands/recipe-runtime-setup.ts +++ b/packages/cli/src/commands/recipe-runtime-setup.ts @@ -127,19 +127,20 @@ export async function applyRecipeRuntimeSetup(args: { interruption?.throwIfInterrupted() } + const extraPluginMounts: MountSpec[] = extraPlugins.map((plugin) => ({ + type: "directory", + source: plugin.source, + target: plugin.target, + mode: "readonly", + metadata: { + kind: "extra-plugin", + slug: plugin.slug, + source: plugin.provenance, + }, + })) await phaseTracker.run("mount_plugins", phasePluginMountData(extraPlugins), async () => { - for (const plugin of extraPlugins) { - await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount({ - type: "directory", - source: plugin.source, - target: plugin.target, - mode: "readonly", - metadata: { - kind: "extra-plugin", - slug: plugin.slug, - source: plugin.provenance, - }, - })) + for (const [index, plugin] of extraPlugins.entries()) { + await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount(extraPluginMounts[index])) interruption?.throwIfInterrupted() } }) @@ -190,6 +191,7 @@ export async function applyRecipeRuntimeSetup(args: { } const materializableMounts: MountSpec[] = [ + ...extraPluginMounts, ...inputMounts, ...stagedFiles.map((stagedFile) => ({ type: stagedFile.type, diff --git a/packages/runtime-playground/src/phpunit-command-handlers.ts b/packages/runtime-playground/src/phpunit-command-handlers.ts index b54a6bdb1..bc25ef148 100644 --- a/packages/runtime-playground/src/phpunit-command-handlers.ts +++ b/packages/runtime-playground/src/phpunit-command-handlers.ts @@ -21,6 +21,7 @@ export interface PhpunitRunCodeOptions { bootstrapMode: string projectBootstrap: string multisite: boolean + preinstalledMultisite?: boolean databaseType: "sqlite" | "mysql" /** * Sandbox-internal, writable path for the structured diagnostics log. Defaults @@ -30,6 +31,8 @@ export interface PhpunitRunCodeOptions { resultFile?: string } +export type PhpunitMultisitePreinstallCodeOptions = Pick + export interface CorePhpunitRunCodeOptions { coreRoot: string testsDir: string @@ -334,6 +337,101 @@ function ${functionName}(array $argv) { }` } +function managedPhpunitConfigWriterPhp(): string { + return `function pg_write_managed_test_config(array $extra_defines, string $table_prefix, string $database_type): string { + $config_path = '/tmp/wp-tests-config.php'; + $config = " getenv('DB_NAME') ?: 'runtime', + 'DB_USER' => getenv('DB_USER') ?: 'runtime', + 'DB_PASSWORD' => getenv('DB_PASSWORD') ?: '', + 'DB_HOST' => $db_host, + ) as $name => $value) { + $config .= 'define(' . var_export($name, true) . ', ' . var_export($value, true) . ");\n"; + } + } else { + $config .= <<<'CONFIG' +define('DB_NAME', ':memory:'); +define('DB_USER', 'root'); +define('DB_PASSWORD', ''); +define('DB_HOST', 'localhost'); +CONFIG; + } + $config .= <<<'CONFIG' +define('DB_CHARSET', 'utf8'); +define('WP_TESTS_DOMAIN', 'example.org'); +define('WP_TESTS_EMAIL', 'admin@example.org'); +define('WP_TESTS_TITLE', 'Test Blog'); +define('WP_PHP_BINARY', 'php'); +define('ABSPATH', '/wordpress/'); +define('FS_CHMOD_FILE', 0644); +define('FS_CHMOD_DIR', 0755); +define('FS_METHOD', 'direct'); +CONFIG; + file_put_contents($config_path, $config); + return $config_path; +}` +} + +export function phpunitMultisitePreinstallCode(options: PhpunitMultisitePreinstallCodeOptions): string { + const wpConfigDefines = { ...options.wpConfigDefines } + for (const name of ["MULTISITE", "SUBDOMAIN_INSTALL", "DOMAIN_CURRENT_SITE", "PATH_CURRENT_SITE", "SITE_ID_CURRENT_SITE", "BLOG_ID_CURRENT_SITE"]) { + delete wpConfigDefines[name] + } + return `error_reporting(E_ALL); +ini_set('display_errors', '1'); +ini_set('display_startup_errors', '1'); + +$tests_dir = ${JSON.stringify(options.testsDir)}; +$bench_env = json_decode(${JSON.stringify(JSON.stringify(options.env))}, true); +$wp_config_defines = json_decode(${JSON.stringify(JSON.stringify(wpConfigDefines))}, true); +$database_type = ${JSON.stringify(options.databaseType)}; +$result_file = ${JSON.stringify(options.resultFile ?? PLUGIN_PHPUNIT_RESULT_FILE)}; +$preinstall_complete = false; + +@file_put_contents($result_file, ''); +register_shutdown_function(static function () use (&$preinstall_complete, $result_file): void { + if ($preinstall_complete) { + return; + } + $failure = error_get_last(); + if (is_array($failure) && in_array($failure['type'] ?? 0, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR), true)) { + @file_put_contents($result_file, 'STAGE_FATAL:preinstall:' . (string) ($failure['message'] ?? '') . ' at ' . (string) ($failure['file'] ?? '') . ':' . (int) ($failure['line'] ?? 0) . "\n", FILE_APPEND); + } +}); + +${phpEnvAssignmentFunction("pg_apply_env", "json_encode", "error_log('Skipping invalid environment key: ' . var_export($name, true));")} +${phpWpConfigDefineAppenderFunction("pg_append_wp_config_defines", "error_log('Skipping invalid wp_config_defines key: ' . var_export($name, true));")} +${managedPhpunitConfigWriterPhp()} + +pg_apply_env($bench_env); +$config_path = pg_write_managed_test_config($wp_config_defines, 'wptests_', $database_type); +if (!defined('WP_TESTS_MULTISITE')) { + define('WP_TESTS_MULTISITE', true); +} +$argv = array('install.php', $config_path, 'run_ms_tests', 'no_core_tests'); +$_SERVER['argv'] = $argv; +$_SERVER['argc'] = count($argv); +try { + require $tests_dir . '/includes/install.php'; + $preinstall_complete = true; +} catch (Throwable $error) { + @file_put_contents($result_file, 'STAGE_FAIL:preinstall:' . get_class($error) . ': ' . $error->getMessage() . ' at ' . $error->getFile() . ':' . $error->getLine() . "\n", FILE_APPEND); + throw $error; +}` +} + export function phpunitRunCode(options: PhpunitRunCodeOptions): string { return `error_reporting(E_ALL); ini_set('display_errors', '1'); @@ -362,6 +460,7 @@ $preload_files = json_decode(${JSON.stringify(JSON.stringify(options.preloadFile $bootstrap_mode = ${JSON.stringify(options.bootstrapMode || "managed")}; $project_bootstrap = ${JSON.stringify(options.projectBootstrap)}; $multisite = ${JSON.stringify(options.multisite)}; +$preinstalled_multisite = ${JSON.stringify(options.preinstalledMultisite ?? false)}; $database_type = ${JSON.stringify(options.databaseType)}; function pg_build_phpunit_argv($raw): array { @@ -415,6 +514,8 @@ ${phpEnvAssignmentFunction("pg_apply_env", "json_encode", "pg_log('NOTICE: skipp ${phpWpConfigDefineAppenderFunction("pg_append_wp_config_defines", "pg_log('NOTICE: skipping invalid wp_config_defines key: ' . var_export($name, true));")} +${managedPhpunitConfigWriterPhp()} + function pg_diagnostic_context(): string { global $current_stage; $hook = function_exists('current_filter') ? current_filter() : null; @@ -661,48 +762,8 @@ function pg_run_boot_stage(array $cfg = []): ?string { $autoload_required = !empty($cfg['autoload_required']); $extra_defines = $cfg['extra_defines'] ?? array(); $table_prefix = isset($cfg['table_prefix']) && is_string($cfg['table_prefix']) && $cfg['table_prefix'] !== '' ? $cfg['table_prefix'] : 'wptests_'; - $config_path = '/tmp/wp-tests-config.php'; - $config = " getenv('DB_NAME') ?: 'runtime', - 'DB_USER' => getenv('DB_USER') ?: 'runtime', - 'DB_PASSWORD' => getenv('DB_PASSWORD') ?: '', - 'DB_HOST' => $db_host, - ) as $name => $value) { - $config .= 'define(' . var_export($name, true) . ', ' . var_export($value, true) . ");\n"; - } - } else { - $config .= <<<'CONFIG' -define('DB_NAME', ':memory:'); -define('DB_USER', 'root'); -define('DB_PASSWORD', ''); -define('DB_HOST', 'localhost'); -CONFIG; - } - $config .= <<<'CONFIG' -define('DB_CHARSET', 'utf8'); -define('WP_TESTS_DOMAIN', 'example.org'); -define('WP_TESTS_EMAIL', 'admin@example.org'); -define('WP_TESTS_TITLE', 'Test Blog'); -define('WP_PHP_BINARY', 'php'); -define('ABSPATH', '/wordpress/'); -define('FS_CHMOD_FILE', 0644); -define('FS_CHMOD_DIR', 0755); -define('FS_METHOD', 'direct'); -CONFIG; - file_put_contents($config_path, $config); + $config_path = pg_write_managed_test_config($extra_defines, $table_prefix, $database_type); if ($harness_autoload !== '' && is_readable($harness_autoload)) { pg_preload_wp_cli_namespaced_functions($harness_autoload); require_once $harness_autoload; @@ -719,6 +780,37 @@ CONFIG; } } +function pg_run_preinstalled_multisite_stage(array $cfg): void { + pg_stage_begin('install'); + try { + $config_path = (string) ($cfg['config_path'] ?? ''); + $tests_dir = (string) ($cfg['tests_dir'] ?? ''); + if ($config_path === '' || !is_readable($config_path)) { + throw new RuntimeException('managed multisite config is not readable: ' . $config_path); + } + if ($tests_dir === '' || !is_readable($tests_dir . '/includes/functions.php')) { + throw new RuntimeException('managed multisite test library is not readable: ' . $tests_dir); + } + if (!defined('WP_INSTALLING')) { + define('WP_INSTALLING', true); + } + if (!defined('DISABLE_WP_CRON')) { + define('DISABLE_WP_CRON', true); + } + require_once $config_path; + require_once $tests_dir . '/includes/functions.php'; + tests_reset__SERVER(); + $GLOBALS['PHP_SELF'] = '/index.php'; + $_SERVER['PHP_SELF'] = '/index.php'; + tests_add_filter('wp_die_handler', '_wp_die_handler_filter_exit'); + require_once ABSPATH . 'wp-settings.php'; + pg_stage_ok('install'); + } catch (Throwable $e) { + pg_stage_fail('install', $e); + exit(1); + } +} + function pg_resolve_runtime_cwd(string $cwd, string $plugin_path): string { $cwd = trim(str_replace('\\\\', '/', $cwd)); if ($cwd === '') { @@ -1163,7 +1255,11 @@ tests_add_filter('muplugins_loaded', function () use ($plugin_slug, $plugin_path $deferred_install_init_callbacks = pg_defer_new_wordpress_hook_callbacks('init', $pre_component_init_callbacks); }); -pg_run_install_stage(array('config_path' => $config_path, 'tests_dir' => $tests_dir, 'multisite' => $multisite)); +if ($preinstalled_multisite) { + pg_run_preinstalled_multisite_stage(array('config_path' => $config_path, 'tests_dir' => $tests_dir)); +} else { + pg_run_install_stage(array('config_path' => $config_path, 'tests_dir' => $tests_dir, 'multisite' => $multisite)); +} pg_remove_new_wordpress_hook_callbacks('shutdown', $pre_component_shutdown_callbacks); $pre_dependency_plugins_loaded_callbacks = pg_snapshot_wordpress_hook_callbacks('plugins_loaded'); $pre_dependency_init_callbacks = pg_snapshot_wordpress_hook_callbacks('init'); diff --git a/packages/runtime-playground/src/wordpress-command-runners.ts b/packages/runtime-playground/src/wordpress-command-runners.ts index 706de66a9..571e8d5ca 100644 --- a/packages/runtime-playground/src/wordpress-command-runners.ts +++ b/packages/runtime-playground/src/wordpress-command-runners.ts @@ -30,6 +30,7 @@ import { restPerformanceObservationInputFromArgs, restPerformanceObservationPhpCode, phpunitRunCode, + phpunitMultisitePreinstallCode, pluginStateInputFromArgs, pluginStatePhpCode, PLUGIN_PHPUNIT_RESULT_FILE, @@ -928,6 +929,7 @@ export async function runPhpunitCommand({ const autoloadFile = argValue(args, "autoload-file")?.trim() || (bootstrapMode === "project" ? "" : "/wp-codebox-vendor/autoload.php") const autoloadFileRole = argValue(args, "autoload-file-role")?.trim() === "harness" ? "harness" : undefined const processIdentity = boundedProcessIdentity(spec.processIdentity) + const multisite = booleanArg(args, "multisite") const resultFile = processIdentity ? `/tmp/wp-codebox-phpunit-result-${processIdentity}.txt` : PLUGIN_PHPUNIT_RESULT_FILE const diagnosticHostFile = `/wordpress/wp-content/plugins/${pluginSlug}/.pg-test-result${processIdentity ? `-${processIdentity}` : ""}.txt` const code = explicitCode ? await phpCodeFromArgs(args, "wordpress.phpunit", false) : phpunitRunCode({ @@ -950,7 +952,8 @@ export async function runPhpunitCommand({ preloadFiles: jsonArrayArg(args, "preload-files-json").filter((value): value is string => typeof value === "string"), bootstrapMode, projectBootstrap: argValue(args, "project-bootstrap")?.trim() || "", - multisite: booleanArg(args, "multisite"), + multisite, + preinstalledMultisite: bootstrapMode === "managed" && databaseType === "mysql" && multisite, databaseType, resultFile, }) @@ -960,6 +963,17 @@ export async function runPhpunitCommand({ let response: PlaygroundRunResponse try { const bootstrapArgs = explicitCode ? args : [...args, "bootstrap=runtime-only"] + if (!explicitCode && bootstrapMode === "managed" && databaseType === "mysql" && multisite) { + const preinstallCode = phpunitMultisitePreinstallCode({ + testsDir: argValue(args, "tests-dir")?.trim() || "/wp-codebox-vendor/wp-phpunit/wp-phpunit", + env: jsonObjectArg(args, "env-json"), + wpConfigDefines: jsonObjectArg(args, "wp-config-defines-json"), + databaseType, + resultFile, + }) + const preinstallResponse = await runPlaygroundCommand("wordpress.phpunit", server, { code: bootstrapPhpCode(runtimeSpec, preinstallCode, bootstrapArgs) }) + assertPlaygroundResponseOk("wordpress.phpunit multisite preinstall", preinstallResponse) + } response = await runPlaygroundCommand("wordpress.phpunit", server, { code: bootstrapPhpCode(runtimeSpec, code, bootstrapArgs, undefined, resultFile) }) } catch (error) { await persistPluginPhpunitResult(server, resultFile, artifactRoot, processIdentity) diff --git a/tests/disposable-mysql-mysqli.integration.test.ts b/tests/disposable-mysql-mysqli.integration.test.ts index 453adc8c1..26cabe850 100644 --- a/tests/disposable-mysql-mysqli.integration.test.ts +++ b/tests/disposable-mysql-mysqli.integration.test.ts @@ -149,9 +149,12 @@ require_once ABSPATH . 'wp-settings.php'; assert.deepEqual(aggregate.entries.map((entry: { inputIndex: number }) => entry.inputIndex), [0, 1]) const multisitePlugin = join(directory, "managed-multisite-fixture") + const multisiteDependency = join(directory, "managed-multisite-dependency") const multisiteRecipePath = join(directory, "managed-multisite-recipe.json") const multisiteArtifacts = join(directory, "managed-multisite-artifacts") await mkdir(join(multisitePlugin, "tests"), { recursive: true }) + await mkdir(multisiteDependency, { recursive: true }) + await writeFile(join(multisiteDependency, "managed-multisite-dependency.php"), "assertInstanceOf(mysqli::class, $wpdb->dbh); $this->assertSame('1', (string) $wpdb->get_var("SELECT JSON_VALID('{\\"valid\\":true}')")); } + + public function test_network_schema_seed_and_dependency_are_materialized(): void { + global $wpdb; + $this->assertGreaterThanOrEqual(1, (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->blogs} WHERE blog_id = 1")); + $this->assertSame($wpdb->sitemeta, $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->sitemeta))); + update_network_option(1, 'managed_multisite_fixture', 'available'); + $this->assertSame('available', get_network_option(1, 'managed_multisite_fixture')); + $this->assertFileExists('/wordpress/wp-content/plugins/managed-multisite-dependency/managed-multisite-dependency.php'); + } } `) const multisiteRecipe = buildWordPressPhpunitRecipe({ @@ -197,7 +209,8 @@ final class ManagedMultisiteTest extends WP_UnitTestCase { databaseType: "mysql", multisite: true, pluginSource: multisitePlugin, - dependencyMounts: ["/wordpress/wp-content/plugins/managed-multisite-fixture"], + extra_plugins: [{ source: multisiteDependency, slug: "managed-multisite-dependency", activate: false }], + dependencyMounts: ["/wordpress/wp-content/plugins/managed-multisite-fixture", "/wordpress/wp-content/plugins/managed-multisite-dependency"], mounts: [{ source: join(harness, "vendor"), target: "/wp-codebox-vendor", mode: "readonly" }], }) await writeFile(multisiteRecipePath, `${JSON.stringify(multisiteRecipe)}\n`) diff --git a/tests/phpunit-project-autoload.test.ts b/tests/phpunit-project-autoload.test.ts index 78b4b9582..95287ad40 100644 --- a/tests/phpunit-project-autoload.test.ts +++ b/tests/phpunit-project-autoload.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os" import { join } from "node:path" import { buildWordPressPhpunitRecipe } from "../packages/runtime-core/src/recipe-builders.js" -import { corePhpunitRunCode, phpunitRunCode } from "../packages/runtime-playground/src/phpunit-command-handlers.js" +import { corePhpunitRunCode, phpunitMultisitePreinstallCode, phpunitRunCode } from "../packages/runtime-playground/src/phpunit-command-handlers.js" import { runPhpunitCommand } from "../packages/runtime-playground/src/wordpress-command-runners.js" import { recipePolicy } from "../packages/cli/src/recipe-validation.js" import { recipeExtraPluginSourceSubpath } from "../packages/cli/src/recipe-sources.js" @@ -13,6 +13,7 @@ import { recipeInputMountPathMap, rewriteInputMountPathArgs } from "../packages/ const woocommerceAutoload = "/wordpress/wp-content/plugins/woocommerce/vendor/autoload_packages.php" const phpunitRuntimeSpec = { + environment: { kind: "wordpress", name: "test", version: "latest" }, runtimeEnv: { TC_MYSQL_PORT: "3306" }, } as never @@ -666,6 +667,75 @@ const decodedMysqlCode = decodedBootstrapWrapper(capturedMysqlCode) assert.ok(decodedMysqlCode.includes('$database_type = "mysql";')) assert.ok(decodedMysqlCode.includes("'DB_HOST' => $db_host"), "managed PHPUnit writes the provisioned MySQL host into wp-tests-config.php") assert.ok(decodedMysqlCode.includes("getenv('DB_PASSWORD')"), "managed PHPUnit consumes the provisioned MySQL credentials") + +const preinstallCode = phpunitMultisitePreinstallCode({ + testsDir: "/wp-codebox-vendor/wp-phpunit/wp-phpunit", + env: {}, + wpConfigDefines: { MULTISITE: true, DOMAIN_CURRENT_SITE: "example.org" }, + databaseType: "mysql", + resultFile: "/tmp/preinstall-result.txt", +}) +assert.ok(preinstallCode.includes("'run_ms_tests'"), "multisite preinstall selects wp-phpunit's network installer") +assert.ok(preinstallCode.includes("define('WP_TESTS_MULTISITE', true)"), "multisite preinstall enables wp-phpunit network installation") +assert.ok(preinstallCode.includes("require $tests_dir . '/includes/install.php'"), "multisite preinstall uses wp-phpunit's installer") +assert.ok(preinstallCode.includes("pg_write_managed_test_config($wp_config_defines, 'wptests_', $database_type)"), "multisite preinstall uses the managed runner's database config and prefix") +assert.equal(preinstallCode.includes("define('MULTISITE'"), false, "multisite preinstall must not bootstrap WordPress as an installed network") +assert.equal(preinstallCode.includes("DOMAIN_CURRENT_SITE"), false, "multisite preinstall must not define current-network constants") +assert.ok(preinstallCode.includes("@file_put_contents($result_file, '');"), "multisite preinstall clears stale diagnostics") +assert.ok(preinstallCode.includes("STAGE_FAIL:preinstall:"), "multisite preinstall records throwable diagnostics") +assert.ok(preinstallCode.includes("STAGE_FATAL:preinstall:"), "multisite preinstall records fatal diagnostics") + +const mysqlMultisiteInvocations: string[] = [] +await runPhpunitCommand({ + artifactRoot: mkdtempSync(join(tmpdir(), "wp-codebox-phpunit-mysql-multisite-")), + mounts: [], + runPlaygroundCommand: async (_command, _server, input) => { + mysqlMultisiteInvocations.push(decodedBootstrapWrapper(input.code)) + return { text: "ok", exitCode: 0 } + }, + runtimeSpec: { + environment: { kind: "wordpress", name: "test", version: "latest", databaseSetup: "external" }, + runtimeEnv: { DB_HOST: "127.0.0.1", DB_PORT: "3307", DB_NAME: "runtime", DB_USER: "runtime", DB_PASSWORD: "secret" }, + policy: { commands: ["wordpress.phpunit"] }, + } as never, + server: { playground: {} } as never, + spec: { command: "wordpress.phpunit", args: ["plugin-slug=demo-plugin", "database-type=mysql", "multisite=1"] }, +}) +assert.equal(mysqlMultisiteInvocations.length, 2, "managed MySQL multisite runs a separate preinstall before PHPUnit") +assert.ok(mysqlMultisiteInvocations[0].includes("'run_ms_tests'")) +assert.ok(mysqlMultisiteInvocations[1].includes("$phpunit_argv = pg_build_phpunit_argv"), "normal managed PHPUnit behavior follows preinstall") +assert.ok(mysqlMultisiteInvocations[1].includes("$preinstalled_multisite = true"), "normal managed PHPUnit preserves the preinstalled network") +assert.ok(mysqlMultisiteInvocations[1].includes("pg_run_preinstalled_multisite_stage"), "normal managed PHPUnit boots without destructively reinstalling multisite") + +const failedPreinstallInvocations: string[] = [] +await assert.rejects( + runPhpunitCommand({ + artifactRoot: mkdtempSync(join(tmpdir(), "wp-codebox-phpunit-mysql-multisite-failure-")), + mounts: [], + runPlaygroundCommand: async (_command, _server, input) => { + failedPreinstallInvocations.push(decodedBootstrapWrapper(input.code)) + return { text: "preinstall failed", exitCode: 1 } + }, + runtimeSpec: { + environment: { kind: "wordpress", name: "test", version: "latest", databaseSetup: "external" }, + runtimeEnv: { DB_HOST: "127.0.0.1", DB_PORT: "3307", DB_NAME: "runtime", DB_USER: "runtime", DB_PASSWORD: "secret" }, + policy: { commands: ["wordpress.phpunit"] }, + } as never, + server: { + playground: { + readFileAsText: async () => "STAGE_FAIL:preinstall:RuntimeException: network install failed", + }, + } as never, + spec: { command: "wordpress.phpunit", args: ["plugin-slug=demo-plugin", "database-type=mysql", "multisite=1"] }, + }), + (error: Error) => { + assert.match(error.message, /wordpress\.phpunit multisite preinstall failed with exit code 1/) + assert.match(error.message, /wordpress\.phpunit structured diagnostics/) + assert.match(error.message, /network install failed/) + return true + }, +) +assert.equal(failedPreinstallInvocations.length, 1, "failed multisite preinstall prevents the main PHPUnit invocation") await assert.rejects(runPhpunitCommand({ artifactRoot: mkdtempSync(join(tmpdir(), "wp-codebox-phpunit-mysql-reject-")), mounts: [], diff --git a/tests/recipe-runtime-setup-staged-materialization.test.ts b/tests/recipe-runtime-setup-staged-materialization.test.ts index 51ec10dbd..98add518a 100644 --- a/tests/recipe-runtime-setup-staged-materialization.test.ts +++ b/tests/recipe-runtime-setup-staged-materialization.test.ts @@ -25,7 +25,16 @@ const recipe: WorkspaceRecipe = { } const prepared: PreparedRecipeRuntimeSetup = { workspaceMounts: [], - extraPlugins: [], + extraPlugins: [{ + source: "/tmp/host-extra-plugin", + slug: "fixture-dependency", + target: "/wordpress/wp-content/plugins/fixture-dependency", + pluginFile: "fixture-dependency/fixture-dependency.php", + activate: false, + loadAs: "plugin", + cleanupPaths: [], + provenance: { kind: "local", original: "/tmp/host-extra-plugin" }, + }], dependencyOverlays: [], overlays: [], inputMountBaselinePaths: [], @@ -55,6 +64,17 @@ const runtime = { assert.equal(this, runtime, "setup calls staged input materializer with runtime binding") calls.push(`materialize:${mounts.map((mount) => mount.target).join(",")}`) assert.deepEqual(mounts, [ + { + type: "directory", + source: "/tmp/host-extra-plugin", + target: "/wordpress/wp-content/plugins/fixture-dependency", + mode: "readonly", + metadata: { + kind: "extra-plugin", + slug: "fixture-dependency", + source: { kind: "local", original: "/tmp/host-extra-plugin" }, + }, + }, { type: "directory", source: inputMountSource, @@ -102,15 +122,18 @@ try { } const mountIndex = calls.indexOf("mount:/workspace/example/bundles/runtime-agent") +const extraPluginMountIndex = calls.indexOf("mount:/wordpress/wp-content/plugins/fixture-dependency") const inputMountIndex = calls.indexOf(`mount:${prepared.inputMountPathMap[0].canonicalTarget}`) const materializeOperationIndex = calls.indexOf("operation:input.materialize") -const materializeIndex = calls.indexOf(`materialize:${prepared.inputMountPathMap[0].canonicalTarget},/workspace/example/bundles/runtime-agent`) +const materializeIndex = calls.indexOf(`materialize:/wordpress/wp-content/plugins/fixture-dependency,${prepared.inputMountPathMap[0].canonicalTarget},/workspace/example/bundles/runtime-agent`) assert.ok(inputMountIndex >= 0, "input source is mounted") assert.match(prepared.inputMountPathMap[0].canonicalTarget, /^\/tmp\/wp-codebox-inputs\/0-public_html-[a-f0-9]{12}$/) assert.ok(calls.includes(`metadata:/home/example/public_html->${prepared.inputMountPathMap[0].canonicalTarget}`), "input mount metadata records original and canonical targets") assert.ok(mountIndex >= 0, "staged source is mounted") +assert.ok(extraPluginMountIndex >= 0, "prepared extra plugin is mounted") assert.ok(materializeOperationIndex > inputMountIndex, "materialization is scheduled after input mount") assert.ok(materializeOperationIndex > mountIndex, "materialization is scheduled after staged mount") +assert.ok(materializeOperationIndex > extraPluginMountIndex, "materialization is scheduled after extra-plugin mount") assert.ok(materializeIndex > materializeOperationIndex, "materialization runs inside the setup phase before commands") assert.equal(calls.some((call) => call.startsWith("execute:")), false)