From 255ca8d96b766e4a74cbc3fea64fb44942e01d6e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 15 Aug 2026 23:47:25 -0700 Subject: [PATCH] fix(ci): trust frozen plugin prerelease fixtures (#124446) * fix(ci): trust frozen plugin prerelease fixtures * fix(ci): apply scoped plugin test omissions --- .github/workflows/full-release-validation.yml | 25 ++- .github/workflows/plugin-prerelease.yml | 109 +++++++++- .../package-acceptance-workflow.test.ts | 1 + .../plugin-prerelease-test-plan.test.ts | 205 +++++++++++++++++- 4 files changed, 331 insertions(+), 9 deletions(-) diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 0e1ad6ac7f7..9bffba1bad3 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -84,6 +84,11 @@ on: - qa-live - npm-telegram - performance + plugin_prerelease_node_exclude_patterns_json: + description: Exact Plugin Prerelease Node test paths omitted only for frozen-target validation + required: false + default: "[]" + type: string reuse_evidence: description: Reuse matching green product validation for the same target or a changelog-only Release SHA required: false @@ -216,7 +221,9 @@ jobs: RERUN_GROUP: ${{ inputs.rerun_group }} LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }} CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }} + PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} run: | + plugin_prerelease_node_exclusions="$(jq -c . <<< "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON")" { echo "## Full release validation" echo @@ -246,6 +253,7 @@ jobs: fi if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "plugin-prerelease" ]]; then echo "- Plugin prerelease: \`Plugin Prerelease\` with \`target_ref=${TARGET_SHA}\`" + echo "- Plugin prerelease Node exclusions: \`${plugin_prerelease_node_exclusions}\`" else echo "- Plugin prerelease: skipped by rerun group" fi @@ -342,6 +350,7 @@ jobs: NPM_TELEGRAM_SCENARIO: ${{ inputs.npm_telegram_scenario }} SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }} ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} + PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} run: | set -euo pipefail # Lane-selection inputs must match the prior run's manifest exactly; @@ -360,6 +369,7 @@ jobs: --arg npmTelegramScenario "$NPM_TELEGRAM_SCENARIO" \ --arg skipPackageTelegramE2e "$SKIP_PACKAGE_TELEGRAM_E2E" \ --arg allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \ + --arg pluginPrereleaseNodeExcludePatternsJson "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" \ '{ provider: $provider, mode: $mode, @@ -373,7 +383,8 @@ jobs: npmTelegramProviderMode: $npmTelegramProviderMode, npmTelegramScenario: $npmTelegramScenario, skipPackageTelegramE2e: $skipPackageTelegramE2e, - allowUnreleasedChangelog: $allowUnreleasedChangelog + allowUnreleasedChangelog: $allowUnreleasedChangelog, + pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson }')" bash workflow/scripts/github/find-reusable-release-validation.sh \ --target-sha "$TARGET_SHA" \ @@ -848,15 +859,19 @@ jobs: dispatch_and_wait ci.yml "$dispatch_run_name" "${args[@]}" ;; plugin-prerelease) + plugin_prerelease_node_exclusions="$( + jq -c . <<< "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" + )" { echo "### Plugin prerelease" echo echo "- Target ref: \`${TARGET_REF}\`" echo "- Target SHA: \`${TARGET_SHA}\`" + echo "- Frozen-target Node test omissions: \`${plugin_prerelease_node_exclusions}\`" } >> "$GITHUB_STEP_SUMMARY" dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-plugin-prerelease" dispatch_run_name="Plugin Prerelease ${dispatch_id}" - args=(-f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f dispatch_id="$dispatch_id") + args=(-f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f dispatch_id="$dispatch_id" -f node_test_exclude_patterns_json="$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON") if [[ -n "${CANDIDATE_ARTIFACT_JSON// }" ]]; then args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON") fi @@ -1006,6 +1021,7 @@ jobs: CHILD_WORKFLOW_REF: ${{ github.ref_name }} PARENT_WORKFLOW_SHA: ${{ github.sha }} CANDIDATE_ARTIFACT_JSON: ${{ needs.prepare_release_candidate.outputs.candidate_artifact_json }} + PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} FAIL_FAST: ${{ inputs.fail_fast }} run: *full_release_child_dispatch @@ -1667,6 +1683,7 @@ jobs: NPM_TELEGRAM_SCENARIO: ${{ inputs.npm_telegram_scenario }} SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }} ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} + PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} run: | set -euo pipefail manifest_dir="${RUNNER_TEMP}/full-release-validation" @@ -1743,6 +1760,7 @@ jobs: --arg npmTelegramScenario "$NPM_TELEGRAM_SCENARIO" \ --arg skipPackageTelegramE2e "$SKIP_PACKAGE_TELEGRAM_E2E" \ --arg allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \ + --arg pluginPrereleaseNodeExcludePatternsJson "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" \ '{ version: 3, workflowName: $workflowName, @@ -1770,7 +1788,8 @@ jobs: npmTelegramProviderMode: $npmTelegramProviderMode, npmTelegramScenario: $npmTelegramScenario, skipPackageTelegramE2e: $skipPackageTelegramE2e, - allowUnreleasedChangelog: $allowUnreleasedChangelog + allowUnreleasedChangelog: $allowUnreleasedChangelog, + pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson }, controls: { stableSoakRequired: ($releaseProfile == "stable" or $releaseProfile == "full"), diff --git a/.github/workflows/plugin-prerelease.yml b/.github/workflows/plugin-prerelease.yml index df56e761df6..e9848effca2 100644 --- a/.github/workflows/plugin-prerelease.yml +++ b/.github/workflows/plugin-prerelease.yml @@ -20,6 +20,11 @@ on: required: false default: false type: boolean + node_test_exclude_patterns_json: + description: Full Release Validation-only exact plugin test paths omitted for a frozen target + required: false + default: "[]" + type: string dispatch_id: description: Optional parent workflow dispatch identifier required: false @@ -53,11 +58,73 @@ jobs: plugin_prerelease_static_matrix: ${{ steps.manifest.outputs.plugin_prerelease_static_matrix }} run_plugin_prerelease_node: ${{ steps.manifest.outputs.run_plugin_prerelease_node }} plugin_prerelease_node_matrix: ${{ steps.manifest.outputs.plugin_prerelease_node_matrix }} + node_test_exclude_patterns_json: ${{ steps.node_test_exclusions.outputs.patterns_json }} run_plugin_prerelease_extensions: ${{ steps.manifest.outputs.run_plugin_prerelease_extensions }} plugin_prerelease_extension_matrix: ${{ steps.manifest.outputs.plugin_prerelease_extension_matrix }} run_plugin_prerelease_docker: ${{ steps.manifest.outputs.run_plugin_prerelease_docker }} plugin_prerelease_docker_lanes: ${{ steps.manifest.outputs.plugin_prerelease_docker_lanes }} steps: + - name: Validate frozen-target Node exclusions + id: node_test_exclusions + env: + FULL_RELEASE_VALIDATION: ${{ inputs.full_release_validation && 'true' || 'false' }} + NODE_TEST_EXCLUDE_PATTERNS_JSON: ${{ inputs.node_test_exclude_patterns_json }} + shell: bash + run: | + set -euo pipefail + node --input-type=module <<'EOF' + import { appendFileSync } from "node:fs"; + + const rawPatterns = process.env.NODE_TEST_EXCLUDE_PATTERNS_JSON ?? ""; + let patterns; + try { + patterns = JSON.parse(rawPatterns); + } catch (error) { + console.error(`node_test_exclude_patterns_json must be valid JSON: ${error.message}`); + process.exit(1); + } + if (!Array.isArray(patterns)) { + console.error("node_test_exclude_patterns_json must be a JSON array"); + process.exit(1); + } + const invalidPattern = patterns.find((pattern) => { + if (typeof pattern !== "string" || pattern.length === 0 || pattern !== pattern.trim()) { + return true; + } + const segments = pattern.split("/"); + return ( + !pattern.startsWith("src/plugins/") || + !pattern.endsWith(".test.ts") || + pattern.includes("\\") || + pattern.includes("..") || + segments.some((segment) => segment.length === 0 || segment === ".") || + !/^[A-Za-z0-9._/-]+$/u.test(pattern) + ); + }); + if (invalidPattern !== undefined) { + console.error( + `Invalid frozen-target Node test omission: ${JSON.stringify(invalidPattern)}`, + ); + process.exit(1); + } + if (new Set(patterns).size !== patterns.length) { + console.error("node_test_exclude_patterns_json must not contain duplicate paths"); + process.exit(1); + } + const fullReleaseValidation = process.env.FULL_RELEASE_VALIDATION === "true"; + if (!fullReleaseValidation && rawPatterns !== "[]") { + console.error( + "node_test_exclude_patterns_json may differ from [] only when full_release_validation=true", + ); + process.exit(1); + } + appendFileSync( + process.env.GITHUB_OUTPUT, + `patterns_json=${JSON.stringify(patterns)}\n`, + "utf8", + ); + EOF + - name: Checkout target uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -305,6 +372,32 @@ jobs: persist-credentials: false submodules: false + - name: Checkout trusted npm security inventory + if: inputs.full_release_validation + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ github.sha }} + path: .plugin-prerelease-trusted + fetch-depth: 1 + fetch-tags: false + persist-credentials: false + sparse-checkout: src/plugins/npm-install-security-scan.release.test.ts + sparse-checkout-cone-mode: false + submodules: false + + - name: Install trusted npm security inventory + if: inputs.full_release_validation + shell: bash + run: | + set -euo pipefail + trusted_checkout=.plugin-prerelease-trusted + trap 'rm -rf -- "$trusted_checkout"' EXIT + install -m 0644 \ + "$trusted_checkout/src/plugins/npm-install-security-scan.release.test.ts" \ + src/plugins/npm-install-security-scan.release.test.ts + rm -rf -- "$trusted_checkout" + trap - EXIT + - name: Setup Node environment uses: ./.github/actions/setup-node-env with: @@ -316,6 +409,7 @@ jobs: - name: Run release-only plugin Node shard env: NODE_OPTIONS: --max-old-space-size=8192 + NODE_TEST_EXCLUDE_PATTERNS_JSON: ${{ needs.preflight.outputs.node_test_exclude_patterns_json }} OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }} OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }} OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }} @@ -336,6 +430,16 @@ jobs: const includePatterns = JSON.parse( process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null", ); + const excludePatterns = JSON.parse( + process.env.NODE_TEST_EXCLUDE_PATTERNS_JSON ?? "[]", + ); + if ( + !Array.isArray(excludePatterns) || + excludePatterns.some((pattern) => typeof pattern !== "string") + ) { + console.error("Invalid frozen-target Node exclusions"); + process.exit(1); + } const childEnv = { ...process.env }; if (Array.isArray(includePatterns) && includePatterns.length > 0) { const includeFile = join( @@ -346,7 +450,10 @@ jobs: childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile; } - const result = spawnSync("pnpm", ["test", "--", ...configs], { + const excludeArgs = excludePatterns.map( + (pattern) => `--exclude=${pattern.slice("src/plugins/".length)}`, + ); + const result = spawnSync("pnpm", ["test", "--", ...configs, "--", ...excludeArgs], { env: childEnv, stdio: "inherit", }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index f2d690e8f36..f2ec9c9b46e 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -525,6 +525,7 @@ exit 0 PACKAGE_ACCEPTANCE_PACKAGE_SPEC: "", PACKAGE_SPEC: "openclaw@beta", PARENT_WORKFLOW_SHA: parentSha, + PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: "[]", PROVIDER: "openai", PROVIDER_MODE: "mock-openai", RELEASE_PACKAGE_SPEC: "", diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 14da35f19ca..dc56d2c001f 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -1,7 +1,8 @@ // Plugin Prerelease Test Plan tests cover plugin prerelease test plan script behavior. -import { execFileSync } from "node:child_process"; -import { readFileSync } from "node:fs"; -import { describe, expect, it } from "vitest"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; import { parse } from "yaml"; import { findLaneByName } from "../../scripts/lib/docker-e2e-plan.mts"; import { BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS } from "../../scripts/lib/docker-e2e-scenarios.mts"; @@ -14,9 +15,11 @@ import { pluginPrereleaseTimeoutComponents, releaseTimeoutForProfile, } from "../helpers/release-workflow-timeouts.js"; +import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const CHECKOUT_V6 = "actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10"; const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"; +const tempDirs = useAutoCleanupTempDirTracker(afterEach); type WorkflowStep = { env?: Record; @@ -63,6 +66,35 @@ function getDockerLane(name: string) { return lane; } +function runFrozenTargetNodeExclusionValidation(params: { + fullReleaseValidation: boolean; + patternsJson: string; +}) { + const workflow = readPluginPrereleaseWorkflow(); + const validationStep = workflow.jobs.preflight.steps.find( + (step: WorkflowStep) => step.name === "Validate frozen-target Node exclusions", + ); + if (!validationStep?.run) { + throw new Error("Missing frozen-target Node exclusion validation step"); + } + + const root = tempDirs.make("openclaw-plugin-prerelease-excludes-"); + const outputPath = join(root, "github-output"); + const result = spawnSync("bash", ["-c", validationStep.run], { + encoding: "utf8", + env: { + FULL_RELEASE_VALIDATION: String(params.fullReleaseValidation), + GITHUB_OUTPUT: outputPath, + NODE_TEST_EXCLUDE_PATTERNS_JSON: params.patternsJson, + PATH: process.env.PATH, + }, + }); + return { + ...result, + output: existsSync(outputPath) ? readFileSync(outputPath, "utf8") : "", + }; +} + describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { it("covers every pre-release plugin skill surface in the plugin prerelease plan", () => { const plan = assertPluginPrereleaseTestPlanComplete(); @@ -276,6 +308,166 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { expect(fixtureServer).toContain("/versions/${fixture.version}/artifact"); }); + it("validates frozen-target Node omissions as exact full-release-only plugin test paths", () => { + const defaultResult = runFrozenTargetNodeExclusionValidation({ + fullReleaseValidation: false, + patternsJson: "[]", + }); + expect(defaultResult.status, defaultResult.stderr).toBe(0); + expect(defaultResult.output).toBe("patterns_json=[]\n"); + + const validPatterns = [ + "src/plugins/example.test.ts", + "src/plugins/runtime/example.runtime.test.ts", + ]; + const fullReleaseResult = runFrozenTargetNodeExclusionValidation({ + fullReleaseValidation: true, + patternsJson: JSON.stringify(validPatterns), + }); + expect(fullReleaseResult.status, fullReleaseResult.stderr).toBe(0); + expect(fullReleaseResult.output).toBe(`patterns_json=${JSON.stringify(validPatterns)}\n`); + + for (const patternsJson of ["", "{}", "null", '"src/plugins/example.test.ts"']) { + const result = runFrozenTargetNodeExclusionValidation({ + fullReleaseValidation: true, + patternsJson, + }); + expect(result.status, `${patternsJson}\n${result.stderr}`).not.toBe(0); + } + + for (const patternsJson of [JSON.stringify(["src/plugins/example.test.ts"]), " [ ] "]) { + const result = runFrozenTargetNodeExclusionValidation({ + fullReleaseValidation: false, + patternsJson, + }); + expect(result.status, `${patternsJson}\n${result.stderr}`).not.toBe(0); + } + + for (const patternsJson of [ + JSON.stringify([""]), + JSON.stringify([42]), + JSON.stringify(["/src/plugins/example.test.ts"]), + JSON.stringify(["C:\\src\\plugins\\example.test.ts"]), + JSON.stringify(["../src/plugins/example.test.ts"]), + JSON.stringify(["./src/plugins/example.test.ts"]), + JSON.stringify(["src/plugins/../example.test.ts"]), + JSON.stringify(["src/plugins//example.test.ts"]), + JSON.stringify(["src/agents/example.test.ts"]), + JSON.stringify(["src/plugins/example.ts"]), + JSON.stringify(["src/plugins/*.test.ts"]), + JSON.stringify(["src/plugins/example?.test.ts"]), + JSON.stringify(["src/plugins/[example].test.ts"]), + JSON.stringify(["src/plugins/example.test.ts", "src/plugins/example.test.ts"]), + ]) { + const result = runFrozenTargetNodeExclusionValidation({ + fullReleaseValidation: true, + patternsJson, + }); + expect(result.status, `${patternsJson}\n${result.stderr}`).not.toBe(0); + } + }); + + it("keeps frozen-target fixture ownership and Node omissions explicit in release evidence", () => { + const pluginWorkflow = readPluginPrereleaseWorkflow(); + const pluginSource = readFileSync(".github/workflows/plugin-prerelease.yml", "utf8"); + const preflight = pluginWorkflow.jobs.preflight; + const nodeShard = pluginWorkflow.jobs["plugin-prerelease-node-shard"]; + const trustedCheckout = nodeShard.steps.find( + (step: WorkflowStep) => step.name === "Checkout trusted npm security inventory", + ); + const installInventory = nodeShard.steps.find( + (step: WorkflowStep) => step.name === "Install trusted npm security inventory", + ); + const runNodeShard = nodeShard.steps.find( + (step: WorkflowStep) => step.name === "Run release-only plugin Node shard", + ); + const releaseWorkflow = readFullReleaseValidationWorkflow(); + const releaseSource = readFileSync(".github/workflows/full-release-validation.yml", "utf8"); + const targetSummary = releaseWorkflow.jobs.resolve_target.steps.find( + (step: WorkflowStep) => step.name === "Summarize target", + ); + const pluginDispatch = releaseWorkflow.jobs.plugin_prerelease.steps.find( + (step: WorkflowStep) => step.name === "Dispatch and monitor plugin prerelease", + ); + const evidenceReuse = releaseWorkflow.jobs.evidence_reuse.steps.find( + (step: WorkflowStep) => step.name === "Find reusable validation evidence", + ); + const manifest = releaseWorkflow.jobs.summary.steps.find( + (step: WorkflowStep) => step.name === "Write release validation manifest", + ); + + expect(pluginWorkflow.on.workflow_dispatch.inputs.node_test_exclude_patterns_json).toEqual({ + default: "[]", + description: + "Full Release Validation-only exact plugin test paths omitted for a frozen target", + required: false, + type: "string", + }); + expect(preflight.outputs.node_test_exclude_patterns_json).toBe( + "${{ steps.node_test_exclusions.outputs.patterns_json }}", + ); + expect(trustedCheckout).toMatchObject({ + if: "inputs.full_release_validation", + uses: CHECKOUT_V6, + with: { + "persist-credentials": false, + ref: "${{ github.sha }}", + path: ".plugin-prerelease-trusted", + "sparse-checkout": "src/plugins/npm-install-security-scan.release.test.ts", + "sparse-checkout-cone-mode": false, + }, + }); + expect(installInventory?.run).toContain( + '"$trusted_checkout/src/plugins/npm-install-security-scan.release.test.ts"', + ); + expect(installInventory?.run).toContain( + "src/plugins/npm-install-security-scan.release.test.ts", + ); + expect(installInventory?.run).toContain('rm -rf -- "$trusted_checkout"'); + expect(runNodeShard?.env?.NODE_TEST_EXCLUDE_PATTERNS_JSON).toBe( + "${{ needs.preflight.outputs.node_test_exclude_patterns_json }}", + ); + expect(runNodeShard?.run).toContain('process.env.NODE_TEST_EXCLUDE_PATTERNS_JSON ?? "[]"'); + expect(runNodeShard?.run).toContain('pattern.slice("src/plugins/".length)'); + expect(runNodeShard?.run).toContain("`--exclude=${pattern.slice"); + expect(runNodeShard?.run).toContain('["test", "--", ...configs, "--", ...excludeArgs]'); + expect(pluginSource).not.toContain("runtime-llm.runtime.test.ts"); + + expect( + releaseWorkflow.on.workflow_dispatch.inputs.plugin_prerelease_node_exclude_patterns_json, + ).toEqual({ + default: "[]", + description: + "Exact Plugin Prerelease Node test paths omitted only for frozen-target validation", + required: false, + type: "string", + }); + expect(targetSummary?.env?.PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON).toBe( + "${{ inputs.plugin_prerelease_node_exclude_patterns_json }}", + ); + expect(targetSummary?.run).toContain("- Plugin prerelease Node exclusions:"); + expect(pluginDispatch?.env?.PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON).toBe( + "${{ inputs.plugin_prerelease_node_exclude_patterns_json }}", + ); + expect(pluginDispatch?.run).toContain( + '-f node_test_exclude_patterns_json="$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON"', + ); + expect(pluginDispatch?.run).toContain("- Frozen-target Node test omissions:"); + expect(evidenceReuse?.env?.PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON).toBe( + "${{ inputs.plugin_prerelease_node_exclude_patterns_json }}", + ); + expect(evidenceReuse?.run).toContain( + "pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson", + ); + expect(manifest?.env?.PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON).toBe( + "${{ inputs.plugin_prerelease_node_exclude_patterns_json }}", + ); + expect(manifest?.run).toContain( + "pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson", + ); + expect(releaseSource).not.toContain("runtime-llm.runtime.test.ts"); + }); + it("wires the full plugin prerelease plan into its release workflow", () => { const workflow = readCiWorkflow(); const preflight = workflow.jobs.preflight; @@ -477,7 +669,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { expect(normalCiDispatchCase).toContain('dispatch_and_wait ci.yml "$dispatch_run_name"'); expect(normalCiDispatchCase).not.toContain("full_release_validation=true"); expect(pluginPrereleaseScript).toContain( - 'args=(-f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f dispatch_id="$dispatch_id")', + 'args=(-f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f dispatch_id="$dispatch_id" -f node_test_exclude_patterns_json="$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON")', ); expect(pluginPrereleaseScript).toContain( 'args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")', @@ -500,7 +692,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { const pluginNodeShardScript = pluginWorkflow.jobs["plugin-prerelease-node-shard"].steps.find( (step: WorkflowStep) => step.name === "Run release-only plugin Node shard", ).run; - expect(pluginNodeShardScript).toContain('spawnSync("pnpm", ["test", "--", ...configs]'); + expect(pluginNodeShardScript).toContain( + 'spawnSync("pnpm", ["test", "--", ...configs, "--", ...excludeArgs]', + ); expect(pluginNodeShardScript).not.toContain("scripts/test-projects.mts"); expect(pluginWorkflow.on.workflow_dispatch.inputs.target_ref).toEqual({ default: "main", @@ -537,6 +731,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { plugin_prerelease_extension_matrix: "${{ steps.manifest.outputs.plugin_prerelease_extension_matrix }}", plugin_prerelease_node_matrix: "${{ steps.manifest.outputs.plugin_prerelease_node_matrix }}", + node_test_exclude_patterns_json: "${{ steps.node_test_exclusions.outputs.patterns_json }}", plugin_prerelease_static_matrix: "${{ steps.manifest.outputs.plugin_prerelease_static_matrix }}", run_plugin_prerelease_docker: "${{ steps.manifest.outputs.run_plugin_prerelease_docker }}",