From 50d2d2203b93f1bce0aa4b8d5513c3fcbe28d225 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 22 Jul 2026 00:22:20 +0000 Subject: [PATCH] fix: verify staged mount file writes --- .../src/mount-materialization.ts | 66 +++++++++++++++++++ .../phpunit-playground-harness/composer.json | 2 + tests/mount-materialization.test.ts | 53 ++++++++++++++- ...phpunit-readonly-cache.integration.test.ts | 5 ++ tests/staged-input-materialization.test.ts | 9 ++- 5 files changed, 132 insertions(+), 3 deletions(-) diff --git a/packages/runtime-playground/src/mount-materialization.ts b/packages/runtime-playground/src/mount-materialization.ts index be3d496df..5c87c752d 100644 --- a/packages/runtime-playground/src/mount-materialization.ts +++ b/packages/runtime-playground/src/mount-materialization.ts @@ -60,6 +60,12 @@ interface HostMountFileMaterializationResponse { skipped?: number } +interface HostMountFileVerificationResponse { + schema?: string + repaired?: number + skipped?: number +} + const HOST_MOUNT_FILE_BATCH_SIZE = 100 const HOST_MOUNT_DIRECTORY_BATCH_SIZE = 500 @@ -200,6 +206,7 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou let skipped = 0 const directoryBatch: string[] = [] const fileBatch: HostMountFilePayload[] = [] + const verificationBatch: HostMountFilePayload[] = [] const flushDirectories = async () => { if (directoryBatch.length === 0) { @@ -224,6 +231,13 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou created += result.created skipped += result.skipped } + const flushVerificationBatch = async () => { + if (verificationBatch.length === 0) { + return + } + const result = await verifyHostMountFilesWithPhp(server, verificationBatch.splice(0, verificationBatch.length)) + skipped += result.skipped + } const writeFilePayload = async (payload: HostMountFilePayload) => { if (!server.playground.writeFile) { fileBatch.push(payload) @@ -249,6 +263,10 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou try { await server.playground.writeFile(target, text) materialized++ + verificationBatch.push(payload) + if (verificationBatch.length >= HOST_MOUNT_FILE_BATCH_SIZE) { + await flushVerificationBatch() + } } catch { const fallback = await materializeHostMountFilesWithPhp(server, [payload], []) materialized += fallback.materialized @@ -271,6 +289,7 @@ async function materializeHostMountToVfs(server: PlaygroundCliServer, mount: Mou } await flushDirectories() await flushFileBatch() + await flushVerificationBatch() return { materialized, created, skipped } } @@ -366,6 +385,19 @@ async function materializeHostMountFilesWithPhp(server: PlaygroundCliServer, fil } } +async function verifyHostMountFilesWithPhp(server: PlaygroundCliServer, files: HostMountFilePayload[]): Promise<{ repaired: number; skipped: number }> { + const response = await server.playground.run({ code: hostMountVerifyPhp(files) }) + assertPlaygroundResponseOk("playground-staged-input-verify", response) + const parsed = parseMaterializationJson(response.text, "wp-codebox/host-mount-verification/v1", "playground-staged-input-verify") + if ((parsed.skipped ?? 0) > 0) { + throw new Error(`playground-staged-input-verify could not preserve ${parsed.skipped} staged input file(s)`) + } + return { + repaired: parsed.repaired ?? 0, + skipped: parsed.skipped ?? 0, + } +} + function parseMaterializationJson(text: string, schema: string, command: string): T { let parsed: unknown try { @@ -493,6 +525,7 @@ foreach (($payload['directories'] ?? array()) as $directory) { } $skipped++; } + foreach (($payload['files'] ?? array()) as $file) { $target = (string) ($file['target'] ?? ''); $contents = (string) ($file['contentsBase64'] ?? ''); @@ -516,6 +549,39 @@ echo json_encode(array('schema' => 'wp-codebox/host-mount-materialization/v1', ' ` } +function hostMountVerifyPhp(files: HostMountFilePayload[]): string { + const payload = JSON.stringify(JSON.stringify({ files })) + return ` 'wp-codebox/host-mount-verification/v1', 'repaired' => $repaired, 'skipped' => $skipped), JSON_UNESCAPED_SLASHES); +` +} + function hostMountMkdirPhp(directories: string[]): string { const payload = JSON.stringify(JSON.stringify({ directories })) return ` = {} + +try { + await mkdir(join(silentlyDroppedSource, "composer"), { recursive: true }) + await writeFile(join(silentlyDroppedSource, "composer", "autoload_real.php"), " { const written = new Map() const server = { playground: { - async run() { - return { text: JSON.stringify({ schema: "wp-codebox/host-mount-directory-materialization/v1", created: 2, skipped: 0 }) } + async run({ code }: { code: string }) { + return code.includes("wp-codebox/host-mount-verification/v1") + ? { text: JSON.stringify({ schema: "wp-codebox/host-mount-verification/v1", repaired: 0, skipped: 0 }) } + : { text: JSON.stringify({ schema: "wp-codebox/host-mount-directory-materialization/v1", created: 2, skipped: 0 }) } }, async writeFile(path: string, contents: string) { written.set(path, contents) @@ -50,6 +52,9 @@ await withTempDir("wp-codebox-staged-input-materialization-fallback-", async (ro const server = { playground: { async run({ code }: { code: string }) { + if (code.includes("wp-codebox/host-mount-verification/v1")) { + return { text: JSON.stringify({ schema: "wp-codebox/host-mount-verification/v1", repaired: 0, skipped: 0 }) } + } if (code.includes("wp-codebox/host-mount-materialization/v1")) { fallbackWrites++ return { text: JSON.stringify({ schema: "wp-codebox/host-mount-materialization/v1", materialized: 1, skipped: 0 }) }