From 20131234724ec2b1cf126d4bb13f510e9fa9bceb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 7 Aug 2026 10:03:14 +0800 Subject: [PATCH] fix(release): isolate private Telegram QA harness (#120088) --- .github/workflows/npm-telegram-beta-e2e.yml | 10 +++ .github/workflows/package-acceptance.yml | 2 +- .../lib/npm-telegram-live/prepare-package.mjs | 14 ++++- scripts/e2e/npm-telegram-live-docker.sh | 3 + test/scripts/npm-telegram-live.test.ts | 62 ++++++++++++++++--- .../package-acceptance-workflow.test.ts | 18 +++++- 6 files changed, 95 insertions(+), 14 deletions(-) diff --git a/.github/workflows/npm-telegram-beta-e2e.yml b/.github/workflows/npm-telegram-beta-e2e.yml index 374525f27aca..67b68ea16070 100644 --- a/.github/workflows/npm-telegram-beta-e2e.yml +++ b/.github/workflows/npm-telegram-beta-e2e.yml @@ -231,6 +231,16 @@ jobs: node-version: ${{ env.NODE_VERSION }} install-bun: "true" + - name: Build private QA harness runtime + env: + NODE_OPTIONS: --max-old-space-size=8192 + OPENCLAW_BUILD_PRIVATE_QA: "1" + run: | + set -euo pipefail + node scripts/build-all.mjs qaRuntime + test -f dist/plugin-sdk/qa-runtime.js + test -f dist/extensions/qa-lab/runtime-api.js + - name: Validate inputs and secrets env: PACKAGE_SPEC: ${{ inputs.package_spec }} diff --git a/.github/workflows/package-acceptance.yml b/.github/workflows/package-acceptance.yml index 8a67a767bc71..be2ae7463f3c 100644 --- a/.github/workflows/package-acceptance.yml +++ b/.github/workflows/package-acceptance.yml @@ -930,7 +930,7 @@ jobs: package_source_sha: ${{ needs.resolve_package.outputs.package_source_sha }} package_version: ${{ needs.resolve_package.outputs.package_version }} package_label: openclaw@${{ needs.resolve_package.outputs.package_version }} - harness_ref: ${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }} + harness_ref: ${{ inputs.workflow_ref }} provider_mode: ${{ needs.resolve_package.outputs.telegram_mode }} scenario: ${{ inputs.telegram_scenarios }} secrets: diff --git a/scripts/e2e/lib/npm-telegram-live/prepare-package.mjs b/scripts/e2e/lib/npm-telegram-live/prepare-package.mjs index 318ff7d76365..e284d6f27d84 100644 --- a/scripts/e2e/lib/npm-telegram-live/prepare-package.mjs +++ b/scripts/e2e/lib/npm-telegram-live/prepare-package.mjs @@ -1,7 +1,14 @@ // Prepares package manifests for npm Telegram live E2E scenarios. import fs from "node:fs"; -for (const packageJsonPath of process.argv.slice(2)) { +const packageJsonPaths = process.argv.slice(2); +if (packageJsonPaths.length !== 2) { + throw new Error( + `expected exactly two ephemeral package manifests, got ${packageJsonPaths.length}`, + ); +} + +for (const packageJsonPath of packageJsonPaths) { const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); pkg.exports = pkg.exports && typeof pkg.exports === "object" ? pkg.exports : {}; if (!pkg.exports["./plugin-sdk/gateway-runtime"]) { @@ -10,5 +17,10 @@ for (const packageJsonPath of process.argv.slice(2)) { default: "./dist/plugin-sdk/gateway-runtime.js", }; } + if (!pkg.exports["./plugin-sdk/qa-runtime"]) { + pkg.exports["./plugin-sdk/qa-runtime"] = { + default: "./.openclaw-qa-harness-dist/plugin-sdk/qa-runtime.js", + }; + } fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`); } diff --git a/scripts/e2e/npm-telegram-live-docker.sh b/scripts/e2e/npm-telegram-live-docker.sh index 2c222e7e4886..9a6f1ae7c22b 100755 --- a/scripts/e2e/npm-telegram-live-docker.sh +++ b/scripts/e2e/npm-telegram-live-docker.sh @@ -414,6 +414,7 @@ run_logged_print_heartbeat "npm-telegram-live-suite" 60 docker_e2e_run_with_harn "${docker_env[@]}" \ -v "$ROOT_DIR/.artifacts:/app/.artifacts" \ -v "$OUTPUT_DIR_HOST:$OUTPUT_DIR_CONTAINER" \ + -v "$ROOT_DIR/dist:/app/.openclaw-qa-harness-dist:ro" \ -v "$ROOT_DIR/extensions/qa-lab:/app/extensions/qa-lab:ro" \ -v "$ROOT_DIR/qa/scenarios:/app/qa/scenarios:ro" \ -v "$npm_prefix_host:/npm-global" \ @@ -453,6 +454,8 @@ rm -rf /app/node_modules/openclaw ln -sfnT "$openclaw_package_dir" /app/node_modules/openclaw rm -rf /app/dist ln -sfnT "$openclaw_package_dir/dist" /app/dist +rm -rf "$openclaw_package_dir/.openclaw-qa-harness-dist" +ln -sfnT /app/.openclaw-qa-harness-dist "$openclaw_package_dir/.openclaw-qa-harness-dist" cp "$openclaw_package_dir/package.json" /app/package.json node scripts/e2e/lib/npm-telegram-live/prepare-package.mjs \ /app/package.json \ diff --git a/test/scripts/npm-telegram-live.test.ts b/test/scripts/npm-telegram-live.test.ts index 12eac62d37e9..a23e8cab1c97 100644 --- a/test/scripts/npm-telegram-live.test.ts +++ b/test/scripts/npm-telegram-live.test.ts @@ -1,4 +1,5 @@ // Npm Telegram Live tests cover npm telegram live script behavior. +import { spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -267,7 +268,7 @@ describe("package Telegram live Docker E2E", () => { expect(script).toContain("OPENCLAW_NPM_TELEGRAM_RTT_CHECKS"); }); - it("keeps private QA harness imports local while using the installed package dist", () => { + it("keeps candidate runtime authoritative while mounting private QA dist separately", () => { const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8"); const preparePackage = readFileSync(PREPARE_PACKAGE_PATH, "utf8"); const gatewayRpcClient = readFileSync( @@ -278,15 +279,12 @@ describe("package Telegram live Docker E2E", () => { path.resolve(TEST_DIR, "../../extensions/qa-lab/src/runtime-api.ts"), "utf8", ); - const qaHarnessSources = [ - "extensions/qa-lab/api.ts", - "extensions/qa-lab/src/self-check.ts", - "extensions/qa-lab/src/live-transports/shared/live-transport-cli.ts", - "extensions/qa-lab/src/suite-launch.runtime.ts", - "extensions/qa-lab/src/suite.ts", - ].map((relativePath) => readFileSync(path.resolve(TEST_DIR, "../..", relativePath), "utf8")); expect(script).toContain('ln -sfnT "$openclaw_package_dir/dist" /app/dist'); + expect(script).toContain('-v "$ROOT_DIR/dist:/app/.openclaw-qa-harness-dist:ro"'); + expect(script).toContain( + 'ln -sfnT /app/.openclaw-qa-harness-dist "$openclaw_package_dir/.openclaw-qa-harness-dist"', + ); expect(script).toContain('cp "$openclaw_package_dir/package.json" /app/package.json'); expect(script).toContain('-v "$ROOT_DIR/extensions/qa-lab:/app/extensions/qa-lab:ro"'); expect(script).toContain('-v "$ROOT_DIR/qa/scenarios:/app/qa/scenarios:ro"'); @@ -295,11 +293,55 @@ describe("package Telegram live Docker E2E", () => { expect(script).toContain("/app/node_modules/openclaw/package.json"); expect(preparePackage).toContain('pkg.exports["./plugin-sdk/gateway-runtime"]'); expect(preparePackage).toContain('"./dist/plugin-sdk/gateway-runtime.js"'); + expect(preparePackage).toContain('pkg.exports["./plugin-sdk/qa-runtime"]'); + expect(preparePackage).toContain('"./.openclaw-qa-harness-dist/plugin-sdk/qa-runtime.js"'); expect(gatewayRpcClient).toContain('from "openclaw/plugin-sdk/gateway-runtime"'); expect(qaRuntimeApi).toContain('from "openclaw/plugin-sdk/gateway-runtime"'); - for (const source of qaHarnessSources) { - expect(source).not.toContain('from "openclaw/plugin-sdk/qa-runtime"'); + }); + + it("adds private harness exports only to two ephemeral manifests", () => { + const root = mkTempRoot(); + const packageJsonPaths = ["root-package.json", "installed-package.json"].map((name) => + path.join(root, name), + ); + for (const packageJsonPath of packageJsonPaths) { + writeFileSync(packageJsonPath, JSON.stringify({ exports: { ".": "./dist/index.js" } })); } + + const result = spawnSync(process.execPath, [PREPARE_PACKAGE_PATH, ...packageJsonPaths], { + encoding: "utf8", + }); + + expect(result.status).toBe(0); + for (const packageJsonPath of packageJsonPaths) { + const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { + exports: Record; + }; + expect(pkg.exports).toMatchObject({ + ".": "./dist/index.js", + "./plugin-sdk/gateway-runtime": { + types: "./dist/plugin-sdk/gateway-runtime.d.ts", + default: "./dist/plugin-sdk/gateway-runtime.js", + }, + "./plugin-sdk/qa-runtime": { + default: "./.openclaw-qa-harness-dist/plugin-sdk/qa-runtime.js", + }, + }); + } + + const thirdPackageJsonPath = path.join(root, "third-package.json"); + writeFileSync(thirdPackageJsonPath, JSON.stringify({ exports: { ".": "./dist/index.js" } })); + const rejected = spawnSync( + process.execPath, + [PREPARE_PACKAGE_PATH, ...packageJsonPaths, thirdPackageJsonPath], + { encoding: "utf8" }, + ); + + expect(rejected.status).toBe(1); + expect(rejected.stderr).toContain("expected exactly two ephemeral package manifests, got 3"); + expect(JSON.parse(readFileSync(thirdPackageJsonPath, "utf8"))).toEqual({ + exports: { ".": "./dist/index.js" }, + }); }); it("exposes installed package dependencies to the mounted QA harness", () => { diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 9cce288b9470..2961293532a1 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -1574,6 +1574,10 @@ describe("package acceptance workflow", () => { it("offers bounded product profiles and can run Telegram against the resolved artifact", () => { const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); const npmTelegramWorkflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8"); + const packageTelegram = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "package_telegram"); + const dockerAcceptance = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "docker_acceptance"); + const npmTelegram = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"); + const buildPrivateQa = workflowStep(npmTelegram, "Build private QA harness runtime"); expect(workflow).toContain("suite_profile:"); expect(workflow).toContain("published_upgrade_survivor_baseline:"); @@ -1638,9 +1642,19 @@ describe("package acceptance workflow", () => { expect(workflow).toContain( "package_source_sha: ${{ steps.resolve.outputs.package_source_sha }}", ); - expect(workflow).toContain( - "harness_ref: ${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }}", + expect(packageTelegram.with?.harness_ref).toBe("${{ inputs.workflow_ref }}"); + expect(dockerAcceptance.with?.ref).toBe( + "${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }}", ); + expect(buildPrivateQa.env).toMatchObject({ + NODE_OPTIONS: "--max-old-space-size=8192", + OPENCLAW_BUILD_PRIVATE_QA: "1", + }); + expectTextToIncludeAll(buildPrivateQa.run, [ + "node scripts/build-all.mjs qaRuntime", + "test -f dist/plugin-sdk/qa-runtime.js", + "test -f dist/extensions/qa-lab/runtime-api.js", + ]); expect(workflow).toContain('fallback_version="$(npm view openclaw@latest version)"'); expect(workflow).toContain('echo "baseline=$fallback_baseline" >> "$GITHUB_OUTPUT"'); expect(workflow).toContain(