From e0ca3fd79a4e759bdeb8b19b5e0b796470fec254 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 11 Jun 2026 11:34:02 -0400 Subject: [PATCH 1/2] ci(e2e): add Vitest job selector --- .github/workflows/e2e-vitest-scenarios.yaml | 63 +++++++++- .../e2e-scenarios-workflow.test.ts | 41 +++++- tools/e2e-scenarios/workflow-boundary.mts | 117 +++++++++++++++--- 3 files changed, 197 insertions(+), 24 deletions(-) diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index 360cbb19fb..9e13be0ebf 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -11,6 +11,11 @@ on: required: false default: "" type: string + jobs: + description: "Optional comma-separated free-standing live Vitest job ids. Empty runs all jobs only when scenarios is also empty." + required: false + default: "" + type: string pr_number: description: Optional PR number for selective-dispatch result comments. required: false @@ -21,10 +26,43 @@ permissions: contents: read concurrency: - group: e2e-vitest-scenarios-${{ github.ref }}-${{ inputs.scenarios || 'supported' }} + group: e2e-vitest-scenarios-${{ github.ref }}-${{ inputs.scenarios || 'supported' }}-${{ inputs.jobs || 'all-jobs' }} cancel-in-progress: false jobs: + validate-jobs: + runs-on: ubuntu-latest + steps: + - id: validate + name: Validate free-standing job selector + env: + JOBS: ${{ inputs.jobs }} + SCENARIOS: ${{ inputs.scenarios }} + run: | + set -euo pipefail + allowed_jobs="openshell-version-pin-vitest,onboard-negative-paths-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery" + if [ -n "${JOBS}" ] && [ -n "${SCENARIOS}" ]; then + echo "::error::Use either scenarios or jobs, not both." >&2 + exit 1 + fi + if [ -z "${JOBS}" ]; then + echo "No free-standing job selector supplied; default workflow behavior applies." + exit 0 + fi + if [[ ! "${JOBS}" =~ ^[A-Za-z0-9_-]+(,[A-Za-z0-9_-]+)*$ ]]; then + echo "::error::Invalid jobs input: ${JOBS}" >&2 + exit 1 + fi + IFS=',' read -ra requested <<< "${JOBS}" + for job in "${requested[@]}"; do + if [[ ",${allowed_jobs}," != *",${job},"* ]]; then + echo "::error::Unknown free-standing Vitest job: ${job}" >&2 + echo "::error::Allowed jobs: ${allowed_jobs}" >&2 + exit 1 + fi + done + printf 'Validated free-standing Vitest jobs: `%s`\n' "${JOBS}" >> "$GITHUB_STEP_SUMMARY" + generate-matrix: runs-on: ubuntu-latest outputs: @@ -74,6 +112,7 @@ jobs: live-scenarios: needs: generate-matrix + if: ${{ inputs.jobs == '' }} runs-on: ${{ matrix.runner }} timeout-minutes: 45 strategy: @@ -174,6 +213,8 @@ jobs: # because the matrix above only runs registry-scenarios.test.ts. Modeled on # #5049's free-standing pattern. openshell-version-pin-vitest: + needs: validate-jobs + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',openshell-version-pin-vitest,') }} runs-on: ubuntu-latest timeout-minutes: 15 env: @@ -213,6 +254,8 @@ jobs: retention-days: 14 onboard-negative-paths-vitest: + needs: validate-jobs + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',onboard-negative-paths-vitest,') }} runs-on: ubuntu-latest timeout-minutes: 15 env: @@ -259,7 +302,8 @@ jobs: # protocol/history contract. The retained legacy bash lane remains the # source for full closeout until a later PR proves replacement and deletes it. openclaw-tui-chat-correlation-vitest: - if: ${{ inputs.scenarios == '' }} + needs: validate-jobs + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',openclaw-tui-chat-correlation-vitest,') }} runs-on: ubuntu-latest timeout-minutes: 75 env: @@ -338,6 +382,8 @@ jobs: # restore the /tmp guard chain after pod recreate). Will fail on `main` # until the #2701 fix lands; flips green afterwards. gateway-guard-recovery: + needs: validate-jobs + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',gateway-guard-recovery,') }} runs-on: ubuntu-latest timeout-minutes: 45 env: @@ -430,6 +476,7 @@ jobs: runs-on: ubuntu-latest needs: [ + validate-jobs, generate-matrix, live-scenarios, openshell-version-pin-vitest, @@ -444,13 +491,18 @@ jobs: steps: - name: Post Vitest scenario results to PR uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + JOB_PR_NUMBER: ${{ inputs.pr_number }} + JOB_SCENARIOS: ${{ inputs.scenarios }} + JOBS: ${{ inputs.jobs }} with: script: | const needs = ${{ toJSON(needs) }}; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const workflowBranch = context.ref.replace('refs/heads/', ''); - const prNumberInput = ${{ toJSON(inputs.pr_number) }} || ''; - const requestedScenarios = ${{ toJSON(inputs.scenarios) }} || ''; + const prNumberInput = process.env.JOB_PR_NUMBER || ''; + const requestedScenarios = process.env.JOB_SCENARIOS || ''; + const requestedJobs = process.env.JOBS || ''; let prNumber = prNumberInput ? Number.parseInt(prNumberInput, 10) : undefined; if (!prNumber) { @@ -491,6 +543,9 @@ jobs: requestedScenarios ? `**Requested scenarios:** \`${requestedScenarios}\`` : '**Requested scenarios:** _(default — all supported)_', + requestedJobs + ? `**Requested jobs:** \`${requestedJobs}\`` + : '**Requested jobs:** _(default — all free-standing when no scenarios are requested)_', `**Summary:** ${passed.length} passed, ${failed.length} failed, ${skipped.length} skipped`, '', '| Job | Result |', diff --git a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts index bacbbfd369..84bcda58ba 100644 --- a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts +++ b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts @@ -27,6 +27,21 @@ describe("e2e-vitest-scenarios workflow boundary", () => { permissions: contents: read jobs: + validate-jobs: + runs-on: macos-latest + steps: + - name: Validate free-standing job selector + env: + JOBS: bad + run: echo unchecked + report-to-pr: + runs-on: ubuntu-latest + needs: [generate-matrix] + steps: + - name: Post Vitest scenario results to PR + env: + JOBS: bad + run: echo "\${{ inputs.pr_number }} \${{ inputs.scenarios }}" live-scenarios: runs-on: ubuntu-latest env: @@ -120,11 +135,19 @@ jobs: expect(errors).toEqual( expect.arrayContaining([ "workflow_dispatch missing input: scenarios", + "workflow_dispatch missing input: jobs", "workflow_dispatch must not expose legacy test_filter input", + "validate-jobs job must run on ubuntu-latest", + "validate-jobs step must pass jobs through JOBS env", + "validate-jobs step must pass scenarios through SCENARIOS env", + "step 'Validate free-standing job selector' run script must include Use either scenarios or jobs, not both", + "step 'Validate free-standing job selector' run script must include allowed_jobs=", + "step 'Validate free-standing job selector' run script must include Unknown free-standing Vitest job", "workflow missing generate-matrix job", "generate-matrix job must run on ubuntu-latest", "live-scenarios job must run on the matrix runner", "live-scenarios job must depend on generate-matrix", + "live-scenarios job must not run when a free-standing jobs selector is supplied", "live-scenarios strategy.fail-fast must be false", "live-scenarios matrix.include must come from generate-matrix output", "live-scenarios job must write artifacts under e2e-artifacts/vitest", @@ -153,8 +176,8 @@ jobs: "artifact upload path must include e2e-artifacts/vitest/${{ matrix.id }}/shell/", "artifact upload retention-days must be 14", "upload-artifact action must be pinned to a full commit SHA", - "openshell-version-pin-vitest job must run independently of generate-matrix", - "openshell-version-pin-vitest job must run independently of workflow dispatch scenario filters", + "openshell-version-pin-vitest job must depend on validate-jobs", + "openshell-version-pin-vitest job must use the shared jobs selector condition", "openshell-version-pin-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", "openshell-version-pin-vitest job must write artifacts under e2e-artifacts/vitest/openshell-version-pin", "openshell-version-pin-vitest job env must not include NVIDIA_API_KEY", @@ -172,8 +195,8 @@ jobs: "openshell-version-pin-vitest artifact upload must set include-hidden-files: false", "openshell-version-pin-vitest artifact upload must ignore missing fixture artifacts", "openshell-version-pin-vitest artifact upload retention-days must be 14", - "onboard-negative-paths-vitest job must run independently of generate-matrix", - "onboard-negative-paths-vitest job must run independently of workflow dispatch scenario filters", + "onboard-negative-paths-vitest job must depend on validate-jobs", + "onboard-negative-paths-vitest job must use the shared jobs selector condition", "onboard-negative-paths-vitest job must set NEMOCLAW_RUN_E2E_SCENARIOS=1", "onboard-negative-paths-vitest job must write artifacts under e2e-artifacts/vitest/onboard-negative-paths", "onboard-negative-paths-vitest job env must not include NVIDIA_API_KEY", @@ -191,6 +214,16 @@ jobs: "onboard-negative-paths-vitest artifact upload must set include-hidden-files: false", "onboard-negative-paths-vitest artifact upload must ignore missing fixture artifacts", "onboard-negative-paths-vitest artifact upload retention-days must be 14", + "openclaw-tui-chat-correlation-vitest job must depend on validate-jobs", + "openclaw-tui-chat-correlation-vitest job must use the shared jobs selector condition", + "gateway-guard-recovery job must depend on validate-jobs", + "gateway-guard-recovery job must use the shared jobs selector condition", + "report-to-pr job must wait for validate-jobs", + "report-to-pr job must wait for live-scenarios", + "report-to-pr step must pass pr_number through JOB_PR_NUMBER env", + "report-to-pr step must pass scenarios through JOB_SCENARIOS env", + "step 'Post Vitest scenario results to PR' run script must include process.env.JOBS", + "step 'Post Vitest scenario results to PR' run script must include **Requested jobs:**", ]), ); } finally { diff --git a/tools/e2e-scenarios/workflow-boundary.mts b/tools/e2e-scenarios/workflow-boundary.mts index 9b06e6d219..5d1e345339 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -122,6 +122,53 @@ function requireNoDispatchInputInterpolation( } } +function freeStandingJobIf(jobName: string): string { + return `\${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',${jobName},') }}`; +} + +function validateFreeStandingJobSelector( + errors: string[], + jobs: WorkflowRecord, + jobName: string, +): void { + const job = asRecord(jobs[jobName]); + if (job.needs !== "validate-jobs") { + errors.push(`${jobName} job must depend on validate-jobs`); + } + if (job.if !== freeStandingJobIf(jobName)) { + errors.push(`${jobName} job must use the shared jobs selector condition`); + } +} + +function validateJobsSelector(errors: string[], jobs: WorkflowRecord): void { + const job = asRecord(jobs["validate-jobs"]); + if (Object.keys(job).length === 0) { + errors.push("workflow missing validate-jobs job"); + return; + } + if (job["runs-on"] !== "ubuntu-latest") { + errors.push("validate-jobs job must run on ubuntu-latest"); + } + const steps = asSteps(job.steps); + requireNoDispatchInputInterpolation(errors, steps); + const validate = requireJobStep(errors, "validate-jobs", steps, "Validate free-standing job selector"); + const env = asRecord(validate?.env); + if (env.JOBS !== "${{ inputs.jobs }}") { + errors.push("validate-jobs step must pass jobs through JOBS env"); + } + if (env.SCENARIOS !== "${{ inputs.scenarios }}") { + errors.push("validate-jobs step must pass scenarios through SCENARIOS env"); + } + requireRunContains(errors, validate, "Use either scenarios or jobs, not both"); + requireRunContains(errors, validate, "allowed_jobs="); + requireRunContains(errors, validate, "openshell-version-pin-vitest"); + requireRunContains(errors, validate, "onboard-negative-paths-vitest"); + requireRunContains(errors, validate, "openclaw-tui-chat-correlation-vitest"); + requireRunContains(errors, validate, "gateway-guard-recovery"); + requireRunContains(errors, validate, "^[A-Za-z0-9_-]+(,[A-Za-z0-9_-]+)*$"); + requireRunContains(errors, validate, "Unknown free-standing Vitest job"); +} + function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRecord): void { const jobName = "openshell-version-pin-vitest"; const job = asRecord(jobs[jobName]); @@ -133,14 +180,7 @@ function validateOpenShellVersionPinVitestJob(errors: string[], jobs: WorkflowRe if (job["runs-on"] !== "ubuntu-latest") { errors.push("openshell-version-pin-vitest job must run on ubuntu-latest"); } - if (Object.hasOwn(job, "needs")) { - errors.push("openshell-version-pin-vitest job must run independently of generate-matrix"); - } - if (Object.hasOwn(job, "if")) { - errors.push( - "openshell-version-pin-vitest job must run independently of workflow dispatch scenario filters", - ); - } + validateFreeStandingJobSelector(errors, jobs, jobName); const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { @@ -221,14 +261,7 @@ function validateOnboardNegativePathsVitestJob(errors: string[], jobs: WorkflowR if (job["runs-on"] !== "ubuntu-latest") { errors.push("onboard-negative-paths-vitest job must run on ubuntu-latest"); } - if (Object.hasOwn(job, "needs")) { - errors.push("onboard-negative-paths-vitest job must run independently of generate-matrix"); - } - if (Object.hasOwn(job, "if")) { - errors.push( - "onboard-negative-paths-vitest job must run independently of workflow dispatch scenario filters", - ); - } + validateFreeStandingJobSelector(errors, jobs, jobName); const jobEnv = asRecord(job.env); if (jobEnv.NEMOCLAW_RUN_E2E_SCENARIOS !== "1") { @@ -312,6 +345,7 @@ export function validateE2eVitestScenariosWorkflowBoundary( const dispatchInputs = asRecord(workflowDispatch.inputs); requireInput(errors, dispatchInputs, "scenarios"); + requireInput(errors, dispatchInputs, "jobs"); if (Object.hasOwn(dispatchInputs, "test_filter")) { errors.push("workflow_dispatch must not expose legacy test_filter input"); } @@ -320,6 +354,8 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (permissions.contents !== "read") errors.push("workflow permissions.contents must be read"); const jobs = asRecord(workflow.jobs); + validateJobsSelector(errors, jobs); + const generateMatrix = asRecord(jobs["generate-matrix"]); if (Object.keys(generateMatrix).length === 0) errors.push("workflow missing generate-matrix job"); if (generateMatrix["runs-on"] !== "ubuntu-latest") { @@ -357,6 +393,9 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (liveScenarios.needs !== "generate-matrix") { errors.push("live-scenarios job must depend on generate-matrix"); } + if (liveScenarios.if !== "${{ inputs.jobs == '' }}") { + errors.push("live-scenarios job must not run when a free-standing jobs selector is supplied"); + } const strategy = asRecord(liveScenarios.strategy); if (strategy["fail-fast"] !== false) { errors.push("live-scenarios strategy.fail-fast must be false"); @@ -482,6 +521,52 @@ export function validateE2eVitestScenariosWorkflowBoundary( validateOpenShellVersionPinVitestJob(errors, jobs); validateOnboardNegativePathsVitestJob(errors, jobs); + validateFreeStandingJobSelector(errors, jobs, "openclaw-tui-chat-correlation-vitest"); + validateFreeStandingJobSelector(errors, jobs, "gateway-guard-recovery"); + + const reportToPr = asRecord(jobs["report-to-pr"]); + if (Object.keys(reportToPr).length === 0) { + errors.push("workflow missing report-to-pr job"); + } else { + const needs = Array.isArray(reportToPr.needs) ? reportToPr.needs : []; + for (const required of [ + "validate-jobs", + "generate-matrix", + "live-scenarios", + "openshell-version-pin-vitest", + "onboard-negative-paths-vitest", + "openclaw-tui-chat-correlation-vitest", + "gateway-guard-recovery", + ]) { + if (!needs.includes(required)) errors.push(`report-to-pr job must wait for ${required}`); + } + const reportSteps = asSteps(reportToPr.steps); + const report = requireJobStep(errors, "report-to-pr", reportSteps, "Post Vitest scenario results to PR"); + const reportEnv = asRecord(report?.env); + if (reportEnv.JOBS !== "${{ inputs.jobs }}") { + errors.push("report-to-pr step must pass jobs through JOBS env"); + } + if (reportEnv.JOB_PR_NUMBER !== "${{ inputs.pr_number }}") { + errors.push("report-to-pr step must pass pr_number through JOB_PR_NUMBER env"); + } + if (reportEnv.JOB_SCENARIOS !== "${{ inputs.scenarios }}") { + errors.push("report-to-pr step must pass scenarios through JOB_SCENARIOS env"); + } + const reportScript = stringValue(asRecord(report?.with).script ?? report?.run); + if (!reportScript.includes("process.env.JOBS")) { + errors.push("step 'Post Vitest scenario results to PR' run script must include process.env.JOBS"); + } + if (!reportScript.includes("**Requested jobs:**")) { + errors.push("step 'Post Vitest scenario results to PR' run script must include **Requested jobs:**"); + } + for (const forbidden of ["toJSON(inputs.pr_number)", "toJSON(inputs.scenarios)"]) { + if (reportScript.includes(forbidden)) { + errors.push( + `step 'Post Vitest scenario results to PR' run script must not include ${forbidden}`, + ); + } + } + } return errors; } From a4c76c1b34e9cf78ccda53a59389c6f105d498b0 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 11 Jun 2026 11:46:11 -0400 Subject: [PATCH 2/2] ci(e2e): harden Vitest job selector reporting --- .github/workflows/e2e-vitest-scenarios.yaml | 15 ++++++++++----- .../support-tests/e2e-scenarios-workflow.test.ts | 7 ++++++- tools/e2e-scenarios/workflow-boundary.mts | 8 ++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index 9e13be0ebf..65e06c7eda 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -50,7 +50,7 @@ jobs: exit 0 fi if [[ ! "${JOBS}" =~ ^[A-Za-z0-9_-]+(,[A-Za-z0-9_-]+)*$ ]]; then - echo "::error::Invalid jobs input: ${JOBS}" >&2 + echo "::error::Invalid jobs input; use comma-separated job ids containing only letters, numbers, underscores, and hyphens." >&2 exit 1 fi IFS=',' read -ra requested <<< "${JOBS}" @@ -502,7 +502,10 @@ jobs: const workflowBranch = context.ref.replace('refs/heads/', ''); const prNumberInput = process.env.JOB_PR_NUMBER || ''; const requestedScenarios = process.env.JOB_SCENARIOS || ''; - const requestedJobs = process.env.JOBS || ''; + const rawRequestedJobs = process.env.JOBS || ''; + const jobsValidationPassed = needs['validate-jobs']?.result === 'success'; + const requestedJobs = jobsValidationPassed ? rawRequestedJobs : ''; + const jobsRejected = rawRequestedJobs && !jobsValidationPassed; let prNumber = prNumberInput ? Number.parseInt(prNumberInput, 10) : undefined; if (!prNumber) { @@ -543,9 +546,11 @@ jobs: requestedScenarios ? `**Requested scenarios:** \`${requestedScenarios}\`` : '**Requested scenarios:** _(default — all supported)_', - requestedJobs - ? `**Requested jobs:** \`${requestedJobs}\`` - : '**Requested jobs:** _(default — all free-standing when no scenarios are requested)_', + jobsRejected + ? '**Requested jobs:** _(selector rejected by validate-jobs)_' + : requestedJobs + ? `**Requested jobs:** \`${requestedJobs}\`` + : '**Requested jobs:** _(default — all free-standing when no scenarios are requested)_', `**Summary:** ${passed.length} passed, ${failed.length} failed, ${skipped.length} skipped`, '', '| Job | Result |', diff --git a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts index 84bcda58ba..056f8b1dc0 100644 --- a/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts +++ b/test/e2e-scenario/support-tests/e2e-scenarios-workflow.test.ts @@ -33,7 +33,8 @@ jobs: - name: Validate free-standing job selector env: JOBS: bad - run: echo unchecked + run: | + echo "::error::Invalid jobs input: \${JOBS}" report-to-pr: runs-on: ubuntu-latest needs: [generate-matrix] @@ -142,6 +143,8 @@ jobs: "validate-jobs step must pass scenarios through SCENARIOS env", "step 'Validate free-standing job selector' run script must include Use either scenarios or jobs, not both", "step 'Validate free-standing job selector' run script must include allowed_jobs=", + "step 'Validate free-standing job selector' run script must include Invalid jobs input; use comma-separated job ids", + "step 'Validate free-standing job selector' run script must not include Invalid jobs input: ${JOBS}", "step 'Validate free-standing job selector' run script must include Unknown free-standing Vitest job", "workflow missing generate-matrix job", "generate-matrix job must run on ubuntu-latest", @@ -223,6 +226,8 @@ jobs: "report-to-pr step must pass pr_number through JOB_PR_NUMBER env", "report-to-pr step must pass scenarios through JOB_SCENARIOS env", "step 'Post Vitest scenario results to PR' run script must include process.env.JOBS", + "step 'Post Vitest scenario results to PR' run script must check validate-jobs before echoing jobs", + "step 'Post Vitest scenario results to PR' run script must omit rejected job selectors", "step 'Post Vitest scenario results to PR' run script must include **Requested jobs:**", ]), ); diff --git a/tools/e2e-scenarios/workflow-boundary.mts b/tools/e2e-scenarios/workflow-boundary.mts index 5d1e345339..1189a0dd07 100644 --- a/tools/e2e-scenarios/workflow-boundary.mts +++ b/tools/e2e-scenarios/workflow-boundary.mts @@ -166,6 +166,8 @@ function validateJobsSelector(errors: string[], jobs: WorkflowRecord): void { requireRunContains(errors, validate, "openclaw-tui-chat-correlation-vitest"); requireRunContains(errors, validate, "gateway-guard-recovery"); requireRunContains(errors, validate, "^[A-Za-z0-9_-]+(,[A-Za-z0-9_-]+)*$"); + requireRunContains(errors, validate, "Invalid jobs input; use comma-separated job ids"); + requireRunDoesNotContain(errors, validate, "Invalid jobs input: ${JOBS}"); requireRunContains(errors, validate, "Unknown free-standing Vitest job"); } @@ -556,6 +558,12 @@ export function validateE2eVitestScenariosWorkflowBoundary( if (!reportScript.includes("process.env.JOBS")) { errors.push("step 'Post Vitest scenario results to PR' run script must include process.env.JOBS"); } + if (!reportScript.includes("jobsValidationPassed")) { + errors.push("step 'Post Vitest scenario results to PR' run script must check validate-jobs before echoing jobs"); + } + if (!reportScript.includes("selector rejected by validate-jobs")) { + errors.push("step 'Post Vitest scenario results to PR' run script must omit rejected job selectors"); + } if (!reportScript.includes("**Requested jobs:**")) { errors.push("step 'Post Vitest scenario results to PR' run script must include **Requested jobs:**"); }