Skip to content

Commit ef8e7f5

Browse files
committed
fix: resolve recipe browser payload paths
1 parent c85c41b commit ef8e7f5

3 files changed

Lines changed: 96 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
"test:browser-artifact-session": "tsx tests/browser-artifact-session.test.ts",
183183
"test:browser-environment-matrix": "tsx --test tests/browser-environment-matrix.test.ts tests/browser-environment-matrix.browser.test.ts",
184184
"test:browser-actions-environment": "tsx --test tests/browser-actions-environment.browser.test.ts",
185+
"test:browser-recipe-file-payloads-integration": "tsx tests/browser-recipe-file-payloads.integration.test.ts",
185186
"test:browser-diagnostic-providers": "tsx tests/browser-diagnostic-providers.test.ts",
186187
"test:browser-capture-html-diagnostics-reliability": "tsx tests/browser-capture-html-diagnostics-reliability.test.ts",
187188
"test:browser-provider-permissions": "tsx tests/browser-provider-permissions.test.ts",

packages/cli/src/agent-sandbox.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function agentRuntimeMounts(options: AgentRuntimeProbeOptions): AgentRunt
117117

118118
export async function recipeExecutionSpec(step: WorkspaceRecipe["workflow"]["steps"][number], recipeDirectory: string, sandboxWorkspace?: SandboxWorkspaceContract, options: { inputMountPathMap?: readonly InputMountPathMapping[] } = {}): Promise<ResolvedRecipeExecutionSpec> {
119119
const originalArgs = step.args ?? []
120-
const resolvedStep = { ...step, args: rewriteRecipeExecutionArgs(step.command, originalArgs, options.inputMountPathMap) }
120+
const resolvedStep = { ...step, args: rewriteRecipeExecutionArgs(step.command, originalArgs, recipeDirectory, options.inputMountPathMap) }
121121
const finish = (spec: ExecutionSpec & { args?: string[] }): ResolvedRecipeExecutionSpec => {
122122
// Commands can generate PHP and serialized payloads after their source args
123123
// are resolved, so canonicalize the generated execution spec as well.
@@ -186,8 +186,8 @@ export async function recipeExecutionSpec(step: WorkspaceRecipe["workflow"]["ste
186186
return finish({ command: resolvedStep.command, args: [...(resolvedStep.args ?? []), ...commandDiagnosticsCaptureArgs(resolvedStep.diagnostics)], diagnostics: resolvedStep.diagnostics })
187187
}
188188

189-
function rewriteRecipeExecutionArgs(command: string, args: readonly string[], inputMountPathMap: readonly InputMountPathMapping[] = []): string[] {
190-
const rewritten = rewriteInputMountPathArgs(args, inputMountPathMap)
189+
function rewriteRecipeExecutionArgs(command: string, args: readonly string[], recipeDirectory: string, inputMountPathMap: readonly InputMountPathMapping[] = []): string[] {
190+
const rewritten = rewriteRecipeBrowserPayloadArgs(command, rewriteInputMountPathArgs(args, inputMountPathMap), recipeDirectory)
191191
if (command === "wordpress.run-workload") {
192192
return rewriteInputMountPathJsonArgs(rewritten, ["workload-json"], inputMountPathMap)
193193
}
@@ -197,6 +197,22 @@ function rewriteRecipeExecutionArgs(command: string, args: readonly string[], in
197197
return rewritten
198198
}
199199

200+
function rewriteRecipeBrowserPayloadArgs(command: string, args: readonly string[], recipeDirectory: string): string[] {
201+
const fileBackedArgs = command === "wordpress.browser-actions"
202+
? new Set(["steps-json", "browser-environment-json"])
203+
: command === "wordpress.browser-scenario"
204+
? new Set(["scenario-json", "steps-json", "browser-environment-json"])
205+
: undefined
206+
if (!fileBackedArgs) return [...args]
207+
return args.map((arg) => {
208+
const separator = arg.indexOf("=")
209+
if (separator < 0 || !fileBackedArgs.has(arg.slice(0, separator))) return arg
210+
const value = arg.slice(separator + 1)
211+
if (!value.startsWith("@")) return arg
212+
return `${arg.slice(0, separator + 1)}@${resolve(recipeDirectory, value.slice(1))}`
213+
})
214+
}
215+
200216
async function wordpressRunWorkloadExecutionSpec(step: WorkspaceRecipe["workflow"]["steps"][number], recipeDirectory: string): Promise<ExecutionSpec & { args: string[] }> {
201217
const args = step.args ?? []
202218
const workloadJson = commandArgValue(args, "workload-json")
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import assert from "node:assert/strict"
2+
import { execFile } from "node:child_process"
3+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
4+
import { tmpdir } from "node:os"
5+
import { dirname, join, resolve } from "node:path"
6+
import { fileURLToPath } from "node:url"
7+
import { promisify } from "node:util"
8+
9+
const execFileAsync = promisify(execFile)
10+
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
11+
const root = await mkdtemp(join(tmpdir(), "wp-codebox-browser-recipe-files-"))
12+
const recipeDirectory = join(root, "recipe")
13+
const invocationDirectory = join(root, "invocation")
14+
const cli = join(repositoryRoot, "packages/cli/dist/index.js")
15+
16+
try {
17+
await Promise.all([mkdir(recipeDirectory), mkdir(invocationDirectory)])
18+
await writeFile(join(recipeDirectory, "action-steps.json"), JSON.stringify([{ kind: "evaluate", expression: "document.documentElement.dataset.actionRecipe = 'ready'", assert: "ready" }]))
19+
await writeFile(join(recipeDirectory, "scenario.json"), JSON.stringify({ url: "about:blank", captures: ["steps"], steps: [{ kind: "evaluate", expression: "document.documentElement.dataset.scenarioRecipe = 'ready'", assert: "ready" }] }))
20+
await writeFile(join(recipeDirectory, "scenario-steps.json"), JSON.stringify([{ kind: "evaluate", expression: "document.documentElement.dataset.scenarioStepsRecipe = 'ready'", assert: "ready" }]))
21+
await writeFile(join(recipeDirectory, "valid-recipe.json"), JSON.stringify({
22+
schema: "wp-codebox/workspace-recipe/v1",
23+
runtime: { backend: "wordpress-playground", wp: "latest", blueprint: { steps: [] } },
24+
workflow: { steps: [
25+
{ command: "wordpress.browser-actions", args: ["url=about:blank", "steps-json=@action-steps.json", "capture=steps"] },
26+
{ command: "wordpress.browser-scenario", args: ["scenario-json=@scenario.json"] },
27+
{ command: "wordpress.browser-scenario", args: ["url=about:blank", "steps-json=@scenario-steps.json", "capture=steps"] },
28+
] },
29+
}))
30+
31+
const validDryRun = await execFileAsync(process.execPath, [cli, "recipe-run", "--recipe", join(recipeDirectory, "valid-recipe.json"), "--dry-run", "--json"], {
32+
cwd: invocationDirectory,
33+
timeout: 120_000,
34+
maxBuffer: 4 * 1024 * 1024,
35+
})
36+
const validDryRunOutput = JSON.parse(validDryRun.stdout)
37+
assert.equal(validDryRunOutput.plan.policy.commands.includes("wordpress.browser-actions.evaluate"), true)
38+
39+
const valid = await execFileAsync(process.execPath, [cli, "recipe-run", "--recipe", join(recipeDirectory, "valid-recipe.json"), "--artifacts", join(root, "valid-artifacts"), "--json"], {
40+
cwd: invocationDirectory,
41+
timeout: 300_000,
42+
maxBuffer: 4 * 1024 * 1024,
43+
})
44+
const validOutput = JSON.parse(valid.stdout)
45+
assert.equal(validOutput.success, true, valid.stdout)
46+
assert.equal(validOutput.executions.length, 3)
47+
48+
await writeFile(join(recipeDirectory, "malformed.json"), "{")
49+
await writeFile(join(recipeDirectory, "malformed-recipe.json"), JSON.stringify({
50+
schema: "wp-codebox/workspace-recipe/v1",
51+
workflow: { steps: [
52+
{ command: "wordpress.browser-actions", args: ["url=/", "steps-json=@malformed.json"] },
53+
{ command: "wordpress.browser-scenario", args: ["scenario-json=@malformed.json"] },
54+
{ command: "wordpress.browser-scenario", args: ["url=/", "steps-json=@malformed.json"] },
55+
] },
56+
}))
57+
58+
let malformedStdout = ""
59+
try {
60+
await execFileAsync(process.execPath, [cli, "recipe-run", "--recipe", join(recipeDirectory, "malformed-recipe.json"), "--dry-run", "--json"], {
61+
cwd: invocationDirectory,
62+
timeout: 120_000,
63+
maxBuffer: 4 * 1024 * 1024,
64+
})
65+
assert.fail("Malformed file-backed browser payloads should fail recipe validation")
66+
} catch (error) {
67+
malformedStdout = String((error as { stdout?: string }).stdout ?? "")
68+
}
69+
const malformedOutput = JSON.parse(malformedStdout)
70+
assert.equal(malformedOutput.success, false)
71+
assert.deepEqual(malformedOutput.validation.issues.map((issue: { code: string }) => issue.code), ["invalid-steps-json", "invalid-scenario-json", "invalid-steps-json"])
72+
} finally {
73+
await rm(root, { recursive: true, force: true })
74+
}
75+
76+
console.log("browser recipe file payload integration passed")

0 commit comments

Comments
 (0)