Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,8 @@ For synthetic web performance investigations, enable `capture=performance` and u

`wordpress.browser-actions` drives the preview with an ordered interaction script so Codebox can prove a plugin still *works* under interaction, not just that it renders. Pass the script as `steps-json=<array>` (inline JSON, or `@<path>` to read it from a file). Each step is a thin, stable mapping over a Playwright locator action — this is not a test-runner DSL.

Action journeys and adaptive exploration can declare their browser context with `browser-environment-json=<object>` (inline or `@<path>`) or the compatible `device`, `viewport`, `device-scale-factor`, `is-mobile`, `has-touch`, `user-agent`, `permissions`, `locale`, `timezone`, and `geolocation-*` arguments. These controls are applied when the Playwright context is created, so mobile/touch emulation and granted, denied, or prompt geolocation remain active for the full journey. Action summaries keep requested configuration, provider-resolved configuration, and browser-observed state separate, with explicit unsupported and inconclusive dimensions. `wordpress.browser-scenario` accepts the same arguments or an `environment` object inside `scenario-json`; probe collection and authored actions share one owned context/page, while multi-actor scenarios create one isolated configured context per actor.

Step kinds: `navigate` (`url`, optional `waitFor=domcontentloaded|load|networkidle`), `click`/`hover` (`selector` or `text`), `fill`/`type` (`selector`, `value`), `press` (`key`, optional `selector`), `drag` (`from` selector, `to` as `{ "selector": ... }` or `{ "x": n, "y": n }`), `select` (`selector`, `value` or `values`), `waitFor` (`selector` or `waitFor=domcontentloaded|load|networkidle|duration|selector:<sel>`), `evaluate` (`expression`, optional `assert` to deep-equal the result), `expect` (`selector`, optional `state=visible|hidden|attached|detached|enabled|disabled|checked|unchecked|editable`), and `screenshot` (optional `name` for a named capture). Every step may set its own `timeout=<n>s`; the command also accepts a global `step-timeout=<n>s` (per step) and `timeout=<n>s` (total-script budget). Both are bounded and deterministic — the run stops cleanly on the first failing step, with no silent partial success.

The arbitrary-JS `evaluate` step is policy-gated **separately** from the non-JS interaction steps: a script containing `evaluate` requires `wordpress.browser-actions.evaluate` in the runtime policy in addition to `wordpress.browser-actions`. Click/fill/drag/expect and friends never require the extra grant, so a consumer can allow UI driving while still forbidding arbitrary page JS.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@
"test:generic-primitives": "npm run test:artifact-path-primitives && npm run test:browser-callback-materialization-contracts && npm run test:browser-canonical-preview-origin && npm run test:source-package-compiler-primitives && npm run test:bench-command-step-behavior && npm run test:generic-ability-runtime-run",
"test:browser-artifact-session": "tsx tests/browser-artifact-session.test.ts",
"test:browser-environment-matrix": "tsx --test tests/browser-environment-matrix.test.ts tests/browser-environment-matrix.browser.test.ts",
"test:browser-actions-environment": "tsx --test tests/browser-actions-environment.browser.test.ts",
"test:browser-recipe-file-payloads-integration": "tsx tests/browser-recipe-file-payloads.integration.test.ts",
"test:browser-diagnostic-providers": "tsx tests/browser-diagnostic-providers.test.ts",
"test:browser-capture-html-diagnostics-reliability": "tsx tests/browser-capture-html-diagnostics-reliability.test.ts",
"test:browser-provider-permissions": "tsx tests/browser-provider-permissions.test.ts",
Expand Down
22 changes: 19 additions & 3 deletions packages/cli/src/agent-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function agentRuntimeMounts(options: AgentRuntimeProbeOptions): AgentRunt

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

function rewriteRecipeExecutionArgs(command: string, args: readonly string[], inputMountPathMap: readonly InputMountPathMapping[] = []): string[] {
const rewritten = rewriteInputMountPathArgs(args, inputMountPathMap)
function rewriteRecipeExecutionArgs(command: string, args: readonly string[], recipeDirectory: string, inputMountPathMap: readonly InputMountPathMapping[] = []): string[] {
const rewritten = rewriteRecipeBrowserPayloadArgs(command, rewriteInputMountPathArgs(args, inputMountPathMap), recipeDirectory)
if (command === "wordpress.run-workload") {
return rewriteInputMountPathJsonArgs(rewritten, ["workload-json"], inputMountPathMap)
}
Expand All @@ -197,6 +197,22 @@ function rewriteRecipeExecutionArgs(command: string, args: readonly string[], in
return rewritten
}

function rewriteRecipeBrowserPayloadArgs(command: string, args: readonly string[], recipeDirectory: string): string[] {
const fileBackedArgs = command === "wordpress.browser-actions"
? new Set(["steps-json", "browser-environment-json"])
: command === "wordpress.browser-scenario"
? new Set(["scenario-json", "steps-json", "browser-environment-json"])
: undefined
if (!fileBackedArgs) return [...args]
return args.map((arg) => {
const separator = arg.indexOf("=")
if (separator < 0 || !fileBackedArgs.has(arg.slice(0, separator))) return arg
const value = arg.slice(separator + 1)
if (!value.startsWith("@")) return arg
return `${arg.slice(0, separator + 1)}@${resolve(recipeDirectory, value.slice(1))}`
})
}

async function wordpressRunWorkloadExecutionSpec(step: WorkspaceRecipe["workflow"]["steps"][number], recipeDirectory: string): Promise<ExecutionSpec & { args: string[] }> {
const args = step.args ?? []
const workloadJson = commandArgValue(args, "workload-json")
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/recipe-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function runRecipe(options: RecipeRunOptions, interruption?: Recipe
await artifactPointer.update({ commandStatus: "queued" })
const issues = [
...await validateWorkspaceRecipe(recipe, recipePath),
...validateRecipeRuntimePolicy(recipe, options.policy ?? recipePolicy(recipe)),
...validateRecipeRuntimePolicy(recipe, options.policy ?? recipePolicy(recipe, recipeDirectory), recipeDirectory),
]
if (issues.length > 0) {
const failure = {
Expand Down Expand Up @@ -727,7 +727,7 @@ async function validateRecipe(options: RecipeValidateOptions): Promise<RecipeVal
const recipe = await loadWorkspaceRecipe(recipePath)
const issues = [
...await validateWorkspaceRecipe(recipe, recipePath),
...validateRecipeRuntimePolicy(recipe, options.policy ?? recipePolicy(recipe)),
...validateRecipeRuntimePolicy(recipe, options.policy ?? recipePolicy(recipe, dirname(recipePath)), dirname(recipePath)),
]

return {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/recipe-dry-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export async function dryRunRecipe(options: RecipeDryRunOptions, context: Recipe

const issues = [
...await validateWorkspaceRecipe(recipe, recipePath),
...validateRecipeRuntimePolicy(recipe, options.policy),
...validateRecipeRuntimePolicy(recipe, options.policy, recipeDirectory),
]

if (issues.length > 0) {
Expand Down Expand Up @@ -324,7 +324,7 @@ export async function dryRunRecipe(options: RecipeDryRunOptions, context: Recipe
}

export async function planWorkspaceRecipe(recipe: WorkspaceRecipe, recipeDirectory: string, options: RecipePlanOptions, context: RecipePlanContext): Promise<RecipePlan> {
const policy = options.policy ?? recipePolicy(recipe)
const policy = options.policy ?? recipePolicy(recipe, recipeDirectory)
const policyValidation = validateRuntimePolicy(policy)
const workspaces = recipeDryRunWorkspaces(recipe, recipeDirectory)
const extraPlugins = recipeDryRunExtraPlugins(recipe, recipeDirectory)
Expand Down
Loading
Loading