Skip to content

Commit ed2db33

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/1916-malformed-bootstrap-diagnostics
2 parents f6321d1 + a9edf31 commit ed2db33

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"test:runtime-sources-playground-integration": "tsx tests/runtime-sources-playground-integration.test.ts",
107107
"test:playground-readonly-mounts": "tsx tests/playground-readonly-mounts.test.ts",
108108
"test:playground-readonly-mounts-integration": "tsx tests/playground-readonly-mounts-integration.test.ts",
109+
"test:playground-staged-upload-preview": "tsx tests/playground-staged-upload-preview.integration.test.ts",
109110
"test:playground-phpunit-readonly-cache-integration": "tsx tests/playground-phpunit-readonly-cache.integration.test.ts",
110111
"test:playground-phpunit-bootstrap-failure-integration": "tsx tests/playground-phpunit-bootstrap-failure.integration.test.ts",
111112
"test:playground-custom-archive-cache": "tsx tests/playground-custom-archive-cache.test.ts && tsx tests/playground-custom-archive-cache-process.test.ts && tsx tests/playground-custom-archive-cache.integration.test.ts",

packages/runtime-playground/src/programmatic-playground-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export async function startProgrammaticPlaygroundServer(spec: RuntimeCreateSpec,
5050
}
5151
const requestHandler = await bootWordPressAndRequestHandler({
5252
createPhpRuntime: () => loadNodeRuntime(phpVersion, programmaticNodeRuntimeOptions(spec, nextProcessId++)),
53+
maxPhpInstances: 1,
5354
phpVersion,
5455
siteUrl: spec.preview?.siteUrl ?? "http://127.0.0.1",
5556
documentRoot: "/wordpress",
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import assert from "node:assert/strict"
2+
import { execFile } from "node:child_process"
3+
import { mkdtemp, rm, writeFile } from "node:fs/promises"
4+
import { tmpdir } from "node:os"
5+
import { join } from "node:path"
6+
import { promisify } from "node:util"
7+
8+
import { PNG } from "pngjs"
9+
10+
const execFileAsync = promisify(execFile)
11+
const root = await mkdtemp(join(tmpdir(), "wp-codebox-staged-upload-preview-"))
12+
const imageSource = join(root, "staged.png")
13+
const htmlSource = join(root, "index.html")
14+
const recipePath = join(root, "recipe.json")
15+
const artifactsPath = join(root, "artifacts")
16+
const uploadPath = "/wp-content/uploads/wp-codebox-staged-image/index.html"
17+
18+
try {
19+
const png = new PNG({ width: 2, height: 2 })
20+
for (let offset = 0; offset < png.data.length; offset += 4) {
21+
png.data.set([17, 34, 51, 255], offset)
22+
}
23+
await writeFile(imageSource, PNG.sync.write(png))
24+
await writeFile(htmlSource, '<!doctype html><meta charset="utf-8"><img id="staged-image" src="staged.png" width="2" height="2" alt="staged image">\n')
25+
await writeFile(recipePath, `${JSON.stringify({
26+
schema: "wp-codebox/workspace-recipe/v1",
27+
runtime: { backend: "wordpress-playground", wp: "6.5", blueprint: { steps: [] } },
28+
inputs: {
29+
stagedFiles: [
30+
{ source: imageSource, target: "/wordpress/wp-content/uploads/wp-codebox-staged-image/staged.png" },
31+
{ source: htmlSource, target: "/wordpress/wp-content/uploads/wp-codebox-staged-image/index.html" },
32+
],
33+
},
34+
workflow: {
35+
steps: [{
36+
command: "wordpress.browser-probe",
37+
args: [
38+
`url=${uploadPath}`,
39+
"wait-for=load",
40+
"capture=html,screenshot",
41+
`script=${browserPaintScript()}`,
42+
],
43+
}],
44+
},
45+
})}\n`)
46+
47+
const output = await runRecipe()
48+
if (output) {
49+
assert.equal(output.success, true, JSON.stringify(output))
50+
const execution = output.executions?.find((entry) => entry.command === "wordpress.browser-probe")
51+
assert.ok(execution?.stdout, "browser probe output should be retained")
52+
const probe = JSON.parse(execution.stdout) as BrowserProbeOutput
53+
assert.match(probe.finalUrl ?? "", /\/wp-content\/uploads\/wp-codebox-staged-image\/index\.html$/)
54+
assert.deepEqual(probe.summary?.scriptResult, {
55+
complete: true,
56+
naturalWidth: 2,
57+
naturalHeight: 2,
58+
width: 2,
59+
height: 2,
60+
pixel: [17, 34, 51, 255],
61+
})
62+
assert.ok(probe.files?.html, "browser probe should capture rendered HTML")
63+
assert.ok(probe.files?.screenshot, "browser probe should capture painted pixels")
64+
}
65+
} finally {
66+
await rm(root, { recursive: true, force: true })
67+
}
68+
69+
function browserPaintScript(): string {
70+
return `
71+
const image = document.querySelector("#staged-image");
72+
if (!(image instanceof HTMLImageElement)) throw new Error("staged image element missing");
73+
await image.decode();
74+
const rect = image.getBoundingClientRect();
75+
const canvas = document.createElement("canvas");
76+
canvas.width = 1;
77+
canvas.height = 1;
78+
const context = canvas.getContext("2d");
79+
if (!context) throw new Error("2d canvas unavailable");
80+
context.drawImage(image, 0, 0);
81+
return {
82+
complete: image.complete,
83+
naturalWidth: image.naturalWidth,
84+
naturalHeight: image.naturalHeight,
85+
width: rect.width,
86+
height: rect.height,
87+
pixel: [...context.getImageData(0, 0, 1, 1).data],
88+
};`
89+
}
90+
91+
async function runRecipe(): Promise<RecipeRunOutput | undefined> {
92+
try {
93+
const result = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", "recipe-run", "--recipe", recipePath, "--artifacts", artifactsPath, "--json"], {
94+
cwd: process.cwd(),
95+
timeout: 300_000,
96+
maxBuffer: 2 * 1024 * 1024,
97+
})
98+
return JSON.parse(result.stdout) as RecipeRunOutput
99+
} catch (error) {
100+
if (isUnavailableWordPressRuntimeSource(error)) {
101+
console.log("playground staged upload preview integration skipped: WordPress runtime source was unavailable before runtime startup")
102+
return undefined
103+
}
104+
throw error
105+
}
106+
}
107+
108+
function isUnavailableWordPressRuntimeSource(error: unknown): boolean {
109+
const output = recipeRunOutput(error && typeof error === "object" && "stdout" in error ? error.stdout : undefined)
110+
const startup = output?.phaseEvidence?.find((phase) => phase.name === "runtime_startup")
111+
const message = startup?.error?.message ?? ""
112+
return output?.schema === "wp-codebox/recipe-run/v1"
113+
&& startup?.status === "failed"
114+
&& /Unable to resolve Playground startup asset (wordpress-archive-cache|wordpress-release-metadata)/.test(message)
115+
&& /fetch failed|Could not resolve host|Connection timed out|network is unreachable/i.test(message)
116+
}
117+
118+
function recipeRunOutput(value: unknown): RecipeRunOutput | undefined {
119+
if (typeof value !== "string") return undefined
120+
try {
121+
return JSON.parse(value) as RecipeRunOutput
122+
} catch {
123+
return undefined
124+
}
125+
}
126+
127+
interface RecipeRunOutput {
128+
schema?: string
129+
success?: boolean
130+
executions?: Array<{ command?: string; stdout?: string }>
131+
phaseEvidence?: Array<{ name?: string; status?: string; error?: { message?: string } }>
132+
}
133+
134+
interface BrowserProbeOutput {
135+
finalUrl?: string
136+
files?: { html?: string; screenshot?: string }
137+
summary?: { scriptResult?: unknown }
138+
}

0 commit comments

Comments
 (0)