1+ import { readFileSync } from "node:fs"
12import { readFile , stat } from "node:fs/promises"
23import { dirname , join , resolve } from "node:path"
34import { BROWSER_PROBE_CHROMIUM_PROFILE_IDS , RUNTIME_BACKED_FUZZ_SUITE_RUNNER_CAPABILITIES , assertFixtureImportDeterministicIdsSupported , assertWorkspaceRecipeJsonSchema , browserEnvironment , commandArgValue , normalizeRuntimeBackendKind , normalizeRuntimeMountTarget , parseCommandJson , safeArtifactRelativePath , validateBrowserInteractionScript , validateRuntimePolicy , validateSourcePackage , workspaceRecipeRuntimeCollectedArtifacts , type MountSpec , type RuntimeAssetSpec , type RuntimePolicy , type RuntimePreviewSpec , type WorkspaceRecipe , type WorkspaceRecipeDeclaredArtifact , type WorkspaceRecipeDependencyOverlay , type WorkspaceRecipeDistribution , type WorkspaceRecipeDistributionStartupProbe , type WorkspaceRecipeFixtureDatabase , type WorkspaceRecipeFuzzCasePhase , type WorkspaceRecipeMount , type WorkspaceRecipePluginRuntime , type WorkspaceRecipePluginRuntimeHealthProbe , type WorkspaceRecipeProbe , type WorkspaceRecipeRuntimeBackendPackage , type WorkspaceRecipeRuntimeOverlay , type WorkspaceRecipeSiteSeed } from "@automattic/wp-codebox-core"
@@ -513,7 +514,7 @@ export async function validateWorkspaceRecipe(recipe: WorkspaceRecipe, recipePat
513514 return validateWorkspaceRecipeSemantics ( recipe , recipePath )
514515}
515516
516- export function validateRecipeRuntimePolicy ( recipe : WorkspaceRecipe , policy : RuntimePolicy | undefined ) : RecipeValidationIssue [ ] {
517+ export function validateRecipeRuntimePolicy ( recipe : WorkspaceRecipe , policy : RuntimePolicy | undefined , recipeDirectory ?: string ) : RecipeValidationIssue [ ] {
517518 if ( ! policy ) {
518519 return [ ]
519520 }
@@ -528,7 +529,7 @@ export function validateRecipeRuntimePolicy(recipe: WorkspaceRecipe, policy: Run
528529 } )
529530 }
530531
531- const requiredCommands = recipePolicy ( recipe ) . commands
532+ const requiredCommands = recipePolicy ( recipe , recipeDirectory ) . commands
532533 for ( const command of requiredCommands ) {
533534 if ( ! policy . commands . includes ( command ) ) {
534535 issues . push ( {
@@ -1051,7 +1052,7 @@ function recipeFuzzWorkflowSteps(recipe: WorkspaceRecipe): RecipeWorkflowStepRef
10511052 } ) ) ) )
10521053}
10531054
1054- export function recipePolicy ( recipe : WorkspaceRecipe ) : RuntimePolicy {
1055+ export function recipePolicy ( recipe : WorkspaceRecipe , recipeDirectory ?: string ) : RuntimePolicy {
10551056 const pluginRuntimeCommands = [
10561057 ...( recipe . inputs ?. pluginRuntime ?. setup ?? [ ] ) ,
10571058 ...( recipe . inputs ?. pluginRuntime ?. healthProbes ?? [ ] ) . map ( pluginRuntimeHealthProbeStep ) ,
@@ -1091,7 +1092,7 @@ export function recipePolicy(recipe: WorkspaceRecipe): RuntimePolicy {
10911092 // Auto-grant the evaluate capability when a browser-actions step opts into the
10921093 // arbitrary-JS escape hatch by including an evaluate step. Recipe authors opt in
10931094 // by writing the step; direct `run` invocations still control the gate via --policy.
1094- if ( recipeDeclaredWorkflowSteps ( recipe ) . some ( ( { step } ) => ( step . command === "wordpress.browser-actions" || step . command === "wordpress.browser-scenario" ) && recipeStepUsesEvaluate ( step ) ) ) {
1095+ if ( recipeDeclaredWorkflowSteps ( recipe ) . some ( ( { step } ) => ( step . command === "wordpress.browser-actions" || step . command === "wordpress.browser-scenario" ) && recipeStepUsesEvaluate ( step , recipeDirectory ) ) ) {
10951096 commands . push ( "wordpress.browser-actions.evaluate" )
10961097 }
10971098
@@ -1649,10 +1650,10 @@ async function validateRecipeStepArgs(step: WorkspaceRecipe["workflow"]["steps"]
16491650 if ( step . command === "wordpress.browser-actions" ) {
16501651 const stepsJson = recipeStepArgValue ( step . args ?? [ ] , "steps-json" )
16511652
1652- if ( stepsJson && ! stepsJson . startsWith ( "@" ) ) {
1653+ if ( stepsJson ) {
16531654 let parsed : unknown
16541655 try {
1655- parsed = JSON . parse ( stepsJson )
1656+ parsed = JSON . parse ( stepsJson . startsWith ( "@" ) ? await readFile ( resolve ( recipeDirectory , stepsJson . slice ( 1 ) ) , "utf8" ) : stepsJson )
16561657 } catch ( error ) {
16571658 addIssue ( "invalid-steps-json" , `${ path } .args` , `wordpress.browser-actions steps-json must be valid JSON: ${ error instanceof Error ? error . message : String ( error ) } ` )
16581659 parsed = undefined
@@ -1670,9 +1671,9 @@ async function validateRecipeStepArgs(step: WorkspaceRecipe["workflow"]["steps"]
16701671 if ( step . command === "wordpress.browser-scenario" ) {
16711672 const scenarioJson = recipeStepArgValue ( step . args ?? [ ] , "scenario-json" )
16721673
1673- if ( scenarioJson && ! scenarioJson . startsWith ( "@" ) ) {
1674+ if ( scenarioJson ) {
16741675 try {
1675- const parsed = JSON . parse ( scenarioJson ) as unknown
1676+ const parsed = JSON . parse ( scenarioJson . startsWith ( "@" ) ? await readFile ( resolve ( recipeDirectory , scenarioJson . slice ( 1 ) ) , "utf8" ) : scenarioJson ) as unknown
16761677 if ( ! parsed || typeof parsed !== "object" || Array . isArray ( parsed ) ) {
16771678 addIssue ( "invalid-scenario-json" , `${ path } .args` , "wordpress.browser-scenario scenario-json must be a JSON object." )
16781679 } else {
@@ -1702,10 +1703,10 @@ async function validateRecipeStepArgs(step: WorkspaceRecipe["workflow"]["steps"]
17021703 }
17031704
17041705 const stepsJson = recipeStepArgValue ( step . args ?? [ ] , "steps-json" )
1705- if ( stepsJson && ! stepsJson . startsWith ( "@" ) ) {
1706+ if ( stepsJson ) {
17061707 let parsed : unknown
17071708 try {
1708- parsed = JSON . parse ( stepsJson )
1709+ parsed = JSON . parse ( stepsJson . startsWith ( "@" ) ? await readFile ( resolve ( recipeDirectory , stepsJson . slice ( 1 ) ) , "utf8" ) : stepsJson )
17091710 } catch ( error ) {
17101711 addIssue ( "invalid-steps-json" , `${ path } .args` , `wordpress.browser-scenario steps-json must be valid JSON: ${ error instanceof Error ? error . message : String ( error ) } ` )
17111712 }
@@ -1969,31 +1970,37 @@ function recipeBenchWorkloadsUseWpCli(value: unknown): boolean {
19691970 return record . type === "wp-cli" || recipeBenchWorkloadsUseWpCli ( record . run )
19701971}
19711972
1972- function recipeStepUsesEvaluate ( step : WorkspaceRecipe [ "workflow" ] [ "steps" ] [ number ] ) : boolean {
1973+ function recipeStepUsesEvaluate ( step : WorkspaceRecipe [ "workflow" ] [ "steps" ] [ number ] , recipeDirectory ?: string ) : boolean {
19731974 const scenarioRaw = recipeStepArgValue ( step . args ?? [ ] , "scenario-json" )
1974- if ( scenarioRaw && ! scenarioRaw . startsWith ( "@" ) ) {
1975+ if ( scenarioRaw ) {
19751976 try {
1976- const parsed = parseCommandJson ( scenarioRaw , "scenario-json" ) as { steps ?: unknown ; assertions ?: unknown }
1977+ const parsed = parseCommandJson ( recipeJsonArgForPolicy ( scenarioRaw , recipeDirectory ) , "scenario-json" ) as { steps ?: unknown ; assertions ?: unknown }
19771978 const steps = normalizeBrowserScenarioStepsForValidation ( parsed . steps )
19781979 const assertions = normalizeBrowserScenarioAssertionsForValidation ( parsed . assertions )
1979- return [ ...steps , ...assertions ] . some ( ( entry ) => entry && typeof entry === "object" && ( entry as { kind ?: unknown } ) . kind === "evaluate" )
1980+ if ( [ ...steps , ...assertions ] . some ( ( entry ) => entry && typeof entry === "object" && ( entry as { kind ?: unknown } ) . kind === "evaluate" ) ) return true
19801981 } catch {
1981- return false
1982+ // Semantic validation reports malformed or missing file-backed payloads.
19821983 }
19831984 }
19841985
19851986 const raw = recipeStepArgValue ( step . args ?? [ ] , "steps-json" )
1986- if ( ! raw || raw . startsWith ( "@" ) ) {
1987+ if ( ! raw ) {
19871988 return false
19881989 }
19891990 try {
1990- const parsed = parseCommandJson ( raw , "steps-json" )
1991+ const parsed = parseCommandJson ( recipeJsonArgForPolicy ( raw , recipeDirectory ) , "steps-json" )
19911992 return Array . isArray ( parsed ) && parsed . some ( ( entry ) => entry && typeof entry === "object" && ( entry as { kind ?: unknown } ) . kind === "evaluate" )
19921993 } catch {
19931994 return false
19941995 }
19951996}
19961997
1998+ function recipeJsonArgForPolicy ( raw : string , recipeDirectory ?: string ) : string {
1999+ if ( ! raw . startsWith ( "@" ) ) return raw
2000+ if ( ! recipeDirectory ) return ""
2001+ return readFileSync ( resolve ( recipeDirectory , raw . slice ( 1 ) ) , "utf8" )
2002+ }
2003+
19972004function normalizeBrowserScenarioStepsForValidation ( value : unknown ) : unknown [ ] {
19982005 if ( ! Array . isArray ( value ) ) {
19992006 return [ ]
0 commit comments