|
| 1 | +import assert from "node:assert/strict" |
| 2 | +import { mkdtemp, rm } from "node:fs/promises" |
| 3 | +import { tmpdir } from "node:os" |
| 4 | +import { join } from "node:path" |
| 5 | +import { createRuntime } from "../packages/runtime-core/src/index.js" |
| 6 | +import { createPlaygroundRuntimeBackend } from "../packages/runtime-playground/src/index.js" |
| 7 | +import { terminalizeOnPhpWasmRuntimeRejection } from "../packages/runtime-playground/src/playground-command-errors.js" |
| 8 | +import type { PlaygroundCliModule } from "../packages/runtime-playground/src/playground-cli-runner.js" |
| 9 | +import { executeRecipeWorkflowStep, recipeStepFailure } from "../packages/cli/src/commands/recipe-run-workflow-evidence.js" |
| 10 | + |
| 11 | +const root = await mkdtemp(join(tmpdir(), "wp-codebox-phpunit-runtime-rejection-")) |
| 12 | +const secret = "sk-abcdefghijklmnopqrstuvwxyz" |
| 13 | +const phpWasmFailure = new WebAssembly.RuntimeError(`null function or function signature mismatch ${secret}`) |
| 14 | +phpWasmFailure.stack = `RuntimeError: ${phpWasmFailure.message}\n at php.wasm.zif_mysqli_poll (wasm://wasm/php.wasm-05996276:wasm-function[12986]:0x9949a8)` |
| 15 | + |
| 16 | +const cliModule: PlaygroundCliModule = { |
| 17 | + runCLI: async () => ({ |
| 18 | + serverUrl: "http://127.0.0.1:9403", |
| 19 | + playground: { |
| 20 | + run: async () => { |
| 21 | + queueMicrotask(() => process.emit("unhandledRejection", phpWasmFailure, Promise.resolve())) |
| 22 | + return await new Promise<never>(() => undefined) |
| 23 | + }, |
| 24 | + readFileAsText: async () => "", |
| 25 | + }, |
| 26 | + [Symbol.asyncDispose]: async () => undefined, |
| 27 | + }), |
| 28 | +} |
| 29 | + |
| 30 | +const runtime = await createRuntime({ |
| 31 | + backend: "wordpress-playground", |
| 32 | + artifactsDirectory: root, |
| 33 | + environment: { kind: "wordpress", name: "phpunit-runtime-rejection", version: "7.0", phpVersion: "8.3", blueprint: { steps: [] } }, |
| 34 | + policy: { network: "deny", filesystem: "sandbox", commands: ["wordpress.phpunit"], secrets: "none", approvals: "never" }, |
| 35 | +}, createPlaygroundRuntimeBackend({ cliModule })) |
| 36 | + |
| 37 | +const workflowStep = { |
| 38 | + phase: "steps" as const, |
| 39 | + index: 0, |
| 40 | + step: { command: "wordpress.phpunit", args: ["plugin-slug=demo"] }, |
| 41 | +} |
| 42 | +const startedAt = Date.now() |
| 43 | + |
| 44 | +try { |
| 45 | + let rejectOperation: (reason: Error) => void = () => undefined |
| 46 | + const operation = new Promise<never>((_resolve, reject) => { |
| 47 | + rejectOperation = reject |
| 48 | + }) |
| 49 | + const racedFailure = terminalizeOnPhpWasmRuntimeRejection( |
| 50 | + () => operation, |
| 51 | + () => rejectOperation(new Error("generic abort won the race")), |
| 52 | + ).then( |
| 53 | + () => assert.fail("PHPUnit runtime rejection unexpectedly completed"), |
| 54 | + (reason: unknown) => reason, |
| 55 | + ) |
| 56 | + process.emit("unhandledRejection", phpWasmFailure, Promise.resolve()) |
| 57 | + const rejection = await racedFailure |
| 58 | + assert.equal((rejection as Error & { code?: string }).code, "wp-codebox-php-wasm-runtime-rejection") |
| 59 | + |
| 60 | + const error = await Promise.race([ |
| 61 | + executeRecipeWorkflowStep(runtime, workflowStep, root, undefined, root).then( |
| 62 | + () => assert.fail("PHPUnit step unexpectedly completed"), |
| 63 | + (reason: unknown) => reason, |
| 64 | + ), |
| 65 | + new Promise<never>((_resolve, reject) => setTimeout(() => reject(new Error("PHPUnit runtime rejection did not terminalize within 500ms")), 500)), |
| 66 | + ]) |
| 67 | + const failure = recipeStepFailure(workflowStep, error, startedAt) |
| 68 | + const serialized = JSON.stringify(failure) |
| 69 | + |
| 70 | + assert.equal(failure.schema, "wp-codebox/recipe-step-failure/v1") |
| 71 | + assert.equal(failure.classification, "error") |
| 72 | + assert.ok(failure.durationMs < 500, `expected immediate terminal failure, received ${failure.durationMs}ms`) |
| 73 | + assert.match(failure.error.message, /Recipe workflow steps\[0\] failed/) |
| 74 | + assert.match(serialized, /wp-codebox-php-wasm-runtime-rejection/) |
| 75 | + assert.match(serialized, /infrastructure-failure/) |
| 76 | + assert.match(serialized, /php-wasm/) |
| 77 | + assert.match(serialized, /null function or function signature mismatch/) |
| 78 | + assert.match(serialized, /\[redacted\]/) |
| 79 | + assert.doesNotMatch(serialized, new RegExp(secret)) |
| 80 | + assert.doesNotMatch(serialized, /recipe-run-timeout/) |
| 81 | +} finally { |
| 82 | + await runtime.destroy() |
| 83 | + await rm(root, { recursive: true, force: true }) |
| 84 | +} |
| 85 | + |
| 86 | +console.log("phpunit runtime rejection terminalization ok") |
0 commit comments