diff --git a/packages/runtime-playground/src/browser-actions-runner.ts b/packages/runtime-playground/src/browser-actions-runner.ts index ec394adf..a52c5f78 100644 --- a/packages/runtime-playground/src/browser-actions-runner.ts +++ b/packages/runtime-playground/src/browser-actions-runner.ts @@ -1,4 +1,4 @@ -import { copyFile, readdir, readFile, writeFile } from "node:fs/promises" +import { copyFile, readFile, stat, writeFile } from "node:fs/promises" import { join } from "node:path" import { BROWSER_ACTION_CORPUS_SCHEMA, BROWSER_ADAPTIVE_EXPLORATION_SCHEMA, BROWSER_MULTI_ACTOR_SCENARIO_SCHEMA, BROWSER_PROBE_PROFILES, BROWSER_TOOL_VERIFIER_RESULT_SCHEMA, HostToolRegistry, assertRuntimeCommandAllowed, browserActionCorpusArtifact, browserActionCorpusContract, browserAdaptiveExplorationContract, browserEnvironment, browserEnvironmentDigest, browserGeolocation, browserInteractionScriptUsesEvaluate, browserToolVerifierInputSummary, createHostToolRegistry, executeHostTool, resolveCommandPath, transportFaultModel, validateBrowserInteractionScript, type BrowserActionCorpusArtifact, type BrowserActionCorpusContract, type BrowserAdaptiveExplorationArtifact, type BrowserAdaptiveExplorationContract, type BrowserEnvironment, type BrowserGeolocationPermissionState, type BrowserInteractionStep, type BrowserMultiActorScenario, type BrowserToolVerifierResult, type ExecutionSpec, type HostToolDefinition, type JsonValue, type RuntimeCreateSpec, type TransportFaultModel } from "@automattic/wp-codebox-core" import { now, sha256 } from "@automattic/wp-codebox-core/internals" @@ -152,6 +152,7 @@ export async function runBrowserActionsCommand({ const videoStagingDirectory = artifactSession.absolutePath("video-source") let videoRecording: import("playwright").Video | null = null let videoSaved = false + let videoCaptureFailed = false let videoStartedAt: number | undefined let videoStartedWallAt: number | undefined let videoRecordingOrigin: number | undefined @@ -567,13 +568,17 @@ export async function runBrowserActionsCommand({ // video.saveAs(), which requires a browser connection that cleanup has closed. try { videoFinishedAt = performance.now() - const staged = (await readdir(videoStagingDirectory)).filter((entry) => entry.endsWith(".webm")).sort() - if (staged.length === 0) throw new Error("wordpress.browser-actions capture=video produced no recording") - const source = join(videoStagingDirectory, staged[0]) + const source = await videoRecording.path() + const recording = await stat(source) + if (!recording.isFile() || recording.size === 0) throw new Error("wordpress.browser-actions capture=video produced an empty recording") await artifactSession.writeGenerated("video", "video.webm", (path) => copyFile(source, path)) + const adopted = await stat(artifactSession.absolutePath("video.webm")) + if (!adopted.isFile() || adopted.size === 0) throw new Error("wordpress.browser-actions capture=video adopted an empty recording") videoSaved = true } catch (error) { errors.push(serializeBrowserError("probe-error", error)) + videoCaptureFailed = true + pendingError ??= error instanceof Error ? error : new Error(String(error)) } } if (capture.has("steps")) { @@ -582,7 +587,7 @@ export async function runBrowserActionsCommand({ if (capture.has("console")) { await artifactSession.writeJsonLines("console", "console.jsonl", consoleMessages) } - if (capture.has("errors")) { + if (capture.has("errors") || videoCaptureFailed) { await artifactSession.writeJsonLines("errors", "errors.jsonl", errors) } if (capture.has("network")) { @@ -642,7 +647,7 @@ export async function runBrowserActionsCommand({ files: { ...(capture.has("steps") ? { steps: "files/browser/steps.jsonl" } : {}), ...(capture.has("console") ? { console: "files/browser/console.jsonl" } : {}), - ...(capture.has("errors") ? { errors: "files/browser/errors.jsonl" } : {}), + ...(capture.has("errors") || videoCaptureFailed ? { errors: "files/browser/errors.jsonl" } : {}), ...(htmlSha256 ? { html: "files/browser/snapshot.html" } : {}), ...(capture.has("network") ? { network: "files/browser/network.jsonl" } : {}), ...(capture.has("network") ? { requestCoverage: "files/browser/request-coverage.json" } : {}), diff --git a/tests/browser-actions-video-capture.browser.test.ts b/tests/browser-actions-video-capture.browser.test.ts index 84efcef2..ed8adf49 100644 --- a/tests/browser-actions-video-capture.browser.test.ts +++ b/tests/browser-actions-video-capture.browser.test.ts @@ -38,11 +38,13 @@ test("browser actions capture=video records the session and adopts it as a named }) assert.equal(result.artifact.summary.video, true, "the summary must report the recording") + assert.equal(result.artifact.summary.errors, 0, "a finalized recording should not add capture errors") const recording = join(artifactRoot, "files/browser/video.webm") const recorded = await stat(recording) assert(recorded.isFile(), "the recording must be adopted as video.webm") assert(recorded.size > 0, "the recording must not be empty") + assert(recorded.size > 1_000, "the recording must contain finalized video data") const summary = JSON.parse(await readFile(join(artifactRoot, "files/browser/action-summary.json"), "utf8")) assert.deepEqual({ width: summary.video.width, height: summary.video.height }, { width: 860, height: 1864 }) assert.deepEqual(summary.video.markers.map((marker: { index: number; name: string }) => [marker.index, marker.name]), [[1, "button pressed"], [2, "after-action"]])