Skip to content

Commit 1969510

Browse files
committed
Probe complete canonical WordPress state
1 parent a23366b commit 1969510

4 files changed

Lines changed: 85 additions & 17 deletions

File tree

packages/runtime-cloudflare/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ The WordPress server corpus is a separate deployment artifact, never a Worker-mo
2020

2121
## MDI Init Diagnostics
2222

23+
Every `mdi-*` lifecycle diagnostic materializes the validated packaged canonical MDI seed and the separately managed R2 WordPress runtime artifact in a disposable PHP-WASM runtime. They never acquire a coordinator lease, read or write a canonical R2 revision, or promote state. The old three-option fixture was intentionally removed because it does not represent complete MDI WordPress state.
24+
25+
`?phase=canonical-wordpress` performs a full normal WordPress boot with that exact seed and artifact outside the coordinator, then returns WordPress version, memory, and non-sensitive table counts. Its widget-state count includes the `widget_*` options and `sidebars_widgets`. `?phase=canonical-bootstrap-setup` additionally runs the normal bootstrap setup with a probe-only internal password and fixed probe origin, flushes only the disposable runtime, and returns changed-path counts. Neither probe returns password, option, user, or content values.
26+
2327
The diagnostic-only `?phase=` probes `mdi-init-callbacks`, `mdi-init-exclude-scheduling`, `mdi-init-exclude-block-registration`, `mdi-init-exclude-theme-patterns-styles`, `mdi-init-exclude-widgets`, `mdi-init-exclude-rest-connectors-sitemaps`, and `mdi-init-exclude-initial-content-types` stop at `init`. The inventory probe returns sorted callback identifiers by priority. Each exclusion probe removes only its fixed callback group, runs `init` once, and returns before subsequent WordPress lifecycle work.
2428

2529
The fixed widget probes split `wp_widgets_init` without changing its normal path: `mdi-widgets-callbacks` returns the sorted nested `widgets_init` inventory before `init`; `mdi-widgets-constructors` runs only the core default widget registrations and reports widget class count; `mdi-widgets-hooks` adds the nested action; `mdi-widgets-factory` runs only `WP_Widget_Factory::_register_widgets`; and `mdi-widgets-remaining-hooks` runs the remaining nested callbacks. The bounded direct-registration phases are `mdi-widgets-direct-basic-classic-first`, `mdi-widgets-direct-basic-classic-second`, `mdi-widgets-direct-media`, `mdi-widgets-direct-custom-html-block`, `mdi-widgets-direct-block`, and `mdi-widgets-direct-custom-html`. They remove every normal nested callback, then call `_register()` only on their fixed allowlisted factory objects and return attempted classes, count, completion, and memory. `mdi-widgets-option-reads` reports only fixed widget option names, grouped by `get_option()` presence and direct `$wpdb` SQLite-row presence; it returns no option values. `mdi-widgets-get-settings` calls only `get_settings()` on each default factory object, with `mdi-widgets-get-settings-first` and `mdi-widgets-get-settings-second` as fixed halves; each reports completed classes and array-shape dimensions only. `mdi-widgets-register-one` preloads the fixed `widget_text` option, calls that object's `get_settings()`, then calls only `_register_one()` for its fixed instance. Every phase uses fixed source identities and returns before later lifecycle work; none accepts callback names, removal rules, widget classes, or option names from request input.
@@ -30,6 +34,6 @@ The fixed widget probes split `wp_widgets_init` without changing its normal path
3034
2. Run `npm run provision:cloudflare-wordpress-runtime-corpus -- --local --persist-to <directory>` to verify and upload the exact content-addressed artifact into isolated local R2 storage. Use `--remote` only for an explicit deployment operation.
3135
3. Run `npm run test:cloudflare-runtime` for routing, canonical-state, artifact validation, source contract, and TypeScript coverage.
3236
4. Run `npm run cloudflare:dry-run` to compile the Worker without creating Cloudflare resources.
33-
5. Run `npm run cloudflare:local-gate` for isolated local workerd evidence. It generates and provisions the artifact before workerd starts, then injects stable test-only admin-password and auth-secret values, verifies the login form, cookie-authenticated admin redirect, authenticated REST post publication, public rendering, representative frontend/admin/editor assets, PHP diagnostics, an existing authenticated cookie after cold restart, and a fresh login after restart.
37+
5. Run `npm run cloudflare:local-gate` for isolated local workerd evidence. It generates and provisions the artifact before workerd starts, then verifies the complete-seed canonical boot and ephemeral bootstrap probes, injects stable test-only admin-password and auth-secret values, verifies the login form, cookie-authenticated admin redirect, authenticated REST post publication, public rendering, representative frontend/admin/editor assets, PHP diagnostics, an existing authenticated cookie after cold restart, and a fresh login after restart.
3438

3539
This document describes local candidate verification only. It does not claim remote deployment.

packages/runtime-cloudflare/src/worker.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ async function runBootProbe(phase: string, bucket: R2Bucket): Promise<Response>
517517
try {
518518
const wordpress = await materializeWordPressServerFiles(php, bucket)
519519
await materializeMarkdownDatabaseIntegration(php)
520-
materializeRuntimeFiles(php, MARKDOWN_ROOT, initialMarkdownFiles())
520+
materializeRuntimeFiles(php, MARKDOWN_ROOT, await packagedCanonicalMarkdownSeed())
521521
const evidence = (await php.run({
522522
code: "<?php echo json_encode(['dropin' => file_exists('/wordpress/wp-content/db.php'), 'storage' => file_exists('/wordpress/wp-content/plugins/markdown-database-integration/inc/class-wp-markdown-storage.php'), 'siteurl' => file_exists('/wordpress/wp-content/markdown/_options/siteurl.json')]);",
523523
})).text.trim()
@@ -528,7 +528,7 @@ async function runBootProbe(phase: string, bucket: R2Bucket): Promise<Response>
528528
}
529529

530530
if (phase === "mdi-shortinit") {
531-
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles(), new Uint8Array(markdownPrimaryBootstrapIndex), SITE_URL, {}, bucket)
531+
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, await packagedCanonicalMarkdownSeed(), new Uint8Array(markdownPrimaryBootstrapIndex), SITE_URL, {}, bucket)
532532
try {
533533
const evidence = (await runtime.php.run({
534534
code: "<?php define('SHORTINIT', true); require '/wordpress/wp-load.php'; echo json_encode(['wordpressVersion' => $wp_version, 'markdownDropin' => defined('MARKDOWN_DB_DROPIN'), 'markdownMode' => defined('MARKDOWN_DB_MODE') ? MARKDOWN_DB_MODE : '']);",
@@ -540,7 +540,7 @@ async function runBootProbe(phase: string, bucket: R2Bucket): Promise<Response>
540540
}
541541

542542
if (phase === "mdi-wordpress" || phase === "mdi-option" || phase === "mdi-insert") {
543-
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles(), new Uint8Array(markdownPrimaryBootstrapIndex), SITE_URL, {}, bucket)
543+
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, await packagedCanonicalMarkdownSeed(), new Uint8Array(markdownPrimaryBootstrapIndex), SITE_URL, {}, bucket)
544544
try {
545545
const operation = phase === "mdi-option"
546546
? "$updated = update_option('wp_codebox_mdi_probe', 1); $result = ['updated' => $updated];"
@@ -557,7 +557,7 @@ async function runBootProbe(phase: string, bucket: R2Bucket): Promise<Response>
557557
}
558558

559559
if (["mdi-includes", "mdi-embed", "mdi-textdomain", "mdi-ai-client", "mdi-plugin-constants", "mdi-muplugins", "mdi-plugins", "mdi-globals", "mdi-theme", "mdi-site-health-class", "mdi-site-health", "mdi-current-user", "mdi-init", "mdi-wp-loaded", "mdi-init-callbacks", "mdi-init-exclude-scheduling", "mdi-init-exclude-block-registration", "mdi-init-exclude-theme-patterns-styles", "mdi-init-exclude-widgets", "mdi-init-exclude-rest-connectors-sitemaps", "mdi-init-exclude-initial-content-types", "mdi-widgets-callbacks", "mdi-widgets-constructors", "mdi-widgets-hooks", "mdi-widgets-factory", "mdi-widgets-remaining-hooks", "mdi-widgets-direct-basic-classic-first", "mdi-widgets-direct-basic-classic-second", "mdi-widgets-direct-media", "mdi-widgets-direct-custom-html-block", "mdi-widgets-direct-block", "mdi-widgets-direct-custom-html", "mdi-widgets-option-reads", "mdi-widgets-get-settings", "mdi-widgets-get-settings-first", "mdi-widgets-get-settings-second", "mdi-widgets-register-one"].includes(phase)) {
560-
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, initialMarkdownFiles(), new Uint8Array(markdownPrimaryBootstrapIndex), SITE_URL, {}, bucket)
560+
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, await packagedCanonicalMarkdownSeed(), new Uint8Array(markdownPrimaryBootstrapIndex), SITE_URL, {}, bucket)
561561
try {
562562
const evidence = (await runtime.php.run({ code: wordpressProbeCode(phase) })).text.trim()
563563
return probeResponse(phase, JSON.parse(evidence) as Record<string, string>)
@@ -566,6 +566,37 @@ async function runBootProbe(phase: string, bucket: R2Bucket): Promise<Response>
566566
}
567567
}
568568

569+
if (phase === "canonical-wordpress" || phase === "canonical-bootstrap-setup") {
570+
const canonicalSeed = await packagedCanonicalMarkdownSeed()
571+
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, undefined, canonicalSeed, new Uint8Array(markdownPrimaryBootstrapIndex), "https://canonical-probe.invalid", {}, bucket)
572+
try {
573+
if (phase === "canonical-bootstrap-setup") {
574+
const passwordFile = "/tmp/wp-codebox-canonical-bootstrap-probe-password"
575+
runtime.php.writeFile(passwordFile, new TextEncoder().encode("probe-only-password"))
576+
const output = (await runtime.php.run({ code: canonicalBootstrapSetupCode(passwordFile, "https://canonical-probe.invalid") })).text.trim()
577+
if (output !== "flushed") throw new Error("Canonical bootstrap setup probe did not confirm its ephemeral flush.")
578+
const changes = canonicalChangedPathCounts(canonicalSeed, collectRuntimeFiles(runtime.php, MARKDOWN_ROOT))
579+
return probeResponse(phase, { wordpressVersion: runtime.wordpressVersion, canonicalSeedFiles: canonicalSeed.length, ...changes })
580+
}
581+
const evidence = (await runtime.php.run({ code: `<?php
582+
require '/wordpress/wp-load.php';
583+
echo json_encode([
584+
'wordpressVersion' => get_bloginfo('version'),
585+
'postCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->posts}"),
586+
'pageCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->posts} WHERE post_type = 'page'"),
587+
'userCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->users}"),
588+
'optionCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->options}"),
589+
'widgetOptionCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->options} WHERE option_name LIKE 'widget\\_%'"),
590+
'widgetStateOptionCount' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {\$wpdb->options} WHERE option_name LIKE 'widget\\_%' OR option_name = 'sidebars_widgets'"),
591+
'memoryBytes' => memory_get_usage(true),
592+
'peakMemoryBytes' => memory_get_peak_usage(true),
593+
]);` })).text.trim()
594+
return probeResponse(phase, { canonicalSeedFiles: canonicalSeed.length, ...JSON.parse(evidence) as Record<string, number | string> })
595+
} finally {
596+
runtime.php.exit()
597+
}
598+
}
599+
569600
if (phase?.startsWith("seeded-")) {
570601
const runtime = await bootWordPressRuntime(
571602
"do-not-attempt-installing",
@@ -1006,18 +1037,6 @@ async function bootWordPressRuntime(
10061037
return { php, requestHandler, wordpressVersion }
10071038
}
10081039

1009-
function initialMarkdownFiles(): RuntimeFile[] {
1010-
const options = [
1011-
{ option_id: 1, option_name: "siteurl", option_value: SITE_URL, autoload: "on" },
1012-
{ option_id: 2, option_name: "home", option_value: SITE_URL, autoload: "on" },
1013-
{ option_id: 3, option_name: "blogname", option_value: "WP Codebox Cloudflare Runtime", autoload: "on" },
1014-
]
1015-
return options.map((option) => ({
1016-
path: `_options/${option.option_name}.json`,
1017-
bytes: new TextEncoder().encode(JSON.stringify(option, null, 2)),
1018-
}))
1019-
}
1020-
10211040
function materializeRuntimeFiles(php: PHP, root: string, files: RuntimeFile[]): void {
10221041
for (const file of files) {
10231042
const destination = `${root}/${file.path}`
@@ -1043,6 +1062,21 @@ function collectRuntimeFiles(php: PHP, root: string): RuntimeFile[] {
10431062
return files.sort((left, right) => left.path.localeCompare(right.path))
10441063
}
10451064

1065+
function canonicalChangedPathCounts(before: RuntimeFile[], after: RuntimeFile[]): { createdPathCount: number; changedPathCount: number; deletedPathCount: number } {
1066+
const beforeByPath = new Map(before.map((file) => [file.path, file.bytes]))
1067+
const afterByPath = new Map(after.map((file) => [file.path, file.bytes]))
1068+
let createdPathCount = 0
1069+
let changedPathCount = 0
1070+
let deletedPathCount = 0
1071+
for (const [path, bytes] of afterByPath) {
1072+
const previous = beforeByPath.get(path)
1073+
if (!previous) createdPathCount++
1074+
else if (previous.byteLength !== bytes.byteLength || previous.some((byte, index) => byte !== bytes[index])) changedPathCount++
1075+
}
1076+
for (const path of beforeByPath.keys()) if (!afterByPath.has(path)) deletedPathCount++
1077+
return { createdPathCount, changedPathCount, deletedPathCount }
1078+
}
1079+
10461080
async function sha256Hex(bytes: Uint8Array): Promise<string> {
10471081
const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer)
10481082
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")

scripts/cloudflare-local-gate.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ try {
1616
await run("npm", ["run", "generate:cloudflare-wordpress-runtime-corpus"])
1717
await run("npm", ["run", "provision:cloudflare-wordpress-runtime-corpus", "--", "--local", "--persist-to", stateDirectory])
1818
await startWorker()
19+
await assertCanonicalProbes()
1920
await assertConcurrentMutations()
2021
const adminHtml = await login()
2122
const editorHtml = await assertPostNewEditor()
@@ -166,6 +167,20 @@ async function assertHealthResponse() {
166167
if (response.status !== 200 || body.schema !== "wp-codebox/cloudflare-runtime-health/v1" || body.marker !== "wp-codebox-cloudflare-runtime-health" || body.phpVersion !== "8.5.8" || typeof body.wordpressVersion !== "string" || body.execution?.status !== "ok") throw new Error(`Unexpected Cloudflare runtime health envelope: ${JSON.stringify(body)}`)
167168
}
168169

170+
async function assertCanonicalProbes() {
171+
const wordpress = await (await request(`${origin}/?phase=canonical-wordpress`)).json()
172+
const setup = await (await request(`${origin}/?phase=canonical-bootstrap-setup`)).json()
173+
const counts = ["canonicalSeedFiles", "postCount", "pageCount", "userCount", "optionCount", "widgetOptionCount", "widgetStateOptionCount", "memoryBytes", "peakMemoryBytes"]
174+
if (wordpress.completed !== true || typeof wordpress.evidence?.wordpressVersion !== "string" || counts.some((key) => !Number.isInteger(wordpress.evidence?.[key])) || wordpress.evidence.widgetStateOptionCount !== 19) {
175+
throw new Error(`Canonical WordPress probe did not boot the complete seed: ${JSON.stringify(wordpress)}`)
176+
}
177+
const changedCounts = ["createdPathCount", "changedPathCount", "deletedPathCount"]
178+
if (setup.completed !== true || typeof setup.evidence?.wordpressVersion !== "string" || changedCounts.some((key) => !Number.isInteger(setup.evidence?.[key])) || setup.evidence.changedPathCount < 1) {
179+
throw new Error(`Canonical bootstrap setup probe did not produce ephemeral changes: ${JSON.stringify(setup)}`)
180+
}
181+
console.log(`Canonical probes: WordPress ${wordpress.evidence.wordpressVersion}, ${wordpress.evidence.widgetOptionCount} widget_* options plus sidebars_widgets, ${setup.evidence.changedPathCount} changed ephemeral paths.`)
182+
}
183+
169184
async function assertWordPressPage(target, label) {
170185
const response = await request(target)
171186
const body = await response.text()

tests/cloudflare-runtime.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ test("Cloudflare MDI init diagnostics use fixed callback inventories and exclusi
225225
assert.doesNotMatch(worker, /searchParams\.get\([^)]*callback|searchParams\.get\([^)]*exclude|searchParams\.get\([^)]*widget/)
226226
})
227227

228+
test("Cloudflare MDI lifecycle diagnostics use the complete packaged seed without canonical persistence", async () => {
229+
const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
230+
const probes = worker.slice(worker.indexOf("async function runBootProbe"), worker.indexOf("if (phase?.startsWith(\"seeded-\"))"))
231+
232+
assert.equal((probes.match(/initialMarkdownFiles/g) ?? []).length, 0)
233+
assert.ok((probes.match(/await packagedCanonicalMarkdownSeed\(\)/g) ?? []).length >= 4)
234+
assert.match(probes, /phase === "canonical-wordpress" \|\| phase === "canonical-bootstrap-setup"/)
235+
assert.match(probes, /materializeWordPressServerFiles/)
236+
assert.match(probes, /canonicalBootstrapSetupCode\(passwordFile, "https:\/\/canonical-probe\.invalid"\)/)
237+
assert.match(probes, /canonicalChangedPathCounts\(canonicalSeed, collectRuntimeFiles/)
238+
assert.doesNotMatch(probes, /persistMarkdownRevision|commitLease|coordinatorCall/)
239+
assert.match(worker, /'widgetOptionCount' => \(int\) \$wpdb->get_var/)
240+
assert.match(worker, /function canonicalChangedPathCounts/)
241+
})
242+
228243
test("Cloudflare keeps PHP-WASM in the entry Worker and uses the Durable Object only for leases", async () => {
229244
const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
230245
const coordinator = await readFile(new URL("../packages/runtime-cloudflare/src/state-coordinator.ts", import.meta.url), "utf8")

0 commit comments

Comments
 (0)