From 2e2de54de3b9517d5463dec52b55828a292754b8 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 20 Jul 2026 21:51:08 -0400 Subject: [PATCH] Retain PHPUnit bootstrap failure diagnostics --- package.json | 2 + .../runtime-playground/src/php-bootstrap.ts | 13 ++- .../src/playground-command-errors.ts | 6 +- .../src/wordpress-command-runners.ts | 12 ++- ...hpunit-runtime-failure-diagnostics.test.ts | 49 +++++++++++ ...unit-bootstrap-failure.integration.test.ts | 83 +++++++++++++++++++ 6 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 tests/phpunit-runtime-failure-diagnostics.test.ts create mode 100644 tests/playground-phpunit-bootstrap-failure.integration.test.ts diff --git a/package.json b/package.json index bc22656e..e5d09a9a 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,8 @@ "test:playground-readonly-mounts": "tsx tests/playground-readonly-mounts.test.ts", "test:playground-readonly-mounts-integration": "tsx tests/playground-readonly-mounts-integration.test.ts", "test:playground-phpunit-readonly-cache-integration": "tsx tests/playground-phpunit-readonly-cache.integration.test.ts", + "test:playground-phpunit-bootstrap-failure-integration": "tsx tests/playground-phpunit-bootstrap-failure.integration.test.ts", + "test:phpunit-runtime-failure-diagnostics": "tsx tests/phpunit-runtime-failure-diagnostics.test.ts", "test:php-wasm-extension-manifests": "tsx tests/php-wasm-extension-manifests.test.ts", "test:browser-task-builder": "tsx tests/browser-task-builder.test.ts", "test:browser-runtime-generic-invoker": "tsx tests/browser-runtime-generic-invoker.test.ts", diff --git a/packages/runtime-playground/src/php-bootstrap.ts b/packages/runtime-playground/src/php-bootstrap.ts index 98951eff..a3da0ad3 100644 --- a/packages/runtime-playground/src/php-bootstrap.ts +++ b/packages/runtime-playground/src/php-bootstrap.ts @@ -20,7 +20,7 @@ require_once '/wordpress/wp-load.php'; ${phpBody(code)}` } -export function bootstrapPhpCode(spec: RuntimeCreateSpec, code: string, args: string[], wpCliBridge?: PhpBootstrapBridge): string { +export function bootstrapPhpCode(spec: RuntimeCreateSpec, code: string, args: string[], wpCliBridge?: PhpBootstrapBridge, failureDiagnosticFile?: string): string { if (argValue(args, "bootstrap") === "none") { return code } @@ -29,6 +29,7 @@ export function bootstrapPhpCode(spec: RuntimeCreateSpec, code: string, args: st return ` runPhpunitCommand({ + artifactRoot, + mounts: [], + runPlaygroundCommand: async (_command, _server, input) => { + submittedCode = input.code + return { exitCode: 1, errors: "", text: "" } + }, + runtimeSpec: { environment: { kind: "wordpress", name: "test", version: "latest" }, policy: { commands: ["wordpress.phpunit"] } } as never, + server: { + playground: { + readFileAsText: async (path: string) => { + assert.equal(path, PLUGIN_PHPUNIT_RESULT_FILE) + return `STAGE_FATAL:bootstrap:Bootstrap failed with token: ${secret} ${"x".repeat(25_000)}` + }, + }, + } as never, + spec: { command: "wordpress.phpunit", args: ["plugin-slug=demo-plugin"] }, + }), + (error: Error) => { + assert.match(error.message, /wordpress\.phpunit failed with exit code 1/) + assert.match(error.message, /wordpress\.phpunit structured diagnostics/) + assert.match(error.message, /Bootstrap failed with token: \[redacted\]/) + assert.match(error.message, /\[diagnostic truncated\]/) + assert.doesNotMatch(error.message, new RegExp(secret)) + return true + }, +) + +const captured = await readFile(join(artifactRoot, "files", "phpunit", ".pg-test-result.txt"), "utf8") +assert.match(captured, /Bootstrap failed with token: \[redacted\]/) +assert.doesNotMatch(captured, new RegExp(secret)) + +const preBootstrapRecorder = submittedCode.indexOf("STAGE_FATAL:bootstrap:") +const wordpressBootstrap = submittedCode.indexOf("require_once '/wordpress/wp-load.php';") +assert.ok(preBootstrapRecorder >= 0 && preBootstrapRecorder < wordpressBootstrap, "fatal diagnostics must be recorded before the WordPress bootstrap boundary") + +console.log("phpunit runtime failure diagnostics ok") diff --git a/tests/playground-phpunit-bootstrap-failure.integration.test.ts b/tests/playground-phpunit-bootstrap-failure.integration.test.ts new file mode 100644 index 00000000..8bad73c9 --- /dev/null +++ b/tests/playground-phpunit-bootstrap-failure.integration.test.ts @@ -0,0 +1,83 @@ +import assert from "node:assert/strict" +import { execFile } from "node:child_process" +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { promisify } from "node:util" + +const execFileAsync = promisify(execFile) +const secret = "sk-abcdefghijklmnopqrstuvwxyz" +const root = await mkdtemp(join(tmpdir(), "wp-codebox-phpunit-bootstrap-failure-")) +const fatalMuPlugin = join(root, "fatal-bootstrap.php") +const recipePath = join(root, "recipe.json") +const artifactsPath = join(root, "artifacts") + +try { + await writeFile(fatalMuPlugin, ` { + try { + const result = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", "recipe-run", "--recipe", recipePath, "--artifacts", artifactsPath, "--json"], { + cwd: process.cwd(), + timeout: 300_000, + maxBuffer: 2 * 1024 * 1024, + }) + return recipeRunOutput(result.stdout) + } catch (error) { + const output = recipeRunOutput(error && typeof error === "object" && "stdout" in error ? error.stdout : undefined) + if (output) { + return output + } + throw error + } +} + +interface RecipeRunOutput { + success?: boolean + error?: { message?: string } +} + +function recipeRunOutput(value: unknown): RecipeRunOutput { + assert.equal(typeof value, "string", "recipe-run must return JSON output") + return JSON.parse(value) as RecipeRunOutput +} + +console.log("playground phpunit bootstrap failure integration ok")