// Package Acceptance Workflow tests cover package acceptance workflow script behavior. import { execFileSync, spawnSync } from "node:child_process"; import { chmodSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs"; import { resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { parse } from "yaml"; import { FULL_RELEASE_WAIT_TIMEOUT_MINUTES } from "../../scripts/full-release-validation-at-sha.mts"; import { createReleaseWorkflowMatrixPlan } from "../../scripts/plan-release-workflow-matrix.mjs"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const PACKAGE_ACCEPTANCE_WORKFLOW = ".github/workflows/package-acceptance.yml"; const LIVE_E2E_WORKFLOW = ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml"; const INSTALL_SMOKE_REUSABLE_WORKFLOW = ".github/workflows/install-smoke-reusable.yml"; const CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW = ".github/workflows/openclaw-cross-os-release-checks-reusable.yml"; const LIVE_MEDIA_RUNNER_DOCKERFILE = ".github/images/live-media-runner/Dockerfile"; const LIVE_MEDIA_RUNNER_IMAGE = "ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04"; const LIVE_MEDIA_RUNNER_IMAGE_WORKFLOW = ".github/workflows/live-media-runner-image.yml"; const NPM_TELEGRAM_WORKFLOW = ".github/workflows/npm-telegram-beta-e2e.yml"; const MANTIS_DISCORD_SMOKE_WORKFLOW = ".github/workflows/mantis-discord-smoke.yml"; const MANTIS_DISCORD_STATUS_REACTIONS_WORKFLOW = ".github/workflows/mantis-discord-status-reactions.yml"; const MANTIS_DISCORD_THREAD_ATTACHMENT_WORKFLOW = ".github/workflows/mantis-discord-thread-attachment.yml"; const MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW = ".github/workflows/mantis-slack-desktop-smoke.yml"; const MANTIS_TELEGRAM_DESKTOP_PROOF_WORKFLOW = ".github/workflows/mantis-telegram-desktop-proof.yml"; const MANTIS_TELEGRAM_LIVE_WORKFLOW = ".github/workflows/mantis-telegram-live.yml"; const MANTIS_WEB_UI_CHAT_PROOF_WORKFLOW = ".github/workflows/mantis-web-ui-chat-proof.yml"; const PACKAGE_JSON = "package.json"; const SETUP_PNPM_STORE_CACHE_ACTION = ".github/actions/setup-pnpm-store-cache/action.yml"; const SETUP_RELEASE_HARNESS_ACTION = ".github/actions/setup-release-harness/action.yml"; const RELEASE_CHECKS_WORKFLOW = ".github/workflows/openclaw-release-checks.yml"; const RELEASE_TELEGRAM_QA_WORKFLOW = ".github/workflows/openclaw-release-telegram-qa.yml"; const RELEASE_PUBLISH_WORKFLOW = ".github/workflows/openclaw-release-publish.yml"; const PLUGIN_PRERELEASE_WORKFLOW = ".github/workflows/plugin-prerelease.yml"; const OPENCLAW_NPM_RELEASE_WORKFLOW = ".github/workflows/openclaw-npm-release.yml"; const PLUGIN_CLAWHUB_RELEASE_WORKFLOW = ".github/workflows/plugin-clawhub-release.yml"; const PLUGIN_NPM_RELEASE_WORKFLOW = ".github/workflows/plugin-npm-release.yml"; const ANDROID_RELEASE_WORKFLOW = ".github/workflows/android-release.yml"; const STABLE_MAIN_CLOSEOUT_WORKFLOW = ".github/workflows/openclaw-stable-main-closeout.yml"; const WINDOWS_NODE_RELEASE_WORKFLOW = ".github/workflows/windows-node-release.yml"; const FULL_RELEASE_VALIDATION_WORKFLOW = ".github/workflows/full-release-validation.yml"; const CI_WORKFLOW = ".github/workflows/ci.yml"; const PERFORMANCE_WORKFLOW = ".github/workflows/openclaw-performance.yml"; const FULL_RELEASE_CHILD_DISPATCHES = [ { jobName: "normal_ci", kind: "ci", nonceSuffix: "-ci", runName: "CI", stepName: "Dispatch and monitor CI", workflow: "ci.yml", }, { jobName: "plugin_prerelease", kind: "plugin-prerelease", nonceSuffix: "-plugin-prerelease", runName: "Plugin Prerelease", stepName: "Dispatch and monitor plugin prerelease", workflow: "plugin-prerelease.yml", }, { jobName: "release_checks", kind: "release-checks", nonceSuffix: "-release-checks", runName: "OpenClaw Release Checks", stepName: "Dispatch and monitor release checks", workflow: "openclaw-release-checks.yml", }, { jobName: "npm_telegram", kind: "npm-telegram", nonceSuffix: "-npm-telegram", runName: "NPM Telegram Beta E2E", stepName: "Dispatch and monitor npm Telegram E2E", workflow: "npm-telegram-beta-e2e.yml", }, { jobName: "performance", kind: "performance", nonceSuffix: "", runName: "OpenClaw Performance", stepName: "Dispatch and monitor OpenClaw Performance", workflow: "openclaw-performance.yml", }, ] as const; const REPO_ROOT = process.env.GITHUB_WORKSPACE ?? process.cwd(); const RELEASE_MAINTAINER_SKILL = resolve( REPO_ROOT, ".agents/skills/release-openclaw-maintainer/SKILL.md", ); const QA_LIVE_TRANSPORTS_WORKFLOW = ".github/workflows/qa-live-transports-convex.yml"; const UPDATE_MIGRATION_WORKFLOW = ".github/workflows/update-migration.yml"; const CI_CHECK_TESTBOX_WORKFLOW = ".github/workflows/ci-check-testbox.yml"; const CI_CHECK_ARM_TESTBOX_WORKFLOW = ".github/workflows/ci-check-arm-testbox.yml"; const CI_BUILD_ARTIFACTS_TESTBOX_WORKFLOW = ".github/workflows/ci-build-artifacts-testbox.yml"; const WINDOWS_BLACKSMITH_TESTBOX_WORKFLOW = ".github/workflows/windows-blacksmith-testbox.yml"; const CRABBOX_HYDRATE_WORKFLOW = ".github/workflows/crabbox-hydrate.yml"; const CRABBOX_CONFIG = ".crabbox.yaml"; const SCHEDULED_LIVE_CHECKS_WORKFLOW = ".github/workflows/openclaw-scheduled-live-checks.yml"; const CI_HYDRATE_LIVE_AUTH_SCRIPT = "scripts/ci-hydrate-live-auth.sh"; const RELEASE_CHECK_ARTIFACT_RESOLVER = "scripts/github/resolve-release-check-artifacts.sh"; const VERIFY_PROVIDER_SECRETS_SCRIPT = ".agents/skills/release-openclaw-ci/scripts/verify-provider-secrets.mjs"; const UPGRADE_SURVIVOR_RUN_SCRIPT = "scripts/e2e/lib/upgrade-survivor/run.sh"; const SETUP_NODE_V6 = "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e"; const DOWNLOAD_ARTIFACT_V8 = "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"; const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); type WorkflowStep = { "continue-on-error"?: boolean | string; env?: Record; id?: string; if?: string; name?: string; run?: string; shell?: string; uses?: string; with?: Record; "working-directory"?: string; }; type WorkflowMatrixEntry = { advisory?: boolean; chunk_id?: string; command?: string; profiles?: string; suite_group?: string; suite_id?: string; timeout_minutes?: number; }; type WorkflowJob = { "continue-on-error"?: boolean | string; concurrency?: { group?: string; "cancel-in-progress"?: boolean | string; }; environment?: string; env?: Record; if?: string; name?: string; needs?: string | string[]; outputs?: Record; permissions?: Record; "runs-on"?: string; strategy?: { "fail-fast"?: boolean; matrix?: { include?: WorkflowMatrixEntry[]; lane?: string; profile?: string[]; shard?: number[]; }; }; secrets?: string | Record; "timeout-minutes"?: number | string; steps?: WorkflowStep[]; uses?: string; with?: Record; }; type Workflow = { env?: Record; jobs?: Record; on?: { workflow_call?: { inputs?: Record; }; workflow_dispatch?: { inputs?: Record; }; }; }; function readWorkflow(path: string): Workflow { return parse(readFileSync(path, "utf8")) as Workflow; } function workflowPaths(): string[] { return readdirSync(".github/workflows") .filter((name) => name.endsWith(".yml")) .map((name) => `.github/workflows/${name}`); } function workflowJob(path: string, jobName: string): WorkflowJob { const job = readWorkflow(path).jobs?.[jobName]; if (!job) { throw new Error(`Expected workflow job ${jobName} in ${path}`); } return job; } function workflowStep(job: WorkflowJob, stepName: string): WorkflowStep { const step = job.steps?.find((candidate) => candidate.name === stepName); if (!step) { throw new Error(`Expected workflow step ${stepName}`); } return step; } function jobNeeds(job: WorkflowJob): string[] { return Array.isArray(job.needs) ? job.needs : job.needs ? [job.needs] : []; } function timeoutForProfile( timeout: number | string | undefined, profile: "beta" | "stable" | "full", ): number { if (typeof timeout === "number") { return timeout; } if (timeout === "${{ matrix.group.timeout_minutes || 60 }}") { return 60; } const match = timeout?.match( /^\$\{\{ inputs\.(?:release_profile|release_test_profile) == 'full' && ([0-9]+) \|\| ([0-9]+) \}\}$/u, ); if (!match) { throw new Error(`Unsupported release timeout expression: ${String(timeout)}`); } return Number(profile === "full" ? match[1] : match[2]); } function evaluatedJobTimeouts(path: string, jobName: string, job: WorkflowJob): number[] { const timeout = job["timeout-minutes"]; if (typeof timeout === "number") { return [timeout]; } if (timeout?.includes("inputs.release_")) { return (["beta", "stable", "full"] as const).map((profile) => timeoutForProfile(timeout, profile), ); } if (timeout === "${{ matrix.group.timeout_minutes || 60 }}") { return [60, 90]; } if (timeout !== "${{ matrix.timeout_minutes }}") { throw new Error(`Unsupported timeout for ${path}:${jobName}: ${String(timeout)}`); } const matrix = (job.strategy as { matrix?: unknown } | undefined)?.matrix; if (matrix && typeof matrix === "object" && "include" in matrix) { const include = (matrix as { include?: WorkflowMatrixEntry[] }).include; if (!Array.isArray(include) || include.length === 0) { throw new Error(`Missing static timeout matrix for ${path}:${jobName}`); } return include.map((entry) => { if (typeof entry.timeout_minutes !== "number") { throw new Error(`Missing matrix timeout for ${path}:${jobName}`); } return entry.timeout_minutes; }); } if (path === LIVE_E2E_WORKFLOW && jobName === "validate_docker_e2e") { return (["beta", "stable", "full"] as const).flatMap((releaseProfile) => createReleaseWorkflowMatrixPlan({ includeReleasePathSuites: true, releaseProfile, }).dockerE2e.matrix.include.map((entry: WorkflowMatrixEntry) => { if (typeof entry.timeout_minutes !== "number") { throw new Error(`Missing planned timeout for ${releaseProfile}:${entry.chunk_id}`); } return entry.timeout_minutes; }), ); } throw new Error(`Missing matrix timeout evaluator for ${path}:${jobName}`); } function pluginPrereleaseTimeoutFloor( pluginPrerelease: Workflow, liveE2e: Workflow, profile: "beta" | "stable" | "full", ): number { const preflight = pluginPrerelease.jobs?.preflight; const dockerSuite = pluginPrerelease.jobs?.["plugin-prerelease-docker-suite"]; const suite = pluginPrerelease.jobs?.["plugin-prerelease-suite"]; const validateSelectedRef = liveE2e.jobs?.validate_selected_ref; const prepareImage = liveE2e.jobs?.prepare_docker_e2e_image; const imageReady = liveE2e.jobs?.docker_e2e_image_ready; const dockerLanes = liveE2e.jobs?.validate_docker_lanes; if ( !preflight || !dockerSuite || !suite || !validateSelectedRef || !prepareImage || !imageReady || !dockerLanes ) { throw new Error("Missing plugin prerelease timeout-chain job"); } expect(jobNeeds(dockerSuite)).toContain("preflight"); expect(jobNeeds(prepareImage)).toEqual(["validate_selected_ref"]); expect(jobNeeds(imageReady)).toEqual(["prepare_docker_e2e_image"]); expect(jobNeeds(dockerLanes)).toEqual( expect.arrayContaining(["prepare_docker_e2e_image", "docker_e2e_image_ready"]), ); expect(jobNeeds(suite)).toContain("plugin-prerelease-docker-suite"); return [ timeoutForProfile(preflight["timeout-minutes"], profile), timeoutForProfile(validateSelectedRef["timeout-minutes"], profile), timeoutForProfile(prepareImage["timeout-minutes"], profile), timeoutForProfile(imageReady["timeout-minutes"], profile), timeoutForProfile(dockerLanes["timeout-minutes"], profile), timeoutForProfile(suite["timeout-minutes"], profile), ].reduce((total, value) => total + value, 0); } function runFullReleaseInputValidation(releaseProfile: string, skipTelegram: string) { const step = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"), "Validate release inputs", ); return spawnSync("bash", ["-c", step.run ?? ""], { encoding: "utf8", env: { PATH: process.env.PATH, RELEASE_PROFILE: releaseProfile, SKIP_PACKAGE_TELEGRAM_E2E: skipTelegram, }, }); } function runReleaseChecksInputValidation( releaseProfile: string, skipTelegram: string, rerunGroup = "all", runReleaseSoak = "false", liveSuiteFilter = "", ) { const step = workflowStep( workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target"), "Capture selected inputs", ); const workdir = tempDirs.make("release-checks-input-validation-"); const outputPath = resolve(workdir, "github-output"); const stepEnv = Object.fromEntries(Object.keys(step.env ?? {}).map((name) => [name, ""])); const result = spawnSync("bash", ["-c", step.run ?? ""], { encoding: "utf8", env: { ...stepEnv, CANDIDATE_ARTIFACT_JSON_INPUT: "", GITHUB_OUTPUT: outputPath, PATH: process.env.PATH, RELEASE_FAIL_FAST_INPUT: "false", RELEASE_LIVE_SUITE_FILTER_INPUT: liveSuiteFilter, RELEASE_MODE_INPUT: "both", RELEASE_PROFILE_INPUT: releaseProfile, RELEASE_PROVIDER_INPUT: "openai", RELEASE_QA_DISCORD_LIVE_CI_ENABLED: "false", RELEASE_QA_SLACK_LIVE_CI_ENABLED: "false", RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED: "false", RELEASE_REF_INPUT: "main", RELEASE_RERUN_GROUP_INPUT: rerunGroup, RELEASE_RUN_MATURITY_SCORECARD_INPUT: "false", RELEASE_RUN_RELEASE_SOAK_INPUT: runReleaseSoak, RELEASE_SKIP_PACKAGE_TELEGRAM_E2E_INPUT: skipTelegram, }, }); return { outputPath, result }; } function runFullReleaseTargetSummary(rerunGroup: string, skipTelegram: string) { const step = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"), "Summarize target", ); const workdir = tempDirs.make("full-release-target-summary-"); const summaryPath = resolve(workdir, "github-summary"); const stepEnv = Object.fromEntries(Object.keys(step.env ?? {}).map((name) => [name, ""])); const result = spawnSync("bash", ["-c", step.run ?? ""], { encoding: "utf8", env: { ...stepEnv, GITHUB_STEP_SUMMARY: summaryPath, PATH: process.env.PATH, RELEASE_PROFILE: "beta", RERUN_GROUP: rerunGroup, SKIP_PACKAGE_TELEGRAM_E2E: skipTelegram, TARGET_REF: "main", TARGET_SHA: "a".repeat(40), }, }); const summary = result.status === 0 ? readFileSync(summaryPath, "utf8") : ""; return { result, summary }; } function shellFunctionSource(source: string, functionName: string): string { const startMarker = `${functionName}() {`; const endMarker = "\n}\n"; const start = source.indexOf(startMarker); if (start < 0) { throw new Error(`Expected shell function ${functionName}`); } const end = source.indexOf(endMarker, start); if (end < 0) { throw new Error(`Expected shell function terminator for ${functionName}`); } return source.slice(start, end + endMarker.length); } function workflowMatrixEntry(path: string, jobName: string, suiteId: string): WorkflowMatrixEntry { const entry = workflowJob(path, jobName).strategy?.matrix?.include?.find( (candidate) => candidate.suite_id === suiteId, ); if (!entry) { throw new Error(`Expected workflow matrix entry ${suiteId} in ${jobName}`); } return entry; } function expectTextToIncludeAll(text: string | undefined, snippets: string[]): void { if (text === undefined) { throw new Error("Expected text to be defined before checking snippets"); } for (const snippet of snippets) { expect(text).toContain(snippet); } } function runFullReleaseChildDispatch( child: (typeof FULL_RELEASE_CHILD_DISPATCHES)[number], overrides: Record = {}, ) { const step = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, child.jobName), child.stepName, ); const script = step.run; if (!script) { throw new Error(`Expected full release child dispatch script for ${child.jobName}`); } const workdir = tempDirs.make("full-release-child-dispatch-"); const ghPath = resolve(workdir, "gh"); const sleepPath = resolve(workdir, "sleep"); const callsPath = resolve(workdir, "gh-calls.jsonl"); const statusPath = resolve(workdir, "status-polls"); writeFileSync(callsPath, ""); writeFileSync( ghPath, `#!${process.execPath} const fs = require("node:fs"); const args = process.argv.slice(2); const env = process.env; fs.appendFileSync(env.MOCK_GH_CALLS, JSON.stringify({ args, childWorkflowRef: env.CHILD_WORKFLOW_REF, dispatchRunName: env.DISPATCH_RUN_NAME, }) + "\\n"); const jobs = JSON.parse(env.MOCK_GH_JOBS); const conclusion = env.MOCK_GH_CONCLUSION; const url = "https://github.com/openclaw/openclaw/actions/runs/101"; function nextStatus() { const statuses = JSON.parse(env.MOCK_GH_STATUSES); let index = 0; try { index = Number(fs.readFileSync(env.MOCK_GH_STATUS_POLLS, "utf8")); } catch {} fs.writeFileSync(env.MOCK_GH_STATUS_POLLS, String(index + 1)); return statuses[Math.min(index, statuses.length - 1)]; } if (args[0] === "workflow" && args[1] === "run") { if (env.MOCK_GH_DISPATCH_ERROR) { console.error(env.MOCK_GH_DISPATCH_ERROR); process.exit(1); } console.log(env.MOCK_GH_DISPATCH_OUTPUT); } else if (args[0] === "api" && args.some((value) => value.includes("/commits/"))) { console.log(env.MOCK_GH_CURRENT_SHA); } else if (args[0] === "api" && args.some((value) => value.includes("/actions/workflows/") && value.endsWith("/runs"))) { console.log(env.MOCK_GH_MATCHES); } else if (args[0] === "api" && args.some((value) => value.includes("/actions/workflows/"))) { console.log(env.MOCK_GH_WORKFLOW_ID); } else if (args[0] === "api" && args.some((value) => value.includes("/jobs?"))) { if (env.MOCK_GH_JOBS_ERROR) { console.error(env.MOCK_GH_JOBS_ERROR); process.exit(1); } jobs.forEach((job) => console.log(JSON.stringify(job))); } else if (args[0] === "api" && args.some((value) => value.includes("/actions/runs/"))) { if (env.MOCK_GH_STATUS_ERROR && fs.existsSync(env.MOCK_GH_STATUS_POLLS)) { console.error(env.MOCK_GH_STATUS_ERROR); process.exit(1); } console.log(JSON.stringify({ conclusion, display_title: env.MOCK_GH_RUN_TITLE, event: env.MOCK_GH_RUN_EVENT, head_branch: env.MOCK_GH_RUN_HEAD_BRANCH, head_sha: env.MOCK_GH_CHILD_SHA, html_url: url, id: Number(env.MOCK_GH_RUN_ID), path: env.MOCK_GH_RUN_PATH, status: nextStatus(), workflow_id: Number(env.MOCK_GH_RUN_WORKFLOW_ID), })); } else if (args[0] === "run" && args[1] === "view") { const field = args[args.indexOf("--json") + 1]; if (field === "status" && env.MOCK_GH_STATUS_ERROR) { console.error(env.MOCK_GH_STATUS_ERROR); process.exit(1); } if (field === "jobs") { if (env.MOCK_GH_JOBS_ERROR) { console.error(env.MOCK_GH_JOBS_ERROR); process.exit(1); } const query = args[args.indexOf("--jq") + 1]; if (query.startsWith("[.jobs")) { console.log(JSON.stringify(jobs.filter((job) => job.status === "completed" && job.conclusion !== "success" && job.conclusion !== "skipped"))); } else { jobs.forEach((job) => console.log(JSON.stringify(job))); } } else { console.log({ conclusion, headSha: env.MOCK_GH_CHILD_SHA, status: field === "status" ? nextStatus() : undefined, url, }[field]); } } else if (args[0] !== "run" || args[1] !== "cancel") { console.error("Unexpected mock gh invocation: " + JSON.stringify(args)); process.exit(2); } `, ); chmodSync(ghPath, 0o755); writeFileSync( sleepPath, `#!/bin/sh if [ -n "\${MOCK_SLEEP_SIGNAL:-}" ] && [ ! -e "\${MOCK_SLEEP_SIGNAL_SENT}" ]; then : > "\${MOCK_SLEEP_SIGNAL_SENT}" kill -"\${MOCK_SLEEP_SIGNAL}" "$PPID" fi exit 0 `, ); chmodSync(sleepPath, 0o755); const parentSha = "a".repeat(40); const defaultJobs = [ { conclusion: "success", html_url: "https://github.com/openclaw/openclaw/actions/runs/101/job/201", name: "Verify release checks", status: "completed", url: "https://github.com/openclaw/openclaw/actions/runs/101/job/201", }, ]; const stepValues: Record = { ALLOW_UNRELEASED_CHANGELOG: "false", CANDIDATE_ARTIFACT_JSON: "", CHILD_WORKFLOW_KIND: child.kind, CHILD_WORKFLOW_REF: "main", CODEX_PLUGIN_SPEC: "", CROSS_OS_SUITE_FILTER: "", FAIL_FAST: "false", GH_TOKEN: "fixture-token", LIVE_SUITE_FILTER: "", MODE: "both", PACKAGE_ACCEPTANCE_PACKAGE_SPEC: "", PACKAGE_SPEC: "openclaw@beta", PARENT_WORKFLOW_SHA: parentSha, PROVIDER: "openai", PROVIDER_MODE: "mock-openai", RELEASE_PACKAGE_SPEC: "", RELEASE_PROFILE: "stable", RERUN_GROUP: "all", RUN_RELEASE_SOAK: "false", SCENARIO: "", SKIP_PACKAGE_TELEGRAM_E2E: "false", TARGET_CONTEXT_REF: "", TARGET_REF: "main", TARGET_SHA: "b".repeat(40), }; const stepEnv = Object.fromEntries( Object.keys(step.env ?? {}).map((name) => { const value = stepValues[name]; if (value === undefined) { throw new Error(`Missing child dispatch fixture value for ${child.jobName}.${name}`); } return [name, value]; }), ); const result = spawnSync("bash", ["-c", script], { cwd: workdir, encoding: "utf8", env: { ...stepEnv, GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN: readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW).env ?.GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ?? "HTTP 5[0-9][0-9]", GITHUB_OUTPUT: resolve(workdir, "github-output"), GITHUB_REPOSITORY: "openclaw/openclaw", GITHUB_RUN_ATTEMPT: "2", GITHUB_RUN_ID: "77", GITHUB_STEP_SUMMARY: resolve(workdir, "github-summary"), MOCK_GH_CALLS: callsPath, MOCK_GH_CHILD_SHA: parentSha, MOCK_GH_CONCLUSION: "success", MOCK_GH_CURRENT_SHA: parentSha, MOCK_GH_DISPATCH_OUTPUT: "Created workflow_dispatch event.", MOCK_GH_JOBS: JSON.stringify(defaultJobs), MOCK_GH_MATCHES: "[101]", MOCK_GH_RUN_EVENT: "workflow_dispatch", MOCK_GH_RUN_HEAD_BRANCH: overrides.MOCK_GH_RUN_HEAD_BRANCH ?? overrides.CHILD_WORKFLOW_REF ?? stepEnv.CHILD_WORKFLOW_REF, MOCK_GH_RUN_ID: "101", MOCK_GH_RUN_PATH: `.github/workflows/${child.workflow}`, MOCK_GH_RUN_TITLE: `${child.runName} full-release-validation-77-2${child.nonceSuffix}`, MOCK_GH_RUN_WORKFLOW_ID: "789", MOCK_GH_STATUSES: '["completed"]', MOCK_GH_STATUS_POLLS: statusPath, MOCK_GH_WORKFLOW_ID: "789", MOCK_SLEEP_SIGNAL_SENT: resolve(workdir, "sleep-signal-sent"), PATH: `${workdir}:${process.env.PATH}`, ...overrides, }, timeout: 10_000, }); const calls = readFileSync(callsPath, "utf8") .split("\n") .filter(Boolean) .map( (line) => JSON.parse(line) as { args: string[]; childWorkflowRef: string; dispatchRunName?: string; }, ); return { calls, result }; } function runPackageAcceptanceSummary(params: { advisory?: boolean; dockerArtifactResult?: string; dockerRegistryResult?: string; telegramAdvisory?: boolean; telegramEnabled: boolean; telegramResult: string; }) { const summary = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "summary"); const script = workflowStep(summary, "Verify package acceptance results").run; if (!script) { throw new Error("Expected package acceptance summary script"); } return spawnSync("bash", ["-c", script], { encoding: "utf8", env: { ADVISORY: String(params.advisory ?? false), DOCKER_ARTIFACT_RESULT: params.dockerArtifactResult ?? "success", DOCKER_REGISTRY_RESULT: params.dockerRegistryResult ?? "skipped", PACKAGE_INTEGRITY_RESULT: "success", PACKAGE_TELEGRAM_RESULT: params.telegramResult, PATH: process.env.PATH, RESOLVE_RESULT: "success", TELEGRAM_ADVISORY: String(params.telegramAdvisory ?? false), TELEGRAM_ENABLED: String(params.telegramEnabled), }, }); } function runNpmTelegramInputValidation(overrides: Record) { const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"); const script = workflowStep(job, "Validate inputs and secrets").run; if (!script) { throw new Error("Expected npm Telegram input validation script"); } return spawnSync("bash", ["-c", script], { encoding: "utf8", env: { OPENCLAW_QA_CONVEX_SECRET_CI: "test-secret", OPENCLAW_QA_CONVEX_SITE_URL: "https://example.invalid", PACKAGE_ARTIFACT_DIGEST: "", PACKAGE_ARTIFACT_ID: "", PACKAGE_ARTIFACT_NAME: "", PACKAGE_ARTIFACT_RUN_ATTEMPT: "", PACKAGE_ARTIFACT_RUN_ID: "", PACKAGE_FILE_NAME: "", PACKAGE_SHA256: "", PACKAGE_SOURCE_SHA: "", PACKAGE_SPEC: "openclaw@beta", PACKAGE_VERSION: "", PATH: process.env.PATH, PROVIDER_MODE: "mock-openai", ...overrides, }, }); } function runNpmTelegramArtifactValidation(params: { currentRunId: string; producerRunId: string; producerStatus: "completed" | "in_progress" | "pending" | "queued" | "requested" | "waiting"; producerConclusion: "success" | null; }) { const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"); const script = workflowStep(job, "Validate package artifact identity").run; if (!script) { throw new Error("Expected npm Telegram artifact identity script"); } const binDir = tempDirs.make("npm-telegram-artifact-gh-"); const ghPath = `${binDir}/gh`; writeFileSync( ghPath, `#!/bin/sh case "$*" in *actions/artifacts*) printf '%s\\n' "$MOCK_ARTIFACT_JSON" ;; *actions/runs*) printf '%s\\n' "$MOCK_ATTEMPT_JSON" ;; *) exit 2 ;; esac `, ); chmodSync(ghPath, 0o755); const attempt = "2"; const artifactId = "987"; const artifactName = `package-under-test-${params.producerRunId}-${attempt}`; const digest = "a".repeat(64); return spawnSync("bash", ["-c", script], { encoding: "utf8", env: { ARTIFACT_DIGEST: digest, ARTIFACT_ID: artifactId, ARTIFACT_NAME: artifactName, ARTIFACT_RUN_ATTEMPT: attempt, ARTIFACT_RUN_ID: params.producerRunId, GITHUB_REPOSITORY: "openclaw/openclaw", GITHUB_RUN_ID: params.currentRunId, MOCK_ARTIFACT_JSON: JSON.stringify({ created_at: "2026-07-15T08:49:20Z", digest: `sha256:${digest}`, expired: false, id: Number(artifactId), name: artifactName, workflow_run: { id: Number(params.producerRunId) }, }), MOCK_ATTEMPT_JSON: JSON.stringify({ conclusion: params.producerConclusion, id: Number(params.producerRunId), run_attempt: Number(attempt), run_started_at: "2026-07-15T08:39:00Z", status: params.producerStatus, updated_at: "2026-07-15T08:49:30Z", }), PATH: `${binDir}:${process.env.PATH}`, }, }); } function runReleasePublishInputValidation(overrides: Record) { const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target"); const script = workflowStep(job, "Validate inputs").run; if (!script) { throw new Error("Expected release publish input validation script"); } return spawnSync("bash", ["-c", script], { encoding: "utf8", env: { FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "1", FULL_RELEASE_VALIDATION_RUN_ID: "222", OPENCLAW_NPM_RESUME_RUN_ID: "", PATH: process.env.PATH, PLUGINS: "", PLUGIN_PUBLISH_SCOPE: "all-publishable", PREFLIGHT_RUN_ID: "111", PUBLISH_DOCKER_ONLY: "false", PUBLISH_OPENCLAW_NPM: "true", RELEASE_NPM_DIST_TAG: "beta", RELEASE_PROFILE: "beta", RELEASE_TAG: "v2026.7.1-beta.3", WINDOWS_NODE_INSTALLER_DIGESTS: "", WINDOWS_NODE_TAG: "", WORKFLOW_REF: "refs/heads/main", ...overrides, }, }); } function runOpenClawNpmTrustedRefGuard(overrides: Record) { const job = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "validate_publish_request"); const script = workflowStep(job, "Require trusted workflow ref for publish").run; if (!script) { throw new Error("Expected OpenClaw npm trusted ref guard"); } const binDir = tempDirs.make("openclaw-npm-trusted-ref-"); const gitPath = `${binDir}/git`; const timeoutPath = `${binDir}/timeout`; writeFileSync( gitPath, `#!/bin/sh\nif [ "$1" = "fetch" ]; then exit 0; fi\nif [ "$1" = "merge-base" ]; then [ "\${MOCK_WORKFLOW_ANCESTOR}" = "true" ]; exit $?; fi\nexit 2\n`, ); chmodSync(gitPath, 0o755); writeFileSync( timeoutPath, `#!/bin/sh\n[ "$1" = "--signal=TERM" ] && [ "$2" = "--kill-after=10s" ] && [ "$3" = "120s" ] || exit 2\nshift 3\nexec "$@"\n`, ); chmodSync(timeoutPath, 0o755); return spawnSync("bash", ["-c", script], { encoding: "utf8", env: { MOCK_WORKFLOW_ANCESTOR: "true", PATH: `${binDir}:${process.env.PATH}`, RELEASE_NPM_DIST_TAG: "beta", RELEASE_TAG: "v2026.7.2-beta.1", WORKFLOW_REF: "refs/heads/release/2026.7.2", WORKFLOW_SHA: "a".repeat(40), ...overrides, }, }); } type ReleaseCheckArtifact = { expired: boolean; id: number; name: string; workflow_run: { id: number }; }; type ReleaseCheckArtifactPair = { job: string; payloadBase: string; statusBase: string; variant?: string; }; type ResolvedReleaseCheckArtifact = { job: string; payload_id: number; payload_name: string; producer_attempt: number; run_id: string; status_id: number; status_name: string; target_sha: string; variant: string; }; function releaseCheckArtifact(params: { expired?: boolean; id: number; name: string; runId?: string; }): ReleaseCheckArtifact { return { expired: params.expired ?? false, id: params.id, name: params.name, workflow_run: { id: Number(params.runId ?? "123456") }, }; } function runReleaseCheckArtifactResolve(params: { artifacts: ReleaseCheckArtifact[]; consumerAttempt: string; pairs: ReleaseCheckArtifactPair[]; runId?: string; targetSha?: string; }) { const workdir = tempDirs.make("release-check-artifact-resolver-"); const binDir = resolve(workdir, "bin"); mkdirSync(binDir, { recursive: true }); const ghPath = resolve(binDir, "gh"); writeFileSync(ghPath, "#!/bin/sh\nprintf '%s\\n' \"$MOCK_ARTIFACT_RESPONSE\"\n"); chmodSync(ghPath, 0o755); const runId = params.runId ?? "123456"; const targetSha = params.targetSha ?? "a".repeat(40); const selectionFile = resolve(workdir, "selection.json"); const githubOutput = resolve(workdir, "github-output"); const args = [ resolve(REPO_ROOT, RELEASE_CHECK_ARTIFACT_RESOLVER), "resolve", "--repository", "openclaw/openclaw", "--run-id", runId, "--consumer-attempt", params.consumerAttempt, "--target-sha", targetSha, ]; for (const pair of params.pairs) { args.push( "--pair", [pair.job, pair.variant ?? "", pair.statusBase, pair.payloadBase].join("|"), ); } args.push("--selection-file", selectionFile, "--github-output", githubOutput); const result = spawnSync("bash", args, { cwd: workdir, encoding: "utf8", env: { MOCK_ARTIFACT_RESPONSE: JSON.stringify({ artifacts: params.artifacts }), PATH: `${binDir}:${process.env.PATH}`, }, }); const selection = result.status === 0 ? (JSON.parse(readFileSync(selectionFile, "utf8")) as ResolvedReleaseCheckArtifact[]) : []; return { result, selection, selectionFile, targetSha, workdir }; } function releaseCheckStatusText( selection: ResolvedReleaseCheckArtifact, status: "cancelled" | "failure" | "skipped" | "success" = "success", ): string { return [ `run_id=${selection.run_id}`, `run_attempt=${selection.producer_attempt}`, `target_sha=${selection.target_sha}`, `job=${selection.job}`, `variant=${selection.variant}`, `status=${status}`, "job_status=success", "step_outcomes=success", "", ].join("\n"); } function runReleaseCheckArtifactValidation(params: { selection: ResolvedReleaseCheckArtifact[]; statusText?: (selection: ResolvedReleaseCheckArtifact) => string; }) { const workdir = tempDirs.make("release-check-artifact-validation-"); const selectionFile = resolve(workdir, "selection.json"); const statusDir = resolve(workdir, "statuses"); const validatedFile = resolve(workdir, "validated.json"); mkdirSync(statusDir, { recursive: true }); writeFileSync(selectionFile, JSON.stringify(params.selection)); for (const selection of params.selection) { const variant = selection.variant ? `-${selection.variant}` : ""; writeFileSync( resolve( statusDir, `${selection.job}${variant}-${selection.run_id}-${selection.producer_attempt}.env`, ), params.statusText?.(selection) ?? releaseCheckStatusText(selection), ); } const result = spawnSync( "bash", [ resolve(REPO_ROOT, RELEASE_CHECK_ARTIFACT_RESOLVER), "validate", "--selection-file", selectionFile, "--status-dir", statusDir, "--validated-file", validatedFile, ], { cwd: workdir, encoding: "utf8", env: { PATH: process.env.PATH }, }, ); const validated = result.status === 0 ? (JSON.parse(readFileSync(validatedFile, "utf8")) as Array< ResolvedReleaseCheckArtifact & { status: string } >) : []; return { result, validated }; } function runReleaseChecksSummary(params: { currentAttempt: string; currentResult: "cancelled" | "failure" | "skipped" | "success"; discordResult?: "failure" | "skipped" | "success"; resolveResult?: "failure" | "success"; telegramSelected?: boolean; validatedStatuses?: Array<{ job: string; status: string; variant: string }>; workflowRef?: string; }) { const summary = workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"); const script = workflowStep(summary, "Verify release check results").run; if (!script) { throw new Error("Expected release checks summary script"); } const runId = "123456"; const targetSha = "a".repeat(40); const workdir = tempDirs.make("openclaw-release-check-status-"); const selectionDir = resolve(workdir, ".artifacts/release-check-selection"); mkdirSync(selectionDir, { recursive: true }); writeFileSync( resolve(selectionDir, "advisory-evidence-validated.json"), JSON.stringify(params.validatedStatuses ?? []), ); return spawnSync("bash", ["-c", script], { cwd: workdir, encoding: "utf8", env: { CROSS_OS_RELEASE_CHECKS_RESULT: "success", DOCKER_E2E_RELEASE_CHECKS_RESULT: "success", GITHUB_RUN_ATTEMPT: params.currentAttempt, GITHUB_RUN_ID: runId, INSTALL_SMOKE_RELEASE_CHECKS_RESULT: "success", LIVE_REPO_E2E_RELEASE_CHECKS_RESULT: "success", MATURITY_SCORECARD_RELEASE_CHECKS_RESULT: "skipped", PACKAGE_ACCEPTANCE_RELEASE_CHECKS_RESULT: "success", PATH: process.env.PATH, PREPARE_RELEASE_PACKAGE_RESULT: "success", QA_LAB_PARITY_LANE_RELEASE_CHECKS_RESULT: "skipped", QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT: "skipped", QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT: "skipped", QA_LIVE_BUZZ_RELEASE_CHECKS_RESULT: "skipped", QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT: params.discordResult ?? "skipped", QA_LIVE_RELEASE_CHECKS_RESULT: "skipped", QA_LIVE_SLACK_RELEASE_CHECKS_RESULT: "skipped", QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT: params.currentResult, QA_LIVE_TELEGRAM_SELECTED: String(params.telegramSelected ?? true), QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT: "skipped", RELEASE_CHECK_RUN_ATTEMPT: params.currentAttempt, RELEASE_CHECK_RUN_ID: runId, RELEASE_CHECK_TARGET_SHA: targetSha, RESOLVE_ADVISORY_EVIDENCE_OUTCOME: "success", RESOLVE_TARGET_RESULT: params.resolveResult ?? "success", RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT: "skipped", VALIDATE_ADVISORY_STATUSES_OUTCOME: "success", WORKFLOW_REF: params.workflowRef ?? "refs/heads/release/2026.7.1", }, }); } describe("package acceptance workflow", () => { it("requires selected plugin names or complete immutable evidence for broad publication", () => { const selected = runReleasePublishInputValidation({ FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "", FULL_RELEASE_VALIDATION_RUN_ID: "", PLUGINS: "@openclaw/meta", PLUGIN_PUBLISH_SCOPE: "selected", PREFLIGHT_RUN_ID: "", PUBLISH_OPENCLAW_NPM: "false", }); expect(selected.status, selected.stderr).toBe(0); const emptySelected = runReleasePublishInputValidation({ FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "", FULL_RELEASE_VALIDATION_RUN_ID: "", PLUGINS: " ", PLUGIN_PUBLISH_SCOPE: "selected", PREFLIGHT_RUN_ID: "", PUBLISH_OPENCLAW_NPM: "false", }); expect(emptySelected.status).toBe(1); expect(emptySelected.stderr).toContain("plugin_publish_scope=selected requires plugins"); const broadWithoutEvidence = runReleasePublishInputValidation({ FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "", FULL_RELEASE_VALIDATION_RUN_ID: "", PREFLIGHT_RUN_ID: "", PUBLISH_OPENCLAW_NPM: "false", }); expect(broadWithoutEvidence.status).toBe(1); expect(broadWithoutEvidence.stderr).toContain("require preflight_run_id"); const partialEvidence = runReleasePublishInputValidation({ FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "", FULL_RELEASE_VALIDATION_RUN_ID: "", PLUGINS: "@openclaw/meta", PLUGIN_PUBLISH_SCOPE: "selected", PUBLISH_OPENCLAW_NPM: "false", }); expect(partialEvidence.status).toBe(1); expect(partialEvidence.stderr).toContain("require full_release_validation_run_id"); expect(runReleasePublishInputValidation({ PUBLISH_OPENCLAW_NPM: "false" }).status).toBe(0); const invalidResumeRun = runReleasePublishInputValidation({ OPENCLAW_NPM_RESUME_RUN_ID: "not-a-run-id", }); expect(invalidResumeRun.status).toBe(1); expect(invalidResumeRun.stderr).toContain( "openclaw_npm_resume_run_id must be a positive GitHub Actions run id", ); }); it("accepts only main-reachable protected SHA-pinned release publish tags", () => { const workflowSha = "a".repeat(40); const binDir = tempDirs.make("release-publish-gh-"); const ghPath = `${binDir}/gh`; writeFileSync(ghPath, `#!/bin/sh\nprintf '%s\\n' "\${MOCK_MERGE_BASE_SHA}"\n`); chmodSync(ghPath, 0o755); const pinnedEnv = { GITHUB_REPOSITORY: "openclaw/openclaw", PATH: `${binDir}:${process.env.PATH}`, WORKFLOW_REF: `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`, WORKFLOW_SHA: workflowSha, }; const valid = runReleasePublishInputValidation({ ...pinnedEnv, MOCK_MERGE_BASE_SHA: workflowSha, }); expect(valid.status, valid.stderr).toBe(0); const mismatchedName = runReleasePublishInputValidation({ ...pinnedEnv, WORKFLOW_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`, }); expect(mismatchedName.status).toBe(1); expect(mismatchedName.stderr).toContain( "SHA-pinned release publish tag does not match workflow SHA", ); const unreachable = runReleasePublishInputValidation({ ...pinnedEnv, MOCK_MERGE_BASE_SHA: "c".repeat(40), }); expect(unreachable.status).toBe(1); expect(unreachable.stderr).toContain( "SHA-pinned release publish tag revision is not reachable from current main", ); }); it("allows protected SHA-pinned tooling tags through the core npm publish guard", () => { const workflowSha = "a".repeat(40); const protectedRef = `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`; const valid = runOpenClawNpmTrustedRefGuard({ WORKFLOW_REF: protectedRef, WORKFLOW_SHA: workflowSha, }); expect(valid.status, valid.stderr).toBe(0); const mismatchedName = runOpenClawNpmTrustedRefGuard({ WORKFLOW_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`, WORKFLOW_SHA: workflowSha, }); expect(mismatchedName.status).toBe(1); expect(mismatchedName.stderr).toContain( "SHA-pinned release-publish tag does not match the OpenClaw npm workflow SHA", ); const unreachable = runOpenClawNpmTrustedRefGuard({ MOCK_WORKFLOW_ANCESTOR: "false", WORKFLOW_REF: protectedRef, WORKFLOW_SHA: workflowSha, }); expect(unreachable.status).toBe(1); expect(unreachable.stderr).toContain( "SHA-pinned OpenClaw npm workflow revision is not reachable from current main", ); }); it("allows protected SHA-pinned tooling tags to consume token-bootstrap evidence", () => { const publishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm"); const evidenceStep = workflowStep(publishJob, "Consume immutable npm publication evidence"); expect(evidenceStep.run).toContain("^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$"); expect(evidenceStep.run).toContain( '[[ "$WORKFLOW_REF" == "refs/heads/main" || "$sha_pinned_release_publish" == "true" ]]', ); expect(evidenceStep.run).toContain('git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main'); }); it("retries child environment approval when deployment propagation lags", () => { const publishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"); const orchestration = workflowStep(publishJob, "Dispatch publish workflows").run; if (!orchestration) { throw new Error("Expected release publish orchestration script"); } const waitForRun = shellFunctionSource(orchestration, "wait_for_run"); const stateAssignment = waitForRun.indexOf('last_state="$state"'); const approvalRetry = waitForRun.indexOf( 'approve_pending_deployments "${workflow}" "${run_id}" "${expected_sha}" ||', ); expect(stateAssignment).toBeGreaterThan(-1); expect(approvalRetry).toBeGreaterThan(stateAssignment); expect(waitForRun).toContain("propagation lag cannot strand an approved release"); }); it("resolves broad release evidence and exact-binds every publish child", () => { const resolveJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target"); const publishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"); const publishOrchestration = workflowStep(publishJob, "Dispatch publish workflows"); for (const stepName of [ "Download OpenClaw npm preflight manifest", "Resolve full release validation run", "Download full release validation manifest", "Download trusted release validation tooling", "Validate OpenClaw npm preflight manifest", "Validate full release validation manifest", ]) { expect(workflowStep(resolveJob, stepName).if).toContain( "inputs.plugin_publish_scope == 'all-publishable'", ); } for (const stepName of [ "Write Android release approval", "Attest Android release approval", "Upload Android release approval", ]) { expect(workflowStep(publishJob, stepName).if).toContain("inputs.publish_openclaw_npm"); } expect(publishOrchestration.env?.PARENT_WORKFLOW_SHA).toBe("${{ github.sha }}"); expect(publishOrchestration.env?.CHILD_WORKFLOW_REF).toBe("${{ github.ref_name }}"); expect(readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8")).toContain( "otherwise approve and monitor the detached runs separately", ); expectTextToIncludeAll(publishOrchestration.run, [ 'gh api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}"', 'if [[ "$resolved_workflow_sha" != "$expected_sha" ]]', 'verify_child_run_sha "$workflow" "$run_id" "$expected_sha" || return 1', 'approve_pending_deployments "${workflow}" "${run_id}" "${expected_sha}"', 'wait_for_run windows-node-release.yml "${windows_node_run_id}" "${PARENT_WORKFLOW_SHA}"', 'dispatch_workflow_at_ref "${RELEASE_TAG}" "${TARGET_SHA}" android-release.yml', 'wait_for_run android-release.yml "${android_release_run_id}" "${TARGET_SHA}"', 'wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}" "${PARENT_WORKFLOW_SHA}"', 'wait_for_run_background openclaw-npm-release.yml "${openclaw_npm_run_id}" "${PARENT_WORKFLOW_SHA}"', "plugin-clawhub-release.yml: detached; approval and publish not awaited", "plugin-clawhub-new.yml: detached; approvals and bootstrap not awaited", ]); }); it("compares dependency evidence zip contents independently of archive timestamps", () => { const orchestration = workflowStep( workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"), "Dispatch publish workflows", ).run; if (!orchestration) { throw new Error("Expected release publish orchestration script"); } const tempDir = tempDirs.make("release-evidence-zip-"); const sourceDir = `${tempDir}/source`; const existingDir = `${tempDir}/existing`; const sourceZip = `${tempDir}/source.zip`; const existingZip = `${tempDir}/existing.zip`; const symlinkZip = `${tempDir}/symlink.zip`; const corruptZip = `${tempDir}/corrupt.zip`; for (const dir of [sourceDir, existingDir]) { mkdirSync(`${dir}/dependency-evidence`, { recursive: true }); writeFileSync(`${dir}/dependency-evidence/proof.json`, '{"ok":true}\n'); } execFileSync("touch", ["-t", "198001010000", `${sourceDir}/dependency-evidence/proof.json`]); execFileSync("touch", ["-t", "202001010000", `${existingDir}/dependency-evidence/proof.json`]); execFileSync("zip", ["-X", "-q", sourceZip, "dependency-evidence/proof.json"], { cwd: sourceDir, }); execFileSync("zip", ["-X", "-q", existingZip, "dependency-evidence/proof.json"], { cwd: existingDir, }); symlinkSync("../../outside", `${existingDir}/dependency-evidence/link`); execFileSync("zip", ["-X", "-y", "-q", symlinkZip, "dependency-evidence/link"], { cwd: existingDir, }); const sourceArchive = readFileSync(sourceZip); writeFileSync(corruptZip, sourceArchive.subarray(0, -10)); const compare = (left: string, right: string) => spawnSync("python3", ["scripts/compare-release-evidence-zip.py", left, right], { encoding: "utf8", }); const result = compare(sourceZip, existingZip); const symlinkResult = compare(symlinkZip, symlinkZip); const corruptResult = compare(corruptZip, corruptZip); expect(result.status, result.stderr).toBe(0); expect(symlinkResult.status).toBe(1); expect(symlinkResult.stderr).toContain("unsupported dependency evidence archive entry"); expect(corruptResult.status).toBe(1); expect(corruptResult.stderr).toContain("dependency evidence ZIP comparison failed"); expect(orchestration).toContain("find dependency-evidence -type f -exec touch -t 198001010000"); expect(orchestration).toContain( 'attach_or_verify_release_asset "${asset_path}" "${asset_name}" zip-tree', ); expect(orchestration).toContain( '"${GITHUB_WORKSPACE}/.release-harness/scripts/compare-release-evidence-zip.py"', ); }); it("verifies immutable postpublish evidence before stable closeout reads it", () => { const workflow = readFileSync(STABLE_MAIN_CLOSEOUT_WORKFLOW, "utf8"); const evidenceStep = workflowStep( workflowJob(STABLE_MAIN_CLOSEOUT_WORKFLOW, "verify"), "Verify release workflow evidence", ); const attachStep = workflowStep( workflowJob(STABLE_MAIN_CLOSEOUT_WORKFLOW, "verify"), "Attach immutable closeout evidence", ); const checksumIndex = workflow.indexOf( 'sha256sum --strict --status -c "$evidence_checksum_asset"', ); const evidenceReadIndex = workflow.indexOf('evidence_release_tag="$(jq -r'); const releaseVersionGateIndex = workflow.indexOf( 'if [[ "$main_version" != "$release_package_version" &&', ); const evidenceDownloadIndex = workflow.indexOf( 'gh_with_retry release download "$evidence_source_tag"', ); const partialRepairIndex = workflow.indexOf('if [[ -f "$closeout_json_path" ]]; then'); const existingCloseoutEvidenceMatchIndex = workflow.indexOf( 'if [[ -n "$existing_closeout_full_release_validation_run_id" &&', ); const rollbackDrillGateIndex = workflow.indexOf( 'if [[ -z "$ROLLBACK_DRILL_ID" || -z "$ROLLBACK_DRILL_DATE" ]]; then', ); const rollbackDrillPushSkipIndex = workflow.indexOf( "Stable closeout skipped: rollback drill repository variables are missing", ); const evidenceScriptSyntax = spawnSync("bash", ["-n"], { encoding: "utf8", input: evidenceStep.run, }); const attachScriptSyntax = spawnSync("bash", ["-n"], { encoding: "utf8", input: attachStep.run, }); expect(evidenceScriptSyntax.status, evidenceScriptSyntax.stderr).toBe(0); expect(attachScriptSyntax.status, attachScriptSyntax.stderr).toBe(0); expect(workflow).toContain('evidence_checksum_asset="${evidence_asset}.sha256"'); expect(workflow).toContain('--pattern "$evidence_checksum_asset"'); expect(workflow).toContain('fallback_package_version="${BASH_REMATCH[1]}"'); expect(workflow).toContain('tag_package_content="$RUNNER_TEMP/tag-package-content.b64"'); expect(workflow).toContain( 'gh_with_retry api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag"', ); expect(workflow).toContain("for attempt in 1 2 3; do"); expect(workflow).toContain("sleep $((attempt * 5))"); expect(workflow).toContain( "Stable closeout could not read package.json for $tag from GitHub API.", ); expect(workflow).toContain( "Stable closeout package.json content for $tag was not valid base64.", ); expect(workflow).toContain('tag_package_version="$(jq -r'); expect(workflow).toContain('evidence_source_tag="v$fallback_package_version"'); expect(workflow).toContain('gh_with_retry release download "$evidence_source_tag"'); expect(workflow).toContain("Checkout fallback evidence tag"); expect(workflow).toContain("Bind fallback correction to the published package source"); expect(workflow).toContain( "Fallback correction ${{ needs.resolve.outputs.tag }} must point to the same source commit", ); expect(workflow).toContain("main_ref: ${{ steps.inputs.outputs.main_ref }}"); expect(workflow).toContain("TRIGGER_SHA: ${{ github.sha }}"); expect(workflow).toContain('main_ref="$TRIGGER_SHA"'); expect(workflow).toContain("ref: ${{ needs.resolve.outputs.main_ref }}"); expect(workflow).toContain( "Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence.", ); expect(workflow).toContain("Stable closeout is required for $tag"); expect(workflow).toContain('closeout_checksum_asset="${closeout_asset}.sha256"'); expect(workflow).toContain('expected_closeout_digest="$(awk'); expect(workflow).toContain('actual_closeout_digest="$(sha256sum "$closeout_json_path"'); expect(workflow).toContain( "Stable closeout manifest for $tag is incomplete; refusing to repair it.", ); expect(workflow).toContain( 'if [[ -f "$closeout_checksum_path" && ! -f "$closeout_json_path" ]]; then', ); expect(workflow).toContain( "Stable closeout evidence for $tag has an invalid checksum; refusing to repair it.", ); expect(workflow).toContain("repair_partial_closeout=false"); expect(workflow).toContain( "Stable closeout manifest for $tag does not match immutable postpublish evidence; refusing to accept it.", ); expect(workflow).toContain("Stable closeout already complete for $tag."); expect(workflow).toContain("allow_failed_publish_recovery:"); expect(workflow).toContain( 'const recoveryRequested = process.env.ALLOW_FAILED_PUBLISH_RECOVERY === "true";', ); expect(workflow).toContain("Failed-publish recovery requires conclusion=failure"); expect(workflow).toContain( '--require-complete-platform-assets "$ALLOW_FAILED_PUBLISH_RECOVERY"', ); expect(workflow).toContain("verify_checksum_manifest OpenClaw-Android-SHA256SUMS.txt"); expect(workflow).toContain("verify_checksum_manifest OpenClawCompanion-SHA256SUMS.txt"); expect(workflow).toContain("actual=\"$(awk 'NF { name=$2;"); expect(workflow).toContain('sub(/^\\*/, "", name)'); expect(workflow).not.toContain('sub(/^\\\\*/, "", name)'); expect(workflow).toContain('sed \'s/\\r$//\' "$manifest" > "$normalized"'); expect(workflow).toContain('sha256sum --strict --check "$normalized"'); expect(workflow).toContain( "Windows Node Release must contain one successful signed-installer promotion job.", ); expect(workflow).toContain('"Verify Authenticode signatures"'); expect(workflow).toContain("EXPECTED_INSTALLER_DIGESTS:"); expect(workflow).toContain('--windows-node-release-run-id "${WINDOWS_NODE_RELEASE_RUN_ID:-}"'); expect(workflow).toContain( '--windows-node-installer-digests "${WINDOWS_NODE_INSTALLER_DIGESTS:-}"', ); expect(workflow).toContain( '--signer-workflow "$GITHUB_REPOSITORY/.github/workflows/android-release.yml"', ); expect(workflow).toContain( "Stable closeout requires repository variables RELEASE_ROLLBACK_DRILL_ID and RELEASE_ROLLBACK_DRILL_DATE, or explicit manual overrides.", ); expect(workflow).toContain( "REPAIR_PARTIAL_CLOSEOUT: ${{ needs.resolve.outputs.repair_partial_closeout }}", ); expect(workflow).toContain('--allow-stale-rollback-drill "$REPAIR_PARTIAL_CLOSEOUT"'); expect(workflow).toContain( 'awk -v asset="openclaw-${release_version}-stable-main-closeout.json"', ); expect(workflow).toContain("attach_or_verify \\"); expect(attachStep.run).toContain('cp -- "$source_path" "$existing_dir/$asset_name"'); expect(attachStep.run).toContain( '"$existing_dir/$asset_name#$asset_name" --repo "$GITHUB_REPOSITORY"', ); expect(attachStep.run).not.toContain('"$source_path#$asset_name"'); expect(workflow).toContain( "full_release_validation_run_attempt: ${{ steps.inputs.outputs.full_release_validation_run_attempt }}", ); expect(workflow).toContain("(.[0].runAttempt == null) or"); expect(workflow).toContain( 'release_manifest_asset="openclaw-${evidence_version}-release-manifest.json"', ); expect(workflow).toContain('sha256sum --strict --status -c "$release_manifest_checksum_asset"'); expect(workflow).toContain( '"$existing_closeout_full_release_validation_run_attempt" != "$full_release_validation_run_attempt"', ); expect(evidenceStep.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe( "${{ needs.resolve.outputs.full_release_validation_run_attempt }}", ); expect(evidenceStep.run).toContain( "actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}", ); expect(evidenceStep.run).toContain( 'String(run.run_attempt ?? "") !== process.env.FULL_RELEASE_VALIDATION_RUN_ATTEMPT', ); expect(evidenceStep.run).toContain( 'manifest_asset="openclaw-${evidence_version}-release-manifest.json"', ); expect(evidenceStep.run).toContain('gh_with_retry release download "$EVIDENCE_TAG"'); expect(evidenceStep.run).toContain(".runId == $run_id"); expect(evidenceStep.run).toContain(".runAttempt == $run_attempt"); expect(workflow).toContain( '--full-release-validation-run-attempt "$FULL_RELEASE_VALIDATION_RUN_ATTEMPT"', ); expect(checksumIndex).toBeGreaterThan(-1); expect(evidenceReadIndex).toBeGreaterThan(checksumIndex); expect(existingCloseoutEvidenceMatchIndex).toBeGreaterThan(evidenceReadIndex); expect(workflow.slice(checksumIndex, existingCloseoutEvidenceMatchIndex)).not.toContain( 'echo "should_closeout=false"', ); expect(releaseVersionGateIndex).toBeGreaterThan(-1); expect(partialRepairIndex).toBeGreaterThan(-1); expect(partialRepairIndex).toBeLessThan(releaseVersionGateIndex); expect(evidenceDownloadIndex).toBeGreaterThan(releaseVersionGateIndex); expect(rollbackDrillGateIndex).toBeGreaterThan(existingCloseoutEvidenceMatchIndex); expect(rollbackDrillPushSkipIndex).toBeGreaterThan(rollbackDrillGateIndex); }); it("keeps pnpm version selection sourced from packageManager", () => { const packageJson = JSON.parse(readFileSync(PACKAGE_JSON, "utf8")) as { packageManager?: string; }; const setupPnpmAction = readFileSync(SETUP_PNPM_STORE_CACHE_ACTION, "utf8"); expect(packageJson.packageManager).toMatch(/^pnpm@\d+\.\d+\.\d+\+sha512\.[a-f0-9]+$/u); expect(setupPnpmAction).toContain("Setup pnpm from packageManager"); expect(setupPnpmAction).toContain("PACKAGE_MANAGER_FILE: ${{ inputs.package-manager-file }}"); expect(setupPnpmAction).toContain('case "$package_manager" in'); expect(setupPnpmAction).toContain('corepack prepare "$package_manager" --activate'); expect(setupPnpmAction).toContain( "if: ${{ inputs.use-actions-cache == 'true' && runner.os != 'Windows' }}", ); expect(setupPnpmAction).toContain( "key: pnpm-store-${{ runner.os }}-${{ runner.arch }}-${{ inputs.node-version }}-${{ hashFiles(inputs.package-manager-file) }}-${{ hashFiles(inputs.lockfile-path) }}", ); expect(setupPnpmAction).not.toContain("pnpm/action-setup"); expect(setupPnpmAction).not.toContain("shasum"); expect(setupPnpmAction).not.toContain("PNPM_VERSION_INPUT"); expect(setupPnpmAction).not.toContain("version: ${{ inputs.pnpm-version }}"); expect(setupPnpmAction).toContain('corepack enable --install-directory "$PNPM_HOME"'); expect(setupPnpmAction).toContain('echo "PNPM_HOME=$PNPM_HOME" >> "$GITHUB_ENV"'); const setupReleaseHarnessAction = readFileSync(SETUP_RELEASE_HARNESS_ACTION, "utf8"); const setupHarnessPackageManagerIndex = setupReleaseHarnessAction.indexOf( "Setup trusted release harness package manager", ); const installHarnessDependenciesIndex = setupReleaseHarnessAction.indexOf( "Install trusted release harness dependencies", ); expect(setupHarnessPackageManagerIndex).toBeGreaterThan(-1); expect(installHarnessDependenciesIndex).toBeGreaterThan(setupHarnessPackageManagerIndex); expect(setupReleaseHarnessAction).toContain( "uses: ./.release-harness/.github/actions/setup-pnpm-store-cache", ); expect(setupReleaseHarnessAction).toContain( "package-manager-file: .release-harness/package.json", ); expect(setupReleaseHarnessAction).toContain("working-directory: .release-harness"); expect(setupReleaseHarnessAction).toContain( "pnpm install --frozen-lockfile --prefer-offline --ignore-scripts", ); const setupNodeAction = readFileSync(".github/actions/setup-node-env/action.yml", "utf8"); expect(setupNodeAction).toContain("Normalize container toolcache"); expect(setupNodeAction).toContain("ln -s /__t /opt/hostedtoolcache"); expect(setupNodeAction).toContain("use-actions-cache: ${{ inputs.use-actions-cache }}"); for (const workflowPath of workflowPaths()) { const workflowText = readFileSync(workflowPath, "utf8"); expect(workflowText, workflowPath).not.toContain("PNPM_VERSION"); expect(workflowText, workflowPath).not.toContain("pnpm-version:"); expect(workflowText, workflowPath).not.toContain("pnpm/action-setup"); } }); it("keeps Crabbox hydration compatible with local Actions replay", () => { const crabboxConfig = parse(readFileSync(CRABBOX_CONFIG, "utf8")) as { actions?: { job?: string }; }; const workflowText = readFileSync(CRABBOX_HYDRATE_WORKFLOW, "utf8"); const hydrate = workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate"); const hydrateWindowsDaemon = workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate-windows-daemon"); const hydrateGithub = workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate-github"); expect(crabboxConfig.actions?.job).toBe("hydrate"); expect(hydrate.if).toBe( "${{ inputs.crabbox_job != 'hydrate-github' && inputs.crabbox_job != 'hydrate-windows-daemon' }}", ); expect(workflowStep(hydrate, "Setup Node.js").uses).toBe(SETUP_NODE_V6); expect(workflowStep(hydrate, "Setup Node.js").with?.["node-version"]).toBe("24"); const hydratePnpm = workflowStep(hydrate, "Setup pnpm and dependencies"); expect(hydratePnpm.if).toBeUndefined(); expect(hydratePnpm.run).toContain('corepack enable --install-directory "$PNPM_HOME"'); expect(hydratePnpm.run).toContain("COREPACK_HOME"); expect(workflowText).not.toContain('PNPM_CONFIG_STORE_DIR: "/var/cache/crabbox/pnpm/store"'); expect(hydratePnpm.run).toContain('preferred_pnpm_store="/var/cache/crabbox/pnpm/store"'); expect(hydratePnpm.run).toContain('mkdir -p "$preferred_pnpm_store" 2>/dev/null'); expect(hydratePnpm.run).toContain('[ -w "$preferred_pnpm_store" ]'); expect(hydratePnpm.run).toContain( 'pnpm_cache_root="${XDG_CACHE_HOME:-$HOME/.cache}/openclaw/pnpm"', ); expect(hydratePnpm.run).toContain('pnpm_install_root="$pnpm_cache_root/install"'); expect(hydratePnpm.run).toContain('export PNPM_CONFIG_STORE_DIR="$pnpm_cache_root/store"'); expect(hydratePnpm.run).toContain( 'export PNPM_CONFIG_MODULES_DIR="$pnpm_install_root/node_modules"', ); expect(hydratePnpm.run).toContain('export PNPM_CONFIG_PACKAGE_IMPORT_METHOD="hardlink"'); expect(hydratePnpm.run).toContain( 'export PNPM_CONFIG_VIRTUAL_STORE_DIR="$pnpm_install_root/virtual-store"', ); expect(hydratePnpm.run).toContain('echo "PNPM_CONFIG_STORE_DIR=$PNPM_CONFIG_STORE_DIR"'); expect(hydratePnpm.run).toContain('echo "PNPM_CONFIG_MODULES_DIR=$PNPM_CONFIG_MODULES_DIR"'); expect(hydratePnpm.run).toContain( 'echo "PNPM_CONFIG_PACKAGE_IMPORT_METHOD=${PNPM_CONFIG_PACKAGE_IMPORT_METHOD:-}"', ); expect(hydratePnpm.run).toContain( 'echo "PNPM_CONFIG_VIRTUAL_STORE_DIR=$PNPM_CONFIG_VIRTUAL_STORE_DIR"', ); expect(hydratePnpm.run).toContain('} >> "$GITHUB_ENV"'); expect(hydratePnpm.run).toContain("prepare_crabbox_pnpm_dirs"); expect(hydratePnpm.run).toContain( 'case "${PNPM_CONFIG_MODULES_DIR:?}" in "$pnpm_install_root"/*)', ); expect(hydratePnpm.run).toContain( 'case "${PNPM_CONFIG_VIRTUAL_STORE_DIR:?}" in "$pnpm_install_root"/*)', ); expect(hydratePnpm.run).toContain('rm -rf -- "$pnpm_install_root"'); expect(hydratePnpm.run).toContain('mkdir -p "$pnpm_install_root" "$PNPM_CONFIG_STORE_DIR"'); expect(hydratePnpm.run).toContain( 'mkdir -p "$PNPM_CONFIG_MODULES_DIR" "$PNPM_CONFIG_VIRTUAL_STORE_DIR"', ); expect(hydratePnpm.run).toContain( '"$(stat -c %d "$PNPM_CONFIG_STORE_DIR")" != "$(stat -c %d "$PNPM_CONFIG_MODULES_DIR")"', ); expect(hydratePnpm.run).toContain( "Fallback pnpm store and modules directories must share a filesystem", ); expect(hydratePnpm.run).toContain( "append_pnpm_option_arg PNPM_CONFIG_PACKAGE_IMPORT_METHOD package-import-method", ); expect(hydratePnpm.run).toContain("Refusing unsafe pnpm directory"); expect(hydratePnpm.run).not.toContain('rm -rf -- "${PNPM_CONFIG_MODULES_DIR:?}"'); expect(hydratePnpm.run).toContain( '[ "$(readlink node_modules)" = "${PNPM_CONFIG_MODULES_DIR:-}" ]', ); expect(hydratePnpm.run).toContain("pnpm_install_artifacts_ready"); expect(hydratePnpm.run).toContain("run_pnpm_install || run_pnpm_install"); expect(hydratePnpm.run).toContain('setsid pnpm "${install_args[@]}"'); expect(hydratePnpm.run).toContain("grep -qE '^Done in .+ using pnpm v'"); expect(hydratePnpm.run).toContain("https://github.com/pnpm/pnpm/issues/12297"); expect(hydratePnpm.run).toContain('kill -TERM -- "-$pnpm_pid"'); expect(hydratePnpm.run).toContain('kill -KILL -- "-$pnpm_pid"'); expect(hydratePnpm.run).toContain('test -s "$PNPM_CONFIG_MODULES_DIR/.modules.yaml"'); expect(hydratePnpm.run).toContain('test -x "$PNPM_CONFIG_MODULES_DIR/.bin/oxfmt"'); expect(hydratePnpm.run).toContain('test -f "$PNPM_CONFIG_MODULES_DIR/typescript/package.json"'); expect(workflowStep(hydrate, "Fetch main ref").run).toContain( "timeout --signal=TERM --kill-after=10s 30s git", ); expect(workflowStep(hydrate, "Fetch main ref").run).toContain( "fetch --no-tags --prune --no-recurse-submodules --depth=50 origin", ); expect(workflowStep(hydrate, "Fetch main ref").run).toContain( '"+refs/heads/main:refs/remotes/origin/main"', ); expect(workflowStep(hydrate, "Prepare Crabbox shell").if).toBeUndefined(); const prepareCrabboxShell = workflowStep(hydrate, "Prepare Crabbox shell").run; expect(prepareCrabboxShell).toContain("link_node_tool()"); expect(prepareCrabboxShell).toContain('readlink -f "$source"'); expect(prepareCrabboxShell).toContain('readlink -f "$target"'); expect(prepareCrabboxShell).toContain("link_node_tool corepack"); const ensureDocker = workflowStep(hydrate, "Ensure Docker is running"); expect(ensureDocker.if).toBeUndefined(); expect(ensureDocker.env).toEqual({ CRABBOX_JOB: "${{ inputs.crabbox_job }}", }); expect(ensureDocker.run).toContain("docker_required=false"); expect(ensureDocker.run).toContain('if [ "${CRABBOX_JOB:-hydrate}" = "hydrate-docker" ]; then'); expect(ensureDocker.run).toContain("other marker names do not"); expect(ensureDocker.run).toContain('if [ "$docker_required" = true ]; then'); expect(ensureDocker.run).toContain( "Docker is unavailable for ${CRABBOX_JOB:-hydrate}; route this workload to a Docker-capable provider", ); expect(ensureDocker.run).toContain( "Docker is unavailable; standard hydration will continue without Docker", ); expect(ensureDocker.run).toContain( 'echo "OPENCLAW_CRABBOX_DOCKER_AVAILABLE=0" >> "$GITHUB_ENV"', ); expect(ensureDocker.run).toContain( 'echo "OPENCLAW_CRABBOX_DOCKER_AVAILABLE=1" >> "$GITHUB_ENV"', ); expect(workflowStep(hydrate, "Ensure SSH is available").if).toBeUndefined(); expect(workflowStep(hydrate, "Hydrate provider env helper").if).toBeUndefined(); const markCrabboxReady = workflowStep(hydrate, "Mark Crabbox ready").run; expect(markCrabboxReady).toContain("COREPACK_HOME"); expect(markCrabboxReady).toContain("OPENCLAW_CRABBOX_DOCKER_AVAILABLE"); expect(markCrabboxReady).toContain("PNPM_CONFIG_PACKAGE_IMPORT_METHOD"); expect(workflowStep(hydrate, "Hydrate provider env helper").env).toBeUndefined(); expect(hydrateWindowsDaemon.if).toBe("${{ inputs.crabbox_job == 'hydrate-windows-daemon' }}"); expect(workflowStep(hydrateWindowsDaemon, "Setup Node.js").uses).toBe(SETUP_NODE_V6); const hydrateWindowsPnpm = workflowStep(hydrateWindowsDaemon, "Setup pnpm and dependencies"); expect(hydrateWindowsPnpm.shell).toBe("powershell"); expect(hydrateWindowsPnpm.run).toContain( '$env:PNPM_CONFIG_MODULES_DIR = Join-Path $pnpmCacheRoot "node_modules"', ); expect(hydrateWindowsPnpm.run).toContain( '$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $pnpmCacheRoot "virtual-store"', ); expect(hydrateWindowsPnpm.run).not.toContain("PNPM_CONFIG_PACKAGE_IMPORT_METHOD"); expect(hydrateWindowsPnpm.run).toContain("--config.side-effects-cache=false"); expect(hydrateWindowsPnpm.run).toContain("--ignore-scripts=true"); expect(hydrateWindowsPnpm.run).toContain('$env:PNPM_CONFIG_CHILD_CONCURRENCY = "4"'); expect(hydrateWindowsPnpm.run).toContain('$env:PNPM_CONFIG_NETWORK_CONCURRENCY = "8"'); expect(hydrateWindowsPnpm.run).toContain('$env:PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN = "false"'); expect(hydrateWindowsPnpm.run).toContain( "$Value | Out-File -FilePath $Path -Encoding utf8 -Append", ); expect(hydrateWindowsPnpm.run).toContain('"--filter",'); expect(hydrateWindowsPnpm.run).toContain('"openclaw",'); expect(hydrateWindowsPnpm.run).toContain( "New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR", ); expect(hydrateWindowsPnpm.run).toContain(".pnpm-workspace-state-v1.json"); expect(hydrateWindowsPnpm.run).not.toContain("Remove-Item -Recurse -Force"); expect(hydrateWindowsPnpm.run).not.toContain("Add-Content -Path $env:GITHUB_ENV"); expect(hydrateWindowsPnpm.run).not.toContain("Add-Content -Path $env:GITHUB_PATH"); expect(hydrateWindowsPnpm.run).toContain("corepack enable --install-directory $env:PNPM_HOME"); expect(hydrateWindowsPnpm.run).toContain("pnpm @installArgs"); expect(hydrateWindowsPnpm.run).toContain( '$corepackShimDir = Join-Path $nodeBin "node_modules\\corepack\\shims"', ); const hydrateWindowsFetch = workflowStep(hydrateWindowsDaemon, "Fetch main ref"); expect(hydrateWindowsFetch.shell).toBe("powershell"); expect(hydrateWindowsFetch.run).toContain( "$fetchInfo = New-Object System.Diagnostics.ProcessStartInfo", ); expect(hydrateWindowsFetch.run).toContain('$fetchInfo.FileName = "git"'); expect(hydrateWindowsFetch.run).toContain("$fetchInfo.WorkingDirectory = $repo"); expect(hydrateWindowsFetch.run).toContain("$fetchInfo.UseShellExecute = $false"); expect(hydrateWindowsFetch.run).not.toContain("$fetchInfo.RedirectStandardOutput = $true"); expect(hydrateWindowsFetch.run).not.toContain("$fetchInfo.RedirectStandardError = $true"); expect(hydrateWindowsFetch.run).toContain("$fetch = New-Object System.Diagnostics.Process"); expect(hydrateWindowsFetch.run).toContain("$fetch.StartInfo = $fetchInfo"); expect(hydrateWindowsFetch.run).toContain("$fetch.WaitForExit(30000)"); expect(hydrateWindowsFetch.run).toContain("$fetch.Kill()"); expect(hydrateWindowsFetch.run).not.toContain("StandardOutput.ReadToEnd()"); expect(hydrateWindowsFetch.run).not.toContain("StandardError.ReadToEnd()"); expect(hydrateWindowsFetch.run).toContain("git fetch failed with exit code $($fetch.ExitCode)"); expect(hydrateWindowsFetch.run).toContain( "--no-tags --no-progress --prune --no-recurse-submodules --depth=50", ); expect(hydrateWindowsFetch.run).toContain('"+refs/heads/main:refs/remotes/origin/main"'); expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").shell).toBe("powershell"); expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").run).toContain('"NODE_BIN"'); expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").run).toContain('"PNPM_HOME"'); expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").run).toContain('"PATH"'); expect(workflowText).toContain("OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300"); expect(workflowText).toContain("OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3"); expect(workflowText).toContain("--retry-all-errors"); expect(workflowText).not.toContain("curl -fsSL https://get.docker.com | sudo sh"); expect(hydrateGithub.if).toBe("${{ inputs.crabbox_job == 'hydrate-github' }}"); expect(workflowStep(hydrateGithub, "Setup Node environment").uses).toBe( "./.github/actions/setup-node-env", ); expect(workflowStep(hydrateGithub, "Setup Node environment").env?.PNPM_HOME).toBe( "${{ runner.temp }}/pnpm-home", ); const hydrateGithubCrabboxShell = workflowStep(hydrateGithub, "Prepare Crabbox shell").run; expect(hydrateGithubCrabboxShell).toContain("link_node_tool()"); expect(hydrateGithubCrabboxShell).toContain('readlink -f "$source"'); expect(hydrateGithubCrabboxShell).toContain('readlink -f "$target"'); expect(hydrateGithubCrabboxShell).toContain("link_node_tool corepack"); const markHydrateGithubReady = workflowStep(hydrateGithub, "Mark Crabbox ready").run; expect(markHydrateGithubReady).toContain("OPENCLAW_CRABBOX_DOCKER_AVAILABLE"); expect(markHydrateGithubReady).toContain("PNPM_CONFIG_PACKAGE_IMPORT_METHOD"); expect(workflowStep(hydrateGithub, "Hydrate provider env helper").env?.FACTORY_API_KEY).toBe( "${{ secrets.FACTORY_API_KEY }}", ); }); it("defaults Crabbox proof to Blacksmith while keeping direct jobs on Azure", () => { const crabboxConfig = parse(readFileSync(CRABBOX_CONFIG, "utf8")) as { aws?: { region?: string }; capacity?: { availabilityZones?: string[]; fallback?: string; market?: string; regions?: string[]; }; jobs?: { changed?: { command?: string; market?: string; provider?: string; shell?: boolean; type?: string; }; prewarm?: { market?: string; provider?: string; type?: string }; }; provider?: string; ssh?: { port?: string; user?: string }; }; expect(crabboxConfig.provider).toBe("blacksmith-testbox"); expect(crabboxConfig.capacity?.market).toBe("on-demand"); expect(crabboxConfig.capacity?.fallback).toBeUndefined(); expect(crabboxConfig.capacity?.regions).toBeUndefined(); expect(crabboxConfig.capacity?.availabilityZones).toBeUndefined(); expect(crabboxConfig.aws?.region).toBe("eu-west-1"); expect(crabboxConfig.jobs?.prewarm?.market).toBe("on-demand"); expect(crabboxConfig.jobs?.prewarm?.provider).toBe("azure"); expect(crabboxConfig.jobs?.prewarm?.type).toBe("Standard_D4ads_v6"); expect(crabboxConfig.jobs?.changed?.market).toBe("on-demand"); expect(crabboxConfig.jobs?.changed?.provider).toBe("azure"); expect(crabboxConfig.jobs?.changed?.type).toBe("Standard_D4ads_v6"); expect(crabboxConfig.jobs?.changed?.shell).toBe(true); expect(crabboxConfig.jobs?.changed?.command).toContain("set -euo pipefail"); expect(crabboxConfig.jobs?.changed?.command).toContain("git init -q"); expect(crabboxConfig.jobs?.changed?.command).toContain( "commit -q --no-gpg-sign -m remote-check-tree", ); expect(crabboxConfig.jobs?.changed?.command).toContain("env CI=1 corepack pnpm check --timed"); expect(crabboxConfig.ssh?.user).toBe("crabbox"); expect(crabboxConfig.ssh?.port).toBe("22"); }); it("resolves candidate package sources before reusing Docker E2E lanes", () => { const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); expect(workflow).toContain("name: Package Acceptance"); expect(workflow).toContain("workflow_call:"); expect(workflow).toContain("workflow_ref:"); expect(workflow).toContain("package_ref:"); expect(workflow).toContain("source:"); expect(workflow).toContain("- npm"); expect(workflow).toContain("- ref"); expect(workflow).toContain("- url"); expect(workflow).toContain("- trusted-url"); expect(workflow).toContain("- artifact"); expect(workflow).toContain("trusted_source_id:"); expect(workflow).toContain("TRUSTED_SOURCE_ID: ${{ inputs.trusted_source_id }}"); expect(workflow).toContain('--trusted-source-id "$TRUSTED_SOURCE_ID"'); expect(workflow).toContain("scripts/resolve-openclaw-package-candidate.mts"); expect(workflow).toContain('--package-ref "$PACKAGE_REF"'); expect(workflow).toContain("artifact-ids: ${{ inputs.artifact_id }}"); expect(workflow).toContain("actions/artifacts/${ARTIFACT_ID}"); expect(workflow).toContain("name: ${{ env.PACKAGE_ARTIFACT_NAME }}"); expect(workflow).toContain("pull-requests: read"); expect(workflow).toContain( "uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml", ); expect(workflow).toContain( "ref: ${{ needs.resolve_package.outputs.package_source_sha || inputs.workflow_ref }}", ); expect(workflow).toContain( "package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}", ); expect(workflow).toContain("package_integrity:"); expect(workflow).toContain("name: Package integrity"); expect(workflow).toContain('node scripts/check-openclaw-package-tarball.mjs "$package"'); expect(workflow).toContain('[[ "$actual_sha256" == "$EXPECTED_PACKAGE_SHA256" ]]'); expect(workflow).toContain("needs: [resolve_package, package_integrity]"); expect(workflow).toContain("package_integrity=${PACKAGE_INTEGRITY_RESULT}"); }); it("keeps ref packaging independent of workflow-checkout dependencies", () => { const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); const resolveJob = workflow.slice( workflow.indexOf(" resolve_package:"), workflow.indexOf(" package_integrity:"), ); expect(resolveJob).toContain("scripts/resolve-openclaw-package-candidate.mts"); expect(resolveJob).not.toContain("pnpm install"); }); 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:"); expect(workflow).toContain("published_upgrade_survivor_baselines:"); expect(workflow).toContain("last-stable-4"); expect(workflow).toContain("all-since-2026.4.23"); expect(workflow).toContain("published_upgrade_survivor_scenarios:"); expect(workflow).toContain("scripts/resolve-upgrade-survivor-baselines.mts"); expect(workflow).toContain("--history-count 6"); expect(workflow).toContain("--include-version 2026.4.23"); expect(workflow).toContain("--pre-date 2026-03-15T00:00:00Z"); expect(workflow).toContain('"last-stable-"'); expect(workflow).toContain('"all-since-"'); expect(workflow).toContain("npm-onboard-channel-agent gateway-network config-reload"); expect(workflow).toContain("npm-onboard-channel-agent doctor-switch"); expect(workflow).toContain("update-channel-switch skill-install update-corrupt-plugin"); expect(workflow).toContain("update-corrupt-plugin upgrade-survivor"); expect(workflow).toContain("published-upgrade-survivor"); expect(workflow).toContain( "published-upgrade-survivor root-managed-vps-upgrade update-restart-auth", ); expect(workflow).toContain("plugins-offline plugin-update"); expect(workflow).toContain("include_release_path_suites=true"); expect(workflow).not.toContain("telegram_mode requires source=npm"); expect(workflow).toContain("uses: ./.github/workflows/npm-telegram-beta-e2e.yml"); expect(workflow).toContain( "package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}", ); expect(workflow).toContain( "package_artifact_digest: ${{ needs.resolve_package.outputs.package_artifact_digest }}", ); expect(workflow).toContain( "package_artifact_id: ${{ needs.resolve_package.outputs.package_artifact_id }}", ); expect(workflow).toContain( "package_artifact_run_attempt: ${{ needs.resolve_package.outputs.package_artifact_run_attempt }}", ); expect(workflow).toContain( "package_artifact_run_id: ${{ needs.resolve_package.outputs.package_artifact_run_id }}", ); expect(workflow).toContain( "package_file_name: ${{ needs.resolve_package.outputs.package_file_name }}", ); expect(workflow).toContain( "package_sha256: ${{ needs.resolve_package.outputs.package_sha256 }}", ); expect(workflow).toContain( "package_source_sha: ${{ needs.resolve_package.outputs.package_source_sha }}", ); expect(workflow).toContain( "package_version: ${{ needs.resolve_package.outputs.package_version }}", ); expect(workflow).toContain("telegram_scenarios:"); expect(workflow).toContain("scenario: ${{ inputs.telegram_scenarios }}"); expect(workflow).toContain( "package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}", ); expect(npmTelegramWorkflow).toContain("package_artifact_run_id:"); expect(npmTelegramWorkflow).toContain("Download package-under-test artifact from release run"); expect(npmTelegramWorkflow).toContain("run-id: ${{ inputs.package_artifact_run_id }}"); expect(npmTelegramWorkflow).toContain("github-token: ${{ github.token }}"); expect(workflow).toContain( "package_source_sha: ${{ steps.resolve.outputs.package_source_sha }}", ); 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, [ "pnpm build 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( "published_upgrade_survivor_baseline: ${{ needs.resolve_package.outputs.published_upgrade_survivor_baseline }}", ); expect(workflow).toContain( "published_upgrade_survivor_baselines: ${{ needs.resolve_package.outputs.published_upgrade_survivor_baselines }}", ); expect(workflow).toContain( "published_upgrade_survivor_scenarios: ${{ needs.resolve_package.outputs.published_upgrade_survivor_scenarios }}", ); expect(workflow).toContain("Published upgrade survivor baseline:"); expect(workflow).toContain("Published upgrade survivor baselines:"); expect(workflow).toContain("Published upgrade survivor scenarios:"); }); it("requires full release child workflows to run at the parent workflow SHA", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const performanceJob = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"), "Dispatch and monitor OpenClaw Performance", ).run; expect(workflow).toContain("TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}"); expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}"); expect(workflow).toContain("PARENT_WORKFLOW_SHA: ${{ github.sha }}"); expect(workflow).toContain("release_package_spec:"); expect(workflow).toContain('args+=(-f release_package_spec="$RELEASE_PACKAGE_SPEC")'); expect(workflow).toContain("package_acceptance_package_spec:"); expect(workflow).toContain( 'args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC")', ); expect(workflow).toContain("codex_plugin_spec:"); expect(workflow).toContain('args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC")'); expect(releaseChecksWorkflow).toContain( 'codex_plugin_spec="npm:@openclaw/codex@${BASH_REMATCH[1]}"', ); expect(releaseChecksWorkflow.match(/run: pnpm build qaRuntime/gu)).toHaveLength(6); expect(releaseChecksWorkflow).not.toContain( "node --import tsx scripts/build-all.mts qaRuntime", ); expect(releaseChecksWorkflow).toContain( "codex_plugin_spec: ${{ needs.resolve_target.outputs.codex_plugin_spec }}", ); expect(workflow).toContain("--json status,conclusion,url,attempt,headSha,jobs"); expect(workflow).toContain( 'gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha', ); expect(workflow).toContain( "Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch.", ); expect(workflow).toContain('if [[ "$head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then'); expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1'); expect(performanceJob).toContain( 'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"', ); expect(performanceJob).toContain('-f dispatch_id="$dispatch_id"'); expect(performanceJob).toContain( 'DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF"', ); expect(performanceJob).toContain(".display_title == env.DISPATCH_RUN_NAME"); expect(performanceJob).toContain("Could not find exact dispatched run ${dispatch_run_name}"); expect(performanceJob).not.toContain("BEFORE_IDS="); expect(performanceJob).not.toContain( "did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs", ); expect(workflow).toContain( "child run used workflow SHA ${head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}", ); expect(workflow).toContain( "Use the SHA-pinned release helper when a moving branch cannot stay fixed", ); expect(workflow).toContain("| Child | Result | Minutes | Head SHA | Run |"); expect(releaseChecksWorkflow).toContain("refs/heads/release-ci/[0-9a-f]{12}-[0-9]+"); expect(releaseChecksWorkflow).toContain( "source: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec != '' || needs.resolve_target.outputs.release_package_spec != '') && 'npm' || 'artifact' }}", ); expect(releaseChecksWorkflow).toContain( "package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || needs.resolve_target.outputs.release_package_spec || 'openclaw@beta' }}", ); }); it("keeps performance evidence advisory for beta releases", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); const performanceStep = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"), "Dispatch and monitor OpenClaw Performance", ); const summaryStep = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"), "Verify child workflow results", ); expect(performanceStep.env?.RELEASE_PROFILE).toBe("${{ inputs.release_profile }}"); expectTextToIncludeAll(performanceStep.run, [ "fail_on_regression=true", 'if [[ "$RELEASE_PROFILE" == "beta" ]]', "fail_on_regression=false", '-f fail_on_regression="$fail_on_regression"', "Release impact: advisory", "advisory for beta", ]); expect(summaryStep.env?.RELEASE_PROFILE).toBe("${{ inputs.release_profile }}"); expectTextToIncludeAll(summaryStep.run, [ '[[ "$RELEASE_PROFILE" == "beta" ]] && performance_advisory=1', 'check_child "product_performance" "$PERFORMANCE_RUN_ID" "$performance_required" "$performance_advisory"', ]); expect(workflow).toContain('performanceBlocking: ($releaseProfile != "beta")'); expect(workflow).toContain('blocking: ($releaseProfile != "beta")'); }); it("keeps beta performance advisory at the publish gate", () => { const validationStep = workflowStep( workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target"), "Validate full release validation manifest", ); const npmValidationStep = workflowStep( workflowJob(".github/workflows/openclaw-npm-release.yml", "publish_openclaw_npm"), "Verify full release validation target", ); expectTextToIncludeAll(validationStep.run, [ 'if [[ "$release_profile" != "beta" && "$performance_blocking" != "true" ]]', "Full release validation manifest does not record blocking product performance evidence.", ]); expectTextToIncludeAll(npmValidationStep.run, [ 'if [[ "$RELEASE_NPM_DIST_TAG" != "beta" && "$PERFORMANCE_BLOCKING" != "true" ]]', "Full release validation manifest does not record blocking product performance evidence.", ]); }); it("keeps child-job fail-fast polling best-effort", () => { for (const child of FULL_RELEASE_CHILD_DISPATCHES.slice(0, 3)) { const dispatch = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, child.jobName), child.stepName, ); expect(dispatch.env?.CHILD_WORKFLOW_KIND).toBe(child.kind); expect(dispatch.run).toContain("continuing with authoritative workflow conclusion."); } }); it("adopts exact full-release child runs without retrying ambiguous dispatch posts", () => { const dispatchScripts = FULL_RELEASE_CHILD_DISPATCHES.map((child) => { const job = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, child.jobName); const step = workflowStep(job, child.stepName); expect(step.env?.CHILD_WORKFLOW_KIND).toBe(child.kind); return step.run ?? ""; }); expect(new Set(dispatchScripts).size).toBe(1); for (const script of dispatchScripts) { expect(script.match(/gh workflow run/gu)).toHaveLength(1); expect(script).not.toContain("gh_with_retry workflow run"); expectTextToIncludeAll(script, [ "The dispatch POST is one-shot", 'encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF"', 'gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha', '"$current_workflow_sha" != "$PARENT_WORKFLOW_SHA"', "refusing dispatch.", "set +e", "dispatch_status=$?", 'if [[ "$dispatch_status" -ne 0 && ! "$dispatch_output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]', "dispatch failed with non-ambiguous status ${dispatch_status}; refusing adoption polling.", 'sed -nE "s#^https://github[.]com/${GITHUB_REPOSITORY}/actions/runs/([0-9]+)\\$#\\1#p"', 'validate_child_run "$run_id"', 'DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF"', ".display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF", "Multiple runs matched ${dispatch_run_name}; refusing to guess.", "The dispatch was not retried to avoid creating a duplicate child.", "adopted exact run ${run_id}", ]); expect(script.indexOf("dispatch failed with non-ambiguous status")).toBeLessThan( script.indexOf('run_id=""'), ); } const parsedWorkflow = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW); const transientPattern = parsedWorkflow.env?.GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN; expect(transientPattern).toBeDefined(); const transientError = new RegExp(transientPattern ?? "", "u"); for (const message of [ "could not create workflow dispatch event: HTTP 500: Failed to run workflow dispatch", "gh: HTTP 502", "500 Internal Server Error", "invalid character '<' looking for beginning of value", "error connecting to api.github.com", "context deadline exceeded", "read: connection reset by peer", "connect: connection refused", "net/http: TLS handshake timeout", "read: i/o timeout", "network is unreachable", "unexpected EOF", 'Post "https://api.github.com/repos/openclaw/openclaw/actions/workflows/ci.yml/dispatches": EOF', "EOF", "ETIMEDOUT", "ECONNRESET", "EAI_AGAIN", ]) { expect(transientError.test(message), message).toBe(true); } for (const message of [ "HTTP 400: Bad Request", "HTTP 401: Bad credentials", "HTTP 403: Resource not accessible by integration", "HTTP 404: workflow not found", "HTTP 422: Validation Failed", "HTTP 429: too many requests", "unknown flag --field", "EOFError while parsing local input", ]) { expect(transientError.test(message), message).toBe(false); } const summaryScript = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"), "Verify child workflow results", ).run ?? ""; for (const script of [...dispatchScripts, summaryScript]) { expect(script.match(/gh_with_retry\(\)/gu)).toHaveLength(1); expectTextToIncludeAll(script, [ '"$output" == *"HTTP 429"*', '"$output" == *"abuse detection"*', '"$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN', 'return "$status"', ]); } const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); const retryCalls = workflow.split("\n").filter((line) => line.includes("gh_with_retry ")); expect(retryCalls.length).toBeGreaterThan(0); for (const call of retryCalls) { expect(call).toMatch(/gh_with_retry (api|run view)/u); } expect(workflow).not.toMatch(/gh_with_retry (workflow run|run cancel)/u); expectTextToIncludeAll(workflow, [ 'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-ci"', 'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-plugin-prerelease"', 'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-release-checks"', 'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-npm-telegram"', 'args+=(-f dispatch_id="$dispatch_id")', ]); expect(readFileSync(".github/workflows/ci.yml", "utf8")).toContain( "format('CI {0}', inputs.dispatch_id)", ); expect(readFileSync(".github/workflows/plugin-prerelease.yml", "utf8")).toContain( "format('Plugin Prerelease {0}', inputs.dispatch_id)", ); expect(readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8")).toContain( "format('OpenClaw Release Checks {0}', inputs.dispatch_id)", ); expect(readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8")).toContain( "format('NPM Telegram Beta E2E {0}', inputs.dispatch_id)", ); }); it.each(FULL_RELEASE_CHILD_DISPATCHES)( "adopts and validates the run URL returned for $jobName without listing runs", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101", }); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1); expect( calls.filter(({ args }) => args.some((value) => value.includes("/actions/workflows/") && value.endsWith("/runs")), ), ).toHaveLength(0); expect( calls.some(({ args }) => args.some((value) => value.endsWith("/actions/runs/101"))), ).toBe(true); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(0); }, ); it("recovers by exact name when a successful dispatch returns no run URL", () => { const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], { MOCK_GH_DISPATCH_OUTPUT: "", }); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1); expect( calls.filter(({ args }) => args.some((value) => value.includes("/actions/workflows/") && value.endsWith("/runs")), ), ).toHaveLength(1); }); it.each([ ["workflow", { MOCK_GH_RUN_WORKFLOW_ID: "790" }], ["title", { MOCK_GH_RUN_TITLE: "Unrelated workflow run" }], ["head branch", { MOCK_GH_RUN_HEAD_BRANCH: "other" }], ["event", { MOCK_GH_RUN_EVENT: "push" }], ] as const)("refuses a returned run URL with the wrong %s", (label, overrides) => { const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], { MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101", ...overrides, }); expect(result.status).toBe(1); expect(result.stderr).toContain( label === "title" ? "Refusing to adopt ci.yml run 101: run never became readable with display title" : "Refusing to adopt unvalidated ci.yml run 101", ); expect( calls.some(({ args }) => args.some((value) => value.includes("/actions/workflows/") && value.endsWith("/runs")), ), ).toBe(false); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(0); }); it("refuses a nonnumeric exact-name candidate without cancellation ownership", () => { const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], { MOCK_GH_DISPATCH_OUTPUT: "", MOCK_GH_MATCHES: '["not-a-run-id"]', }); expect(result.status).toBe(1); expect(result.stderr).toContain("Refusing to adopt invalid ci.yml run ID not-a-run-id"); expect( calls.some(({ args }) => args.some((value) => value.endsWith("/actions/runs/not-a-run-id"))), ).toBe(false); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(0); }); it.each(FULL_RELEASE_CHILD_DISPATCHES)( "rejects moved workflow refs before dispatching $jobName", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { MOCK_GH_CURRENT_SHA: "c".repeat(40), }); expect(result.status).toBe(1); expect(result.stderr).toContain("refusing dispatch."); expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(0); }, ); it.each(FULL_RELEASE_CHILD_DISPATCHES)( "adopts the one exact $jobName child after an ambiguous dispatch without reposting", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { MOCK_GH_DISPATCH_ERROR: "HTTP 500: Failed to run workflow dispatch", }); const dispatchCalls = calls.filter(({ args }) => args[0] === "workflow"); const adoptionCall = calls.find(({ args }) => args.includes("-X")); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); expect(result.stderr).toContain("adopted exact run 101"); expect(dispatchCalls).toHaveLength(1); expect(dispatchCalls[0]?.args.slice(0, 5)).toEqual([ "workflow", "run", child.workflow, "--ref", "main", ]); expect(adoptionCall).toMatchObject({ childWorkflowRef: "main", dispatchRunName: `${child.runName} full-release-validation-77-2${child.nonceSuffix}`, }); expect(adoptionCall?.args).toContain( "[.workflow_runs[] | select(.display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF) | .id]", ); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(0); }, ); it("leaves the adopted child running when the monitor receives SIGTERM", () => { const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], { MOCK_GH_STATUSES: '["in_progress"]', MOCK_SLEEP_SIGNAL: "TERM", }); expect(result.status, `${result.stdout}\n${result.stderr}`).toBeNull(); expect(result.signal).toBe("SIGTERM"); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(0); }); it.each(FULL_RELEASE_CHILD_DISPATCHES)( "refuses duplicate exact adoption candidates for $jobName", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { MOCK_GH_MATCHES: "[101, 102]", }); expect(result.status).toBe(1); expect(result.stderr).toContain("Multiple runs matched"); expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(0); }, ); it.each(FULL_RELEASE_CHILD_DISPATCHES)( "refuses to adopt or retry a non-transient $jobName dispatch failure", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { MOCK_GH_DISPATCH_ERROR: "HTTP 422: Validation Failed", }); expect(result.status).toBe(1); expect(result.stderr).toContain("refusing adoption polling"); expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1); expect(calls.some(({ args }) => args.includes("-X"))).toBe(false); expect(calls.some(({ args }) => args[0] === "run" && args[1] === "cancel")).toBe(false); }, ); it.each(FULL_RELEASE_CHILD_DISPATCHES)( "cancels exactly the identified $jobName child when its workflow SHA differs", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { MOCK_GH_CHILD_SHA: "c".repeat(40), MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101", }); expect(result.status).toBe(1); expect(result.stderr).toContain("expected parent workflow SHA"); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toEqual([ expect.objectContaining({ args: ["run", "cancel", "101"] }), ]); }, ); it.each(FULL_RELEASE_CHILD_DISPATCHES)( "leaves the adopted $jobName child running when monitoring fails unexpectedly", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { MOCK_GH_STATUS_ERROR: "HTTP 403: Resource not accessible by integration", }); expect(result.status).toBe(1); expect(result.stderr).toContain("HTTP 403"); expect( calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel"), `${result.stdout}\n${result.stderr}\n${JSON.stringify(calls)}`, ).toEqual([]); }, ); it.each(FULL_RELEASE_CHILD_DISPATCHES.slice(0, 4))( "cancels the exact $jobName child after its first blocking failed job", (child) => { const { calls, result } = runFullReleaseChildDispatch(child, { FAIL_FAST: "true", MOCK_GH_JOBS: JSON.stringify([ { conclusion: "failure", html_url: "https://github.com/openclaw/openclaw/actions/runs/101/job/201", name: "Run package acceptance", status: "completed", url: "https://github.com/openclaw/openclaw/actions/runs/101/job/201", }, ]), MOCK_GH_STATUSES: JSON.stringify([ "in_progress", "in_progress", "in_progress", "in_progress", "in_progress", "in_progress", "completed", ]), }); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(1); expect(result.stdout).toContain("has failed child jobs before the workflow completed"); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(1); }, ); it("keeps CI fail-fast job lookups advisory and npm Telegram fail-closed without cancellation", () => { const overrides = { FAIL_FAST: "true", MOCK_GH_JOBS_ERROR: "HTTP 403: Resource not accessible by integration", MOCK_GH_STATUSES: JSON.stringify([ "in_progress", "in_progress", "in_progress", "in_progress", "in_progress", "in_progress", "completed", ]), }; const normalCi = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], overrides); const npmTelegram = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[3], overrides); expect(normalCi.result.status, normalCi.result.stderr).toBe(0); expect(normalCi.result.stdout).toContain("continuing with authoritative workflow conclusion."); expect(npmTelegram.result.status).toBe(1); expect( npmTelegram.calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel"), `${npmTelegram.result.stdout}\n${npmTelegram.result.stderr}\n${JSON.stringify(npmTelegram.calls)}`, ).toEqual([]); }); it.each([ { expectedStatus: 0, jobName: "Run QA Lab parity lane (sqlite)" }, { expectedStatus: 0, jobName: "Run QA Lab live Discord lane" }, { expectedStatus: 0, jobName: "Run repo/live E2E validation / Docker live" }, { expectedStatus: 0, jobName: "Run package acceptance / Telegram package acceptance / mock-openai", }, { expectedStatus: 1, jobName: "Run repo/live E2E validation / Repo E2E" }, { expectedStatus: 1, jobName: "Run package acceptance / Verify package integrity" }, ])("preserves beta fail-fast ownership for $jobName", ({ expectedStatus, jobName }) => { const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[2], { FAIL_FAST: "true", MOCK_GH_JOBS: JSON.stringify([ { conclusion: "failure", html_url: "https://github.com/openclaw/openclaw/actions/runs/101/job/201", name: jobName, status: "completed", }, ]), MOCK_GH_STATUSES: JSON.stringify([ "in_progress", "in_progress", "in_progress", "in_progress", "in_progress", "in_progress", "completed", ]), RELEASE_PROFILE: "beta", }); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(expectedStatus); expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength( expectedStatus, ); }); it.each([ { expectedStatus: 0, failOnRegression: "false", profile: "beta" }, { expectedStatus: 1, failOnRegression: "true", profile: "stable" }, ])( "keeps failed product performance $profile release behavior unchanged", ({ expectedStatus, failOnRegression, profile }) => { const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[4], { MOCK_GH_CONCLUSION: "failure", RELEASE_PROFILE: profile, }); const dispatch = calls.find(({ args }) => args[0] === "workflow"); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(expectedStatus); expect(dispatch?.args).toContain(`fail_on_regression=${failOnRegression}`); if (profile === "beta") { expect(result.stdout).toContain("advisory for beta"); } }, ); it.each([ { expectedStatus: 0, failingJob: "Run optional live-provider check" }, { expectedStatus: 1, failingJob: "Run package acceptance" }, ])("keeps Tideclaw alpha package-safety lanes blocking", ({ expectedStatus, failingJob }) => { const { result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[2], { CHILD_WORKFLOW_REF: "tideclaw/alpha/2026-08-01-0000Z", MOCK_GH_CONCLUSION: "failure", MOCK_GH_JOBS: JSON.stringify([ { conclusion: "success", html_url: "https://github.com/openclaw/openclaw/actions/runs/101/job/201", name: "Verify release checks", status: "completed", }, { conclusion: "failure", html_url: "https://github.com/openclaw/openclaw/actions/runs/101/job/202", name: failingJob, status: "completed", }, ]), }); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(expectedStatus); if (expectedStatus === 0) { expect(result.stdout).toContain("accepted Tideclaw alpha advisory lanes"); } else { expect(result.stdout).toContain("package-safety Tideclaw alpha release-check lane"); } }); it("keeps exhaustive update migration as a separate manual package gate", () => { const workflow = readFileSync(UPDATE_MIGRATION_WORKFLOW, "utf8"); const packageWorkflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); expect(workflow).toContain("name: Update Migration"); expect(workflow).toContain("uses: ./.github/workflows/package-acceptance.yml"); expect(workflow).toContain("source: ref"); expect(workflow).toContain("suite_profile: custom"); expect(workflow).toContain("docker_lanes: update-migration"); expect(workflow).toContain("default: all-since-2026.4.23"); expect(workflow).toContain("default: plugin-deps-cleanup"); expect(workflow).toContain("telegram_mode: none"); expect(workflow).toContain("secrets: inherit"); expect(packageWorkflow).toContain("published-upgrade-survivor/update-migration"); }); }); describe("package artifact reuse", () => { it("binds package acceptance input artifacts to the complete producer tuple", () => { const resolvePackage = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package"); expect(workflowStep(resolvePackage, "Setup Node environment").with).toMatchObject({ "install-deps": "true", }); expect( workflowStep(resolvePackage, "Checkout package workflow ref").with?.["persist-credentials"], ).toBe(false); const identity = workflowStep(resolvePackage, "Validate package artifact input identity"); expect(identity.env).toMatchObject({ ARTIFACT_DIGEST: "${{ inputs.artifact_digest }}", ARTIFACT_ID: "${{ inputs.artifact_id }}", ARTIFACT_NAME: "${{ inputs.artifact_name }}", ARTIFACT_RUN_ATTEMPT: "${{ inputs.artifact_run_attempt }}", ARTIFACT_RUN_ID: "${{ inputs.artifact_run_id }}", EXPECTED_PACKAGE_FILE_NAME: "${{ inputs.package_file_name }}", EXPECTED_PACKAGE_SHA256: "${{ inputs.package_sha256 }}", EXPECTED_PACKAGE_SOURCE_SHA: "${{ inputs.package_source_sha }}", EXPECTED_PACKAGE_VERSION: "${{ inputs.package_version }}", }); expectTextToIncludeAll(identity.run, [ "source=artifact requires the complete immutable artifact and package identity tuple.", '[[ "$ARTIFACT_NAME" == *"-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" ]]', '--arg digest "sha256:${ARTIFACT_DIGEST}"', "actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}", ]); expect(workflowStep(resolvePackage, "Download package artifact input").with).toMatchObject({ "artifact-ids": "${{ inputs.artifact_id }}", "github-token": "${{ github.token }}", "run-id": "${{ inputs.artifact_run_id }}", }); const resolveStep = workflowStep(resolvePackage, "Resolve package candidate"); expect(resolveStep.env).toMatchObject({ PACKAGE_FILE_NAME: "${{ inputs.package_file_name }}", PACKAGE_SHA256: "${{ inputs.package_sha256 }}", PACKAGE_SOURCE_SHA: "${{ inputs.package_source_sha }}", PACKAGE_VERSION: "${{ inputs.package_version }}", }); expectTextToIncludeAll(resolveStep.run, [ 'artifact_tarball="${artifact_dir}/${PACKAGE_FILE_NAME}"', "Selected artifact package SHA-256 differs from package_sha256.", "Resolved package identity differs from the declared immutable tuple.", ]); const packageIntegrity = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "package_integrity"); expect( workflowStep(packageIntegrity, "Setup package validation dependencies").with, ).toMatchObject({ "install-deps": "true", }); expect( workflowStep(packageIntegrity, "Download package-under-test artifact").with, ).toMatchObject({ "artifact-ids": "${{ needs.resolve_package.outputs.package_artifact_id }}", "github-token": "${{ github.token }}", "run-id": "${{ needs.resolve_package.outputs.package_artifact_run_id }}", }); }); it("lets reusable Docker E2E consume an already resolved package artifact", () => { const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); const parsedWorkflow = parse(workflow) as { jobs?: Record; on?: { workflow_call?: { inputs?: Record } }; }; const packageJson = readFileSync(PACKAGE_JSON, "utf8"); const scheduler = readFileSync("scripts/test-docker-all.mts", "utf8"); const publishedUpgradeSurvivor = readFileSync(UPGRADE_SURVIVOR_RUN_SCRIPT, "utf8"); expect(workflow).toContain("package_artifact_name:"); expect(workflow).toContain("package_artifact_digest:"); expect(workflow).toContain("package_artifact_id:"); expect(workflow).toContain("package_artifact_run_attempt:"); expect(workflow).toContain("package_artifact_run_id:"); expect(workflow).toContain("package_file_name:"); expect(workflow).toContain("package_source_sha:"); expect(workflow).toContain("package_sha256:"); expect(workflow).toContain("package_version:"); expect(workflow).toContain("published_upgrade_survivor_baseline:"); expect(workflow).toContain("published_upgrade_survivor_baselines:"); expect(workflow).toContain("published_upgrade_survivor_scenarios:"); expect(parsedWorkflow.on?.workflow_call?.inputs).toHaveProperty( "allow_frozen_target_scenario_omissions", ); expect(workflow).toContain("docker_e2e_bare_image:"); expect(workflow).toContain("docker_e2e_functional_image:"); expect(workflow).toContain("OPENCLAW_DOCKER_E2E_SELECTED_SHA:"); expect(workflow).toContain( "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}", ); expect(workflow).toContain( "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ matrix.group.published_upgrade_survivor_baselines || inputs.published_upgrade_survivor_baselines }}", ); expect(workflow).toContain( "OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}", ); expect(workflow).toContain("OPENCLAW_UPGRADE_SURVIVOR_TARGET_ROOT: ${{ github.workspace }}"); expect(workflow).toContain( "OPENCLAW_ALLOW_FROZEN_TARGET_SCENARIO_OMISSIONS: ${{ inputs.allow_frozen_target_scenario_omissions && '1' || '0' }}", ); expect(workflow).toContain("Download current-run OpenClaw Docker E2E package"); expect(workflow).toContain("Download previous-run OpenClaw Docker E2E package"); expect(workflow).toContain("inputs.package_artifact_id != ''"); expect(workflow).toContain( 'bare_image="${PROVIDED_BARE_IMAGE:-ghcr.io/${repository}-docker-e2e-bare:${image_tag}}"', ); expect(workflow).toContain( 'functional_image="${PROVIDED_FUNCTIONAL_IMAGE:-ghcr.io/${repository}-docker-e2e-functional:${image_tag}}"', ); expect(workflow).toContain("artifact-ids: ${{ inputs.package_artifact_id }}"); expect(workflow).toContain( '[[ "$ARTIFACT_NAME" == *"-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" ]]', ); expect(workflow).toContain('--arg digest "sha256:${ARTIFACT_DIGEST}"'); expect(workflow).toContain("actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}"); expect(workflow).not.toContain("uses: ./.github/actions/docker-e2e-plan"); expect(workflow).toContain("Checkout trusted release harness"); expect(workflow).toContain("OPENCLAW_DOCKER_E2E_REPO_ROOT:"); expect(workflow).toContain("node .release-harness/scripts/test-docker-all.mjs --plan-json"); expect(workflow).toContain("node .release-harness/scripts/docker-e2e.mjs github-outputs"); expect(parsedWorkflow.on?.workflow_call?.inputs).toHaveProperty( "enable_prepublish_plugin_registry", ); expect(workflow).toContain("Pack prerelease plugin registry artifact"); expect(workflow).toContain("Validate prerelease plugin registry artifact"); expect(workflow).toContain("Download targeted prerelease plugin registry artifact"); expect(workflow).toContain("OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR"); expect(workflow).toContain("prepublishPluginRegistryManifestSha256"); expect( workflowStep( workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"), "Pack prerelease plugin registry artifact", ).id, ).toBe("create_prepublish_plugin_registry"); expect( workflowStep( workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"), "Validate prerelease plugin registry artifact", ).env?.EXPECTED_MANIFEST_SHA256, ).toBe( "${{ steps.create_prepublish_plugin_registry.outputs.manifest_sha256 || inputs.prepublish_plugin_registry_manifest_sha256 }}", ); expect(workflow).toContain( "if: inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1'", ); expect( workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image").outputs ?.prepublish_plugin_registry_artifact_id, ).toContain("inputs.enable_prepublish_plugin_registry"); expect(workflow).toContain("bash .release-harness/scripts/ci-docker-pull-retry.sh"); const setupHarnessStepName = "Setup trusted release harness"; const harnessJobCases = [ { jobId: "validate_docker_e2e", planStepName: "Plan Docker E2E chunk", setupIf: "contains(matrix.profiles, inputs.release_test_profile)", }, { jobId: "validate_docker_lanes", planStepName: "Plan targeted Docker E2E lanes", setupIf: undefined, }, { jobId: "validate_docker_openwebui", planStepName: "Plan Open WebUI Docker E2E chunk", setupIf: undefined, }, { jobId: "prepare_docker_e2e_image", planStepName: "Plan Docker E2E images", setupIf: undefined, }, ] as const; const typedHarnessJobIds = Object.entries(parsedWorkflow.jobs ?? {}) .filter(([, job]) => (job.steps ?? []).some( (step) => step.run?.includes("node .release-harness/scripts/test-docker-all.mjs") || step.run?.includes("node .release-harness/scripts/docker-e2e.mjs"), ), ) .map(([jobId]) => jobId) .toSorted(); expect(typedHarnessJobIds).toEqual(harnessJobCases.map(({ jobId }) => jobId).toSorted()); for (const { jobId, planStepName, setupIf } of harnessJobCases) { const harnessJob = workflowJob(LIVE_E2E_WORKFLOW, jobId); const harnessJobSteps = harnessJob.steps ?? []; const harnessJobStepNames = harnessJobSteps.map((step) => step.name); const checkoutIndex = harnessJobStepNames.indexOf("Checkout trusted release harness"); const setupIndex = harnessJobStepNames.indexOf(setupHarnessStepName); const typedHarnessIndex = harnessJobSteps.findIndex( (step) => step.run?.includes("node .release-harness/scripts/test-docker-all.mjs") || step.run?.includes("node .release-harness/scripts/docker-e2e.mjs"), ); expect(harnessJobStepNames.filter((name) => name === setupHarnessStepName)).toHaveLength(1); expect(checkoutIndex).toBeGreaterThan(-1); expect(setupIndex).toBeGreaterThan(checkoutIndex); expect(typedHarnessIndex).toBeGreaterThan(setupIndex); expect(workflowStep(harnessJob, planStepName)).toBeDefined(); const setupHarnessStep = workflowStep(harnessJob, setupHarnessStepName); expect(setupHarnessStep).toMatchObject({ uses: "./.release-harness/.github/actions/setup-release-harness", with: { "node-version": "${{ env.NODE_VERSION }}" }, }); expect(setupHarnessStep.if).toBe(setupIf); } const prepareDockerImage = workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"); const prepareDockerImageStepNames = (prepareDockerImage.steps ?? []).map((step) => step.name); const planStepName = "Plan Docker E2E images"; const setupCandidateStepName = "Setup Node environment"; expect( prepareDockerImageStepNames.filter((name) => name === setupCandidateStepName), ).toHaveLength(1); expect(prepareDockerImageStepNames.indexOf(setupCandidateStepName)).toBeGreaterThan( prepareDockerImageStepNames.indexOf(planStepName), ); expect(workflowStep(prepareDockerImage, setupCandidateStepName)).toMatchObject({ if: "(steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == '' && inputs.package_artifact_run_id == '') || (inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id == '')", uses: "./.github/actions/setup-node-env", }); expect(workflowStep(prepareDockerImage, planStepName).env).toEqual({ INCLUDE_OPENWEBUI: "${{ inputs.include_openwebui }}", INCLUDE_RELEASE_PATH_SUITES: "${{ inputs.include_release_path_suites }}", LANES: "${{ inputs.docker_lanes }}", OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: "${{ inputs.published_upgrade_survivor_baseline }}", OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: "${{ inputs.published_upgrade_survivor_baselines }}", OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: "${{ inputs.published_upgrade_survivor_scenarios }}", PREPARE_ONLY: "${{ inputs.prepare_only }}", RELEASE_TEST_PROFILE: "${{ inputs.release_test_profile }}", }); expect(workflow).toContain("plan_docker_lane_groups:"); expect(workflow).toContain("targeted_docker_lane_group_size:"); expect(workflow).toContain("scripts/plan-targeted-docker-lane-groups.mjs"); expect(workflow).toContain( "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}", ); expect(workflow).toContain("Docker E2E targeted lanes (${{ matrix.group.label }})"); expect(workflow).toContain("LANES: ${{ matrix.group.docker_lanes }}"); expect(workflow).toContain("GROUP_LABEL: ${{ matrix.group.label }}"); expect(workflow).toContain("DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}"); expect(workflow).toContain("name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}"); expect(scheduler).toContain( "published_upgrade_survivor_baseline=${shellQuote(env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC)}", ); expect(scheduler).toContain( "published_upgrade_survivor_baselines=${shellQuote(env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS)}", ); expect(scheduler).toContain( '["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC]', ); expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS",'); expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS",'); expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1"); expect(packageJson).toContain("test:docker:update-restart-auth"); expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE=auto-auth"); expect(publishedUpgradeSurvivor).toContain("validate_baseline_package_spec"); expect(publishedUpgradeSurvivor).toContain("OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE"); expect(publishedUpgradeSurvivor).toContain('local shim_dir="$npm_config_prefix/bin"'); expect(publishedUpgradeSurvivor).toContain("seed_update_restart_probe_device_auth"); expect(publishedUpgradeSurvivor).toContain("upgrade survivor restart probe"); expect(publishedUpgradeSurvivor).toContain("write_update_restart_service_env"); expect(publishedUpgradeSurvivor).toContain("GATEWAY_AUTH_TOKEN_REF=%s"); expect(publishedUpgradeSurvivor).toContain("OPENCLAW_CLAWHUB_URL=%s"); expect(publishedUpgradeSurvivor).toContain("assert-no-requests"); expect(publishedUpgradeSurvivor).toContain( "env -u OPENCLAW_GATEWAY_TOKEN -u OPENCLAW_GATEWAY_PASSWORD openclaw", ); expect(publishedUpgradeSurvivor).toContain("phase prepare-update-restart-probe"); expect(publishedUpgradeSurvivor).toContain("openclaw@(alpha|beta|latest|"); expect(publishedUpgradeSurvivor).toContain("plugin_deps_cleanup_plugin_dirs"); expect(publishedUpgradeSurvivor).toContain('"$(package_root)/extensions/$plugin"'); expect(publishedUpgradeSurvivor).toContain("probe_gateway_endpoint"); expect(publishedUpgradeSurvivor).toContain( "assert_legacy_plugin_dependency_debris_before_doctor", ); expect(publishedUpgradeSurvivor.indexOf("phase seed-source-only-plugin-shadow")).toBeLessThan( publishedUpgradeSurvivor.indexOf("phase assert-baseline"), ); expect(publishedUpgradeSurvivor).toContain('"id": "opik-openclaw"'); expect(publishedUpgradeSurvivor).toContain('"configSchema": {'); expect(publishedUpgradeSurvivor).toContain( "Legacy plugin dependency debris was already removed before doctor", ); expect( publishedUpgradeSurvivor.indexOf('validate_baseline_package_spec "$baseline_spec"'), ).toBeLessThan( publishedUpgradeSurvivor.indexOf('npm install -g --prefix "$npm_config_prefix"'), ); }); it("reuses a content-addressed bare image for prepared E2E images", () => { const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); expect(workflow).toContain("bare_context_sha="); expect(workflow).toContain("-docker-e2e-bare:base-${bare_context_sha:0:32}"); expect(workflow).toContain('docker manifest inspect "$CACHE_IMAGE_REF"'); expect(workflow).toContain('cache=(--cache-from "$CACHE_IMAGE_REF")'); expect(workflow).not.toContain('docker tag "$CACHE_IMAGE_REF" "$IMAGE_REF"'); expect(workflow).toContain( "Shared release candidate preparation requires both Docker image variants.", ); expect(workflow).toContain( "inputs.shared_image_artifact_id != '' && '1' || steps.plan.outputs.needs_bare_image", ); expect(workflow).toContain("env DOCKER_BUILDKIT=1 docker build"); expect(workflow).toContain( 'if bash .release-harness/scripts/ci-docker-pull-retry.sh "$CACHE_IMAGE_REF"; then', ); expect(workflow).toContain("Bare image cache pull failed; continuing with a cold build."); expect(workflow).toContain("--build-context openclaw_package=.artifacts/docker-e2e-package"); expect(workflow).toContain('cache=(--cache-from "$BARE_IMAGE_REF")'); expect(workflow).not.toContain('docker push "$CACHE_IMAGE_REF"'); expect(workflow).toContain("uses: useblacksmith/setup-docker-builder@"); expect(workflow).toContain("uses: useblacksmith/build-push-action@"); expect(workflow).not.toContain("cache-from: type=gha,scope=docker-e2e"); expect(workflow).not.toContain("cache-to: type=gha,mode=max,scope=docker-e2e"); }); it("prepares one immutable candidate for release validation children", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); const prepare = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "prepare_release_candidate"); const pluginDispatch = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "plugin_prerelease"), "Dispatch and monitor plugin prerelease", ); const releaseDispatch = workflowStep( workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_checks"), "Dispatch and monitor release checks", ); expect(prepare.uses).toBe("./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml"); expect(prepare.with).toMatchObject({ enable_prepublish_plugin_registry: true, prepare_only: true, shared_image_policy: "no-push-artifact", }); expect(prepare.with?.published_upgrade_survivor_scenarios).toBe( "${{ (inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full') && 'reported-issues' || '' }}", ); expect(prepare.with?.allow_frozen_target_scenario_omissions).toBe( "${{ inputs.target_context_ref != '' }}", ); expect(pluginDispatch.run).toContain( 'args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")', ); expect(releaseDispatch.run).toContain( 'args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")', ); expect(workflow).toContain("Shared release candidate preparation ended with"); }); it("enables prerelease plugin companions for scheduled ref validation", () => { const scheduled = workflowJob(SCHEDULED_LIVE_CHECKS_WORKFLOW, "live_and_openwebui_checks"); expect(scheduled.with).toMatchObject({ enable_prepublish_plugin_registry: true, ref: "${{ github.sha }}", }); }); it("gives memory extension shards enough CPU without lowering their planner cost", () => { const workflow = readFileSync(PLUGIN_PRERELEASE_WORKFLOW, "utf8"); expect(workflow).toContain('extensionId.startsWith("memory-")'); expect(workflow).toContain('"blacksmith-16vcpu-ubuntu-2404"'); expect(workflow).toContain("vitest_max_workers:"); expect(workflow).toContain("OPENCLAW_VITEST_MAX_WORKERS: ${{ matrix.vitest_max_workers }}"); expect(readFileSync("scripts/lib/extension-test-plan.mts", "utf8")).toContain( '"test/vitest/vitest.extension-memory.config.ts": 1', ); }); it("shards broad native live tests instead of one serial live-all job", () => { const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); const retryHelper = readFileSync("scripts/ci-live-command-retry.sh", "utf8"); expect(workflow).toContain("validate_selected_ref:\n runs-on: ubuntu-24.04"); expect(workflow).not.toContain("suite_id: live-all"); expect(workflow).not.toContain("command: pnpm test:live\n"); expect(workflow).toContain("suite_id: native-live-src-agents"); expect(workflow).toContain("Checkout trusted live shard harness"); expect(workflow).toContain( "command: node .release-harness/scripts/test-live-shard.mjs native-live-src-agents", ); expect(workflow).toContain("suite_id: native-live-src-agents-zai-coding"); expect(workflow).toContain( "command: ZAI_CODING_LIVE_TEST=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-agents-zai-coding", ); expect(workflow).toContain("OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}"); expect(workflow).toContain("live_suite_filter:"); expect(workflow).toContain("validate_live_suite_filter:"); expect(workflow).toContain("LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}"); expect(workflow).toContain("live-cache attempt ${attempt}/2"); expect(workflow).toContain( "live_suite_filter '${LIVE_SUITE_FILTER}' does not match any runnable suite", ); expect(workflow).toContain('add_profile_suite docker-live-models "beta minimum stable full"'); expect(workflow).toContain( 'add_profile_suite native-live-src-gateway-core "beta minimum stable full"', ); expect(workflow).toContain('add_profile_suite native-live-src-infra "stable full"'); expect(workflow).toContain('add_profile_suite live-gateway-docker "beta minimum stable full"'); expect(workflow).toContain('add_profile_suite live-gateway-anthropic-docker "stable full"'); expect(workflow).toContain('add_profile_suite live-gateway-anthropic-docker-full "full"'); expect(workflow).toContain('add_profile_suite live-gateway-advisory-docker "full"'); expect(workflow).toContain( 'add_profile_suite live-gateway-advisory-docker-deepseek-fireworks "full"', ); expect(workflow).toContain( 'add_profile_suite live-gateway-advisory-docker-opencode-openrouter "full"', ); expect(workflow).toContain('add_profile_suite live-gateway-advisory-docker-xai-zai "full"'); expect(workflow).toContain('add_profile_suite live-cli-backend-docker "stable full"'); expect(workflow).toContain('add_profile_suite live-subagent-announce-docker "stable full"'); expect(workflow).toContain( "inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id", ); expect(workflow).not.toContain("openai-ws-stream-live-e2e"); expect(workflow).not.toContain("src/agents/openai-ws-stream.e2e.test.ts"); expect(workflow).toContain("suite_id: live-gateway-advisory-docker-deepseek-fireworks"); expect(workflow).toContain("suite_id: live-gateway-advisory-docker-opencode-openrouter"); expect(workflow).toContain("suite_id: live-gateway-advisory-docker-xai-zai"); expect(workflow).toContain("suite_id: live-subagent-announce-docker"); expect(workflow).toContain("suite_group: live-gateway-advisory-docker"); expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,fireworks"); expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go,openrouter"); expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_PROVIDERS=xai,zai"); expect(workflow).toContain("inputs.live_suite_filter == matrix.suite_group"); expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6"); expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key"); expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1"); expect(workflow).not.toContain('service_tier=\\"fast\\"'); expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_ARGS="); expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS="); expect(workflow).not.toContain( 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","--skip-git-repo-check"]', ); expect(workflow).toContain("bash .release-harness/scripts/ci-live-command-retry.sh"); expect(workflow).toContain("use_github_hosted_runners:"); expect(workflow).toMatch( /validate_repo_e2e:[\s\S]*?runs-on: \$\{\{ inputs\.use_github_hosted_runners && 'ubuntu-24\.04' \|\| 'blacksmith-8vcpu-ubuntu-2404' \}\}/u, ); expect(workflow).toMatch( /validate_special_e2e:[\s\S]*?runs-on: \$\{\{ inputs\.use_github_hosted_runners && 'ubuntu-24\.04' \|\| 'blacksmith-8vcpu-ubuntu-2404' \}\}/u, ); expect(workflow).toMatch( /validate_live_provider_suites:[\s\S]*?runs-on: \$\{\{ inputs\.use_github_hosted_runners && 'ubuntu-24\.04' \|\| 'blacksmith-8vcpu-ubuntu-2404' \}\}/u, ); expect(workflow).toContain("suite_id: native-live-src-gateway-core"); expect(workflow).toContain("suite_id: native-live-src-gateway-backends"); expect(workflow).toContain( "command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-core", ); expect(workflow).toContain( "command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-backends", ); expect(workflow).toContain("suite_id: native-live-src-infra"); expect(workflow).toContain( "command: OPENCLAW_LIVE_APNS_REACHABILITY=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-infra", ); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-anthropic-smoke"); expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_SETUP_TIMEOUT_MS=300000"); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-anthropic-opus"); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-anthropic-sonnet-haiku"); expect(workflow).toContain("suite_group: native-live-src-gateway-profiles-anthropic"); expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-5"); expect(workflow).toContain("anthropic/claude-sonnet-4-6,anthropic/claude-haiku-4-5"); expect(workflow).toMatch( /suite_id: native-live-src-gateway-profiles-fireworks[\s\S]*?advisory: true/u, ); expect(workflow).toMatch( /suite_id: native-live-src-gateway-profiles-openai[\s\S]*?timeout_minutes: 60[\s\S]*?profiles: beta minimum stable full/u, ); expect(workflow).toContain( "command: OPENCLAW_LIVE_GATEWAY_SETUP_TIMEOUT_MS=300000 OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.6-luna OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=180000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000", ); expect(workflow).toContain( "OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles", ); expect(workflow).toContain( "OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2", ); expect(workflow).toMatch( /suite_id: native-live-src-gateway-profiles-fireworks[\s\S]*?timeout_minutes: 30[\s\S]*?advisory: true/u, ); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-deepseek"); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-opencode-go"); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-openrouter"); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-xai"); expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-zai"); expect(workflow).not.toContain("Z.AI API Platform validation is temporarily disabled"); expect(workflow).not.toContain( "OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,opencode-go,openrouter,xai,zai", ); expect(workflow).toContain("suite_id: live-gateway-anthropic-docker"); expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2"); expect(workflow).toContain( "OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.6-luna OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000", ); expect(workflow).toContain( "OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-sonnet-4-6,anthropic/claude-haiku-4-5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2", ); expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000"); expect(workflow).toContain("timeout --foreground --kill-after=30s 35m"); expect(workflow).toMatch(/suite_id: live-gateway-docker[\s\S]*?timeout_minutes: 40/u); expect(workflow).toContain("suite_id: native-live-extensions-a-k"); expect(workflow).toContain("suite_id: native-live-extensions-l-n"); expect(workflow).toContain("suite_id: native-live-extensions-moonshot"); expect(workflow).toMatch(/suite_id: native-live-extensions-moonshot[\s\S]*?advisory: true/u); expect(workflow).toContain("OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}"); expect(workflow).toContain("Advisory live suite failed with exit code"); expect(workflow).toMatch( /validate_live_media_provider_suites:[\s\S]*?OPENCLAW_LIVE_SUITE_ADVISORY: \$\{\{ matrix\.advisory \}\}[\s\S]*?shell: bash[\s\S]*?Advisory live suite failed with exit code/u, ); expect(workflow).toMatch( /suite_id: live-gateway-advisory-docker-deepseek-fireworks[\s\S]*?advisory: true/u, ); expect(workflow).toMatch( /validate_live_media_provider_suites:[\s\S]*?OPENCLAW_LIVE_SUITE_ADVISORY: \$\{\{ matrix\.advisory \}\}/u, ); expect(workflow).toMatch( /suite_id: native-live-extensions-media-video-d[\s\S]*?timeout_minutes: 30[\s\S]*?advisory: true/u, ); expect(workflow).toContain("suite_id: native-live-extensions-openai"); expect(workflow).toContain("suite_id: native-live-extensions-o-z-other"); expect(workflow).toContain("validate_live_media_provider_suites:"); expect(workflow).toMatch( /validate_live_media_provider_suites:[\s\S]*?runs-on: \$\{\{ inputs\.use_github_hosted_runners && 'ubuntu-24\.04' \|\| 'blacksmith-8vcpu-ubuntu-2404' \}\}/u, ); expect(workflow).toContain(`image: ${LIVE_MEDIA_RUNNER_IMAGE}`); expect(workflow).toContain("ffmpeg -version | head -1"); expect(workflow).toContain("ffprobe -version | head -1"); const imageDockerfile = readFileSync(LIVE_MEDIA_RUNNER_DOCKERFILE, "utf8"); const imageWorkflow = readFileSync(LIVE_MEDIA_RUNNER_IMAGE_WORKFLOW, "utf8"); const buildJob = workflowJob(LIVE_MEDIA_RUNNER_IMAGE_WORKFLOW, "build"); const buildStep = workflowStep(buildJob, "Build and push live media runner image"); expect(imageDockerfile).toMatch(/^FROM ubuntu:24\.04$/m); expect(imageDockerfile).toContain("apt-get install -y --no-install-recommends"); for (const packageName of ["bash", "curl", "ffmpeg", "git", "openssh-client", "zstd"]) { expect(imageDockerfile).toContain(` ${packageName} \\`); } expect(imageDockerfile).toContain("rm -rf /var/lib/apt/lists/*"); expect(imageWorkflow).toContain(`- "${LIVE_MEDIA_RUNNER_DOCKERFILE}"`); expect(buildStep.with?.context).toBe(".github/images/live-media-runner"); expect(buildStep.with?.file).toBe(LIVE_MEDIA_RUNNER_DOCKERFILE); expect(buildStep.with?.tags).toContain(LIVE_MEDIA_RUNNER_IMAGE); expect(workflow).toContain("suite_id: native-live-extensions-media-audio"); expect(workflow).toContain("suite_id: native-live-extensions-media-music-google"); expect(workflow).toContain("suite_id: native-live-extensions-media-music-minimax"); expect(workflow).toContain("suite_id: native-live-extensions-media-video"); expect(workflow).toContain("suite_group: native-live-extensions-media-video"); expect(workflow).toContain("OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=google,minimax"); expect(workflow).toContain("OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=openai,openrouter,xai"); expect(workflow).toContain( "inputs.live_suite_filter == 'native-live-src-gateway-profiles-anthropic'", ); expect(workflow).toContain( "inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go'", ); expect(workflow).toContain("inputs.live_suite_filter == 'native-live-extensions-media-video'"); expect(workflow).not.toContain("needs_ffmpeg: true"); expect(retryHelper).toContain("OPENCLAW_LIVE_COMMAND_ATTEMPTS:-2"); expect(retryHelper).toContain("ECONNRESET"); expect(retryHelper).toContain("fetch failed"); expect(retryHelper).toContain("gateway request timeout"); expect(retryHelper).toContain("model idle timeout"); expect(retryHelper).toContain("OPENCLAW_LIVE_COMMAND_RATE_LIMIT_RETRY_DELAY_SECONDS:-60"); expect(retryHelper).toContain("Rate limit reached"); expect(retryHelper).toContain("tokens per min"); expect( workflow.match(/moonshot\) require_any Moonshot MOONSHOT_API_KEY KIMI_API_KEY ;;/gu), ).toHaveLength(2); }); it("pins DeepSeek live profiles to both current V4 model refs", () => { const deepSeek = workflowMatrixEntry( LIVE_E2E_WORKFLOW, "validate_live_provider_suites", "native-live-src-gateway-profiles-deepseek", ); const openCodeGo = workflowMatrixEntry( LIVE_E2E_WORKFLOW, "validate_live_provider_suites", "native-live-src-gateway-profiles-opencode-go-deepseek-glm", ); expect(deepSeek).toMatchObject({ advisory: true, command: "OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek OPENCLAW_LIVE_GATEWAY_MODELS=deepseek/deepseek-v4-flash,deepseek/deepseek-v4-pro node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles", profiles: "full", }); expect(openCodeGo.command).toContain( "OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/deepseek-v4-flash,opencode-go/deepseek-v4-pro", ); }); it("pins OpenCode Go MiMo live profiles to both current V2.5 model refs", () => { const mimo = workflowMatrixEntry( LIVE_E2E_WORKFLOW, "validate_live_provider_suites", "native-live-src-gateway-profiles-opencode-go-mimo", ); expect(mimo).toMatchObject({ advisory: true, command: "OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/mimo-v2.5,opencode-go/mimo-v2.5-pro node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles", profiles: "full", suite_group: "native-live-src-gateway-profiles-opencode-go", }); expect(mimo.command).not.toContain("opencode-go/mimo-v2-omni"); expect(mimo.command).not.toContain("opencode-go/mimo-v2-pro"); }); it("runs the fresh OpenAI API-key default without hard-coding a model filter", () => { const openaiDefault = workflowMatrixEntry( LIVE_E2E_WORKFLOW, "validate_live_provider_suites", "native-live-src-gateway-profiles-openai-api-default", ); expect(openaiDefault).toMatchObject({ profiles: "stable full" }); expect(openaiDefault.command).toContain("OPENCLAW_LIVE_GATEWAY_OPENAI_API_DEFAULT=1"); expect(openaiDefault.command).toContain("OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai"); expect(openaiDefault.command).not.toContain("OPENCLAW_LIVE_GATEWAY_MODELS="); }); it("runs Docker live harnesses from trusted helper scripts", () => { const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); const providerSuites = workflowJob(LIVE_E2E_WORKFLOW, "validate_live_docker_provider_suites"); const scenarios = readFileSync("scripts/lib/docker-e2e-scenarios.mts", "utf8"); const scheduler = readFileSync("scripts/test-docker-all.mts", "utf8"); const harness = readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8"); const codexLiveTest = readFileSync("src/gateway/gateway-codex-harness.live.test.ts", "utf8"); const liveDockerAuth = readFileSync("scripts/lib/live-docker-auth.sh", "utf8"); const sharedLiveScripts = [ readFileSync("scripts/test-live-models-docker.sh", "utf8"), readFileSync("scripts/test-live-gateway-models-docker.sh", "utf8"), readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8"), readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8"), readFileSync("scripts/test-live-subagent-announce-docker.sh", "utf8"), ]; const build = readFileSync("scripts/test-live-build-docker.sh", "utf8"); const stage = readFileSync("scripts/lib/live-docker-stage.sh", "utf8"); expect(workflow).toContain( 'run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh', ); expect(workflow).toContain( "command: OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.6-luna OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1", ); expect(workflow).toContain( "command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2", ); expect(workflow).toContain( 'command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-cli-backend-docker.sh', ); expect(workflow).toContain( 'command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-acp-bind-docker.sh', ); expect(workflow).toContain( 'command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-codex-harness-docker.sh', ); const codexCompatibility = workflowStep( providerSuites, "Resolve frozen Codex live compatibility", ); expect(codexCompatibility).toMatchObject({ id: "codex_compat", env: { OPENCLAW_FROZEN_CODEX_SUITE_ID: "${{ matrix.suite_id }}", OPENCLAW_FROZEN_TARGET_ROOT: "${{ github.workspace }}", OPENCLAW_SELECTED_SHA: "${{ needs.validate_selected_ref.outputs.selected_sha }}", OPENCLAW_WORKFLOW_SHA: "${{ needs.validate_selected_ref.outputs.workflow_sha }}", }, run: "node .release-harness/scripts/resolve-frozen-codex-live-suite.mjs", }); for (const stepName of [ "Validate live-test image artifact binding", "Download live-test image artifact", "Verify and load live-test image artifact", "Setup Node environment", "Hydrate live auth/profile inputs", "Log in to GHCR", "Configure suite-specific env", ]) { expect(workflowStep(providerSuites, stepName).if, stepName).toContain( "steps.codex_compat.outputs.run_lane != 'false'", ); } const runCodexSuite = providerSuites.steps?.find((candidate) => candidate.name?.startsWith("Run ${{ matrix.label }}"), ); expect(runCodexSuite?.if).toContain("steps.codex_compat.outputs.run_lane != 'false'"); for (const [model, thinking] of [ ["sol", "ultra"], ["terra", "ultra"], ["luna", "max"], ]) { expect(workflow).toContain( `OPENCLAW_LIVE_CODEX_HARNESS_TARGETS=openai/gpt-5.6-${model}=${thinking}`, ); } expect(workflow.match(/live-codex-harness\*-docker\)/gu)).toHaveLength(2); for (const suiteId of [ "native-live-src-gateway-profiles-openai-api-default", "native-live-src-gateway-profiles-openai-gpt56-ultra", "live-codex-harness-gpt56-sol-docker", "live-codex-harness-gpt56-terra-docker", "live-codex-harness-gpt56-luna-docker", "live-codex-harness-gpt56-docker", ]) { expect(workflow).toContain(`add_profile_suite ${suiteId} "stable full"`); } expect(codexLiveTest).toContain("command: `/model ${modelKey} --runtime codex`"); expect(codexLiveTest).toContain("thinkingLevel: CODEX_HARNESS_THINKING"); expect(workflow).toContain( 'command: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 20m bash .release-harness/scripts/test-live-subagent-announce-docker.sh', ); expect(scenarios).toContain("function liveDockerScriptCommand"); expect(scenarios).toContain("const LIVE_DOCKER_DEFAULT_HARNESS_DIR"); expect(scenarios).toContain("fileURLToPath(import.meta.url)"); expect(scenarios).toContain('? ".release-harness"'); expect(scenarios).toContain("process.env.OPENCLAW_DOCKER_E2E_REPO_ROOT"); expect(scenarios).toContain( 'harness="\\${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-${LIVE_DOCKER_DEFAULT_HARNESS_DIR}}"', ); expect(scenarios).not.toContain("harness=.release-harness"); expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-models-docker\.sh"/u); expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-gateway-models-docker\.sh"/u); expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-cli-backend-docker\.sh"/u); expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-acp-bind-docker\.sh"/u); expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-codex-harness-docker\.sh"/u); expect(scenarios).toMatch( /liveDockerScriptCommand\(\s*"e2e\/codex-npm-plugin-live-docker\.sh"/u, ); expect(scenarios).toMatch( /liveDockerScriptCommand\(\s*"test-live-subagent-announce-docker\.sh"/u, ); expect(scheduler).toContain("function liveDockerHarnessScriptCommand"); expect(scheduler).toContain("const LIVE_DOCKER_DEFAULT_HARNESS_DIR"); expect(scheduler).toContain('path.basename(SCRIPT_ROOT_DIR) === ".release-harness"'); expect(scheduler).toContain("ROOT_DIR !== SCRIPT_ROOT_DIR"); expect(scheduler).toContain( 'harness="\\${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-${LIVE_DOCKER_DEFAULT_HARNESS_DIR}}"', ); expect(scheduler).not.toContain("harness=.release-harness"); expect(scheduler).toContain('liveDockerHarnessScriptCommand("test-live-build-docker.sh")'); expect(liveDockerAuth).toContain("codex-cli | openai)"); expect(liveDockerAuth).toContain("openclaw_live_init_docker_run_args()"); expect(liveDockerAuth).toContain("openclaw_live_stage_profile_into_home()"); expect(liveDockerAuth).toContain("openclaw_live_chown_bind_dirs_for_container_user()"); expect(liveDockerAuth).toContain("openclaw_live_uses_managed_bind_dirs()"); expect(liveDockerAuth).toContain('openclaw_live_truthy "${OPENCLAW_TESTBOX:-}"'); expect(liveDockerAuth).toContain('[[ -n "${OPENCLAW_DOCKER_CACHE_HOME_DIR:-}" ]]'); expect(liveDockerAuth).toContain( 'timeout_value="${2:-${OPENCLAW_LIVE_DOCKER_RUN_TIMEOUT:-2700s}}"', ); expect(harness).toContain('source "$TRUSTED_HARNESS_DIR/scripts/lib/live-docker-auth.sh"'); expect(harness).not.toContain('source "$ROOT_DIR/scripts/lib/live-docker-auth.sh"'); expect(harness).toContain( 'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$ROOT_DIR" "$TRUSTED_HARNESS_DIR/scripts/test-live-build-docker.sh"', ); expect(harness).toContain( '-e OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR="${DOCKER_TRUSTED_HARNESS_CONTAINER_DIR}/scripts"', ); expect(harness).toContain('node --import tsx "$trusted_scripts_dir/prepare-codex-ci-auth.ts"'); expect(harness).toContain('source "$trusted_scripts_dir/lib/live-docker-stage.sh"'); for (const script of [harness, ...sharedLiveScripts]) { expect(script).toContain('source "$TRUSTED_HARNESS_DIR/scripts/lib/live-docker-auth.sh"'); expect(script).not.toContain('source "$ROOT_DIR/scripts/lib/live-docker-auth.sh"'); expect(script).toContain("openclaw_live_init_docker_run_args DOCKER_RUN_ARGS"); expect(script).toContain("DOCKER_RUN_ARGS+=(--rm -t \\"); expect(script).not.toContain("DOCKER_RUN_ARGS=(docker run --rm -t \\"); } expect(liveDockerAuth).toContain("openclaw_live_prepare_bind_dir_for_container_user"); for (const script of sharedLiveScripts) { expect(script).toContain("openclaw_live_uses_managed_bind_dirs"); expect(script).toContain( 'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$ROOT_DIR" "$TRUSTED_HARNESS_DIR/scripts/test-live-build-docker.sh"', ); expect(script).toContain('source "$trusted_scripts_dir/lib/live-docker-stage.sh"'); expect(script).toContain( '-e OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR="${DOCKER_TRUSTED_HARNESS_CONTAINER_DIR}/scripts"', ); expect(script).toContain( "openclaw_live_append_array DOCKER_RUN_ARGS DOCKER_TRUSTED_HARNESS_MOUNT", ); } for (const script of [ readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8"), readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8"), readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8"), ]) { expect(script).toContain("openclaw_live_run_setup_command"); expect(script).not.toContain('timeout --kill-after=30s "${OPENCLAW_LIVE_'); } expect(stage).toContain("elif command -v gtimeout >/dev/null 2>&1; then"); expect(stage).toContain('if "$timeout_bin" --kill-after=1s 1s true'); expect(stage).toContain('"$timeout_bin" --kill-after=30s "${timeout_seconds}s" "$@"'); expect(stage).toContain( 'echo "timeout command not found; cannot bound ${label} after ${timeout_seconds}s"', ); expect(readFileSync("scripts/test-live-models-docker.sh", "utf8")).toContain( "OPENCLAW_LIVE_MODELS_DOCKER_RUN_TIMEOUT:-2100s", ); expect(readFileSync("scripts/test-live-gateway-models-docker.sh", "utf8")).toContain( "OPENCLAW_LIVE_GATEWAY_DOCKER_RUN_TIMEOUT:-2100s", ); expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain( "OPENCLAW_LIVE_CLI_BACKEND_DOCKER_RUN_TIMEOUT:-2700s", ); expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain( 'CLI_SETUP_TIMEOUT_SECONDS="$(openclaw_live_read_positive_int_env OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS 180)"', ); expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain( '"${OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS:?missing live CLI backend setup timeout seconds}"', ); expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain( '"live CLI backend setup"', ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( "OPENCLAW_LIVE_ACP_BIND_DOCKER_RUN_TIMEOUT:-2700s", ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( 'ACP_SETUP_TIMEOUT_SECONDS="$(openclaw_live_read_positive_int_env OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS 180)"', ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( '"${OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS:?missing live ACP bind setup timeout seconds}"', ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( '-e OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS="$ACP_SETUP_TIMEOUT_SECONDS"', ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( '-e OPENCLAW_LIVE_ACP_BIND_REQUIRE_CRON="${OPENCLAW_LIVE_ACP_BIND_REQUIRE_CRON:-}"', ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( '"live ACP bind setup"', ); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( 'run_setup_command npm install -g "@anthropic-ai/claude-code@$claude_code_version"', ); const acpBindScript = readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8"); expect(acpBindScript).toContain( "OPENCLAW_LIVE_ACP_BIND_CLAUDE_AUTH must be one of: auto, api-key, subscription.", ); expect(acpBindScript).toContain( 'if [[ "$ACP_AGENT" == "claude" && "$CLAUDE_AUTH_MODE" == "subscription" ]]; then', ); expect(acpBindScript).toContain( "unset ANTHROPIC_API_KEY ANTHROPIC_API_KEY_OLD ANTHROPIC_API_TOKEN", ); expect(acpBindScript).toContain('-e CLAUDE_CODE_OAUTH_TOKEN="${CLAUDE_CODE_OAUTH_TOKEN:-}"'); expect(acpBindScript).not.toContain(" -e ANTHROPIC_API_KEY \\\n"); expect(workflow.match(/OPENCLAW_LIVE_ACP_BIND_CLAUDE_AUTH=subscription/g)).toHaveLength(2); expect(workflow.match(/OPENCLAW_LIVE_ACP_BIND_CLAUDE_AUTH=api-key/g)).toHaveLength(2); expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain( "run_setup_command bash -lc 'curl -fsSL https://app.factory.ai/cli | sh'", ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( "OPENCLAW_LIVE_CODEX_HARNESS_DOCKER_RUN_TIMEOUT:-$((2100 * CODEX_HARNESS_TARGET_COUNT))s", ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( 'CODEX_HARNESS_SETUP_TIMEOUT_SECONDS="$(openclaw_live_read_positive_int_env OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS 180)"', ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( '"${OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS:?missing live Codex harness setup timeout seconds}"', ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( '-e OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS="$CODEX_HARNESS_SETUP_TIMEOUT_SECONDS"', ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( '"live Codex harness setup"', ); expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain( 'run_setup_command npm install -g "$OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC"', ); expect(readFileSync("scripts/test-live-subagent-announce-docker.sh", "utf8")).toContain( "OPENCLAW_LIVE_SUBAGENT_DOCKER_RUN_TIMEOUT:-1200s", ); expect(build).toContain('ROOT_DIR="${OPENCLAW_LIVE_DOCKER_REPO_ROOT:-$SCRIPT_ROOT_DIR}"'); expect(build).toContain('source "$SCRIPT_ROOT_DIR/scripts/lib/docker-build.sh"'); expect(build).toContain('source "$SCRIPT_ROOT_DIR/scripts/lib/docker-e2e-container.sh"'); expect(build).toContain( 'DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_LIVE_DOCKER_PULL_TIMEOUT:-600s}}"', ); expect(build).toContain('LIVE_IMAGE_PULL_ATTEMPTS="${OPENCLAW_LIVE_DOCKER_PULL_ATTEMPTS:-3}"'); expect(build).toContain('docker_e2e_docker_cmd pull "$LIVE_IMAGE_NAME"'); expect(build).not.toContain('docker pull "$LIVE_IMAGE_NAME"'); expect(stage).toContain( 'local scripts_dir="${OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR:-/src/scripts}"', ); expect(stage).toContain('node --import tsx "$scripts_dir/live-docker-normalize-config.ts"'); }); it("fails Droid ACP Docker live proof when Factory auth is missing", () => { const script = readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8"); expect(script).toContain("openclaw_live_acp_bind_load_factory_api_key_from_profile"); expect(script).not.toContain('source "$PROFILE_FILE"'); expect(script.indexOf("openclaw_live_acp_bind_load_factory_api_key_from_profile")).toBeLessThan( script.indexOf('if [[ "$ACP_AGENT" == "droid" && -z "${FACTORY_API_KEY:-}" ]]; then'), ); expect(script).toContain( "ERROR: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container.", ); expect(script).not.toContain( "SKIP: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container.", ); expect(script).not.toMatch( /Droid Docker ACP bind requires FACTORY_API_KEY[\s\S]{0,160}(exit 0|continue)/u, ); }); it("plumbs live credentials through planned Docker E2E live lanes", () => { const reusableWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const scheduledWorkflow = readFileSync(SCHEDULED_LIVE_CHECKS_WORKFLOW, "utf8"); const packageAcceptanceWorkflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); const testboxWorkflow = readFileSync(CI_CHECK_TESTBOX_WORKFLOW, "utf8"); const hydrateScript = readFileSync(CI_HYDRATE_LIVE_AUTH_SCRIPT, "utf8"); const providerVerifier = readFileSync(VERIFY_PROVIDER_SECRETS_SCRIPT, "utf8"); const testboxProviderSecretKeys = [ "OPENAI_API_KEY", "OPENAI_BASE_URL", "ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD", "ANTHROPIC_API_TOKEN", "FACTORY_API_KEY", "BYTEPLUS_API_KEY", "CEREBRAS_API_KEY", "DEEPINFRA_API_KEY", "DASHSCOPE_API_KEY", "GROQ_API_KEY", "KIMI_API_KEY", "MODELSTUDIO_API_KEY", "MOONSHOT_API_KEY", "MISTRAL_API_KEY", "MINIMAX_API_KEY", "OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCLAW_LIVE_BROWSER_CDP_URL", "OPENCLAW_LIVE_SETUP_TOKEN", "OPENCLAW_LIVE_SETUP_TOKEN_MODEL", "OPENCLAW_LIVE_SETUP_TOKEN_PROFILE", "OPENCLAW_LIVE_SETUP_TOKEN_VALUE", "GEMINI_API_KEY", "GOOGLE_API_KEY", "OPENROUTER_API_KEY", "QWEN_API_KEY", "FAL_KEY", "RUNWAY_API_KEY", "DEEPGRAM_API_KEY", "TOGETHER_API_KEY", "VYDRA_API_KEY", "XAI_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY", "BYTEPLUS_ACCESS_KEY_ID", "BYTEPLUS_SECRET_ACCESS_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "OPENCLAW_CODEX_AUTH_JSON", "OPENCLAW_CODEX_CONFIG_TOML", "OPENCLAW_CLAUDE_JSON", "OPENCLAW_CLAUDE_CREDENTIALS_JSON", "OPENCLAW_CLAUDE_SETTINGS_JSON", "OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON", "OPENCLAW_GEMINI_SETTINGS_JSON", "FIREWORKS_API_KEY", ]; const githubBackedTestboxProviderSteps = [ workflowStep( workflowJob(CI_CHECK_TESTBOX_WORKFLOW, "check"), "Hydrate Testbox provider env helper", ), workflowStep( workflowJob(CI_CHECK_ARM_TESTBOX_WORKFLOW, "check-arm"), "Hydrate Testbox provider env helper", ), workflowStep( workflowJob(CI_BUILD_ARTIFACTS_TESTBOX_WORKFLOW, "build-artifacts"), "Hydrate Testbox provider env helper", ), workflowStep( workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate-github"), "Hydrate provider env helper", ), ]; expect(hydrateScript).toContain(" FACTORY_API_KEY \\"); expect(providerVerifier).toContain('url: "https://api.anthropic.com/v1/messages"'); expect(providerVerifier).toContain('model: "claude-haiku-4-5"'); expect(providerVerifier).toContain("validateResponse:"); expect(providerVerifier).not.toContain("ANTHROPIC_OAUTH_TOKEN"); for (const workflow of [ reusableWorkflow, releaseChecksWorkflow, scheduledWorkflow, packageAcceptanceWorkflow, testboxWorkflow, ]) { expect(workflow).toContain("FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}"); } for (const step of githubBackedTestboxProviderSteps) { for (const key of testboxProviderSecretKeys) { expect(step.env?.[key]).toBe("${{ secrets." + key + " }}"); } } expect(reusableWorkflow).toContain("FACTORY_API_KEY:\n required: false"); expect(packageAcceptanceWorkflow).toContain("FACTORY_API_KEY:\n required: false"); expectTextToIncludeAll(reusableWorkflow, [ 'if [[ "$credentials" == *",openai,"* ]]; then', "require_any OpenAI OPENAI_API_KEY", 'if [[ "$credentials" == *",codex,"* ]]; then', "require_any Codex OPENCLAW_CODEX_AUTH_JSON", 'if [[ "$credentials" == *",gemini,"* ]]; then', "require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON", 'if [[ "$credentials" == *",opencode,"* ]]; then', "require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY", ]); expect(reusableWorkflow.match(/OPENCLAW_LIVE_CLI_BACKEND_AUTH=subscription/g)).toHaveLength(2); expect( reusableWorkflow.match( /if \[\[ -n "\$\{OPENCLAW_CLAUDE_CREDENTIALS_JSON:-\}" \|\| -n "\$\{CLAUDE_CODE_OAUTH_TOKEN:-\}" \]\]; then/g, ), ).toHaveLength(4); }); it("finalizes dispatched Testbox delegation even when setup or the remote command fails", () => { const workflow = readFileSync(CI_CHECK_TESTBOX_WORKFLOW, "utf8"); const checkTestboxJob = workflowJob(CI_CHECK_TESTBOX_WORKFLOW, "check"); const setupNodeStep = workflowStep(checkTestboxJob, "Setup Node environment"); const runTestboxStep = workflowStep(checkTestboxJob, "Run Testbox"); const closeTestboxSshStep = workflowStep(checkTestboxJob, "Close Testbox SSH sessions"); const setupNodeWith = setupNodeStep.with ?? {}; const checkTestboxSteps = checkTestboxJob.steps ?? []; const runArmTestboxStep = workflowStep( workflowJob(CI_CHECK_ARM_TESTBOX_WORKFLOW, "check-arm"), "Run Testbox", ); const runBuildArtifactsTestboxStep = workflowStep( workflowJob(CI_BUILD_ARTIFACTS_TESTBOX_WORKFLOW, "build-artifacts"), "Run Testbox", ); const runWindowsTestboxStep = workflowStep( workflowJob(WINDOWS_BLACKSMITH_TESTBOX_WORKFLOW, "windows"), "Run Testbox", ); expect(workflow).not.toContain('PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"'); expect(workflow).not.toContain("PNPM_CONFIG_MODULES_DIR"); expect(workflow).not.toContain("PNPM_CONFIG_VIRTUAL_STORE_DIR"); expect(setupNodeWith["sticky-disk"]).toBe( "${{ github.event_name == 'workflow_dispatch' && 'true' || 'false' }}", ); expect(setupNodeWith["use-actions-cache"]).toBe( "${{ github.event_name == 'workflow_dispatch' && 'false' || 'true' }}", ); expect(checkTestboxJob["timeout-minutes"]).toBe( "${{ fromJSON(inputs.timeout_minutes || '120') }}", ); expect(runTestboxStep.uses).toContain("useblacksmith/run-testbox@"); expect(runTestboxStep.if).toBe("github.event_name == 'workflow_dispatch' && always()"); expect(closeTestboxSshStep.if).toBe("github.event_name == 'workflow_dispatch' && always()"); expect(closeTestboxSshStep.run).toContain( `sudo sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }'`, ); expect(closeTestboxSshStep.run).toContain( 'ss -K state established \\\n "( sport = :${runner_ssh_local_port} )"', ); expect(checkTestboxSteps.indexOf(closeTestboxSshStep)).toBe( checkTestboxSteps.indexOf(runTestboxStep) + 1, ); expect(runArmTestboxStep.if).toBe("always()"); expect(runBuildArtifactsTestboxStep.if).toBe("always()"); expect(runWindowsTestboxStep.if).toBe("always()"); expect(runTestboxStep["continue-on-error"]).toBeUndefined(); }); it("allows the Telegram lane to run from reusable package acceptance artifacts", () => { const workflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8"); expect(workflow).toContain("workflow_call:"); expect(workflow).toContain("package_artifact_name:"); expect(workflow).toContain("Download package-under-test artifact"); expect(workflow).toContain("harness_ref:"); expect(workflow).toContain("ref: ${{ inputs.harness_ref || github.sha }}"); expect(workflow).toContain("OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ"); expect(workflow).toContain("provider_mode:"); expect(workflow).toContain("provider_mode must be mock-openai or live-frontier"); expect(workflow).toContain("run_package_telegram_e2e:"); }); it.each(["stable", "full"])( "rejects Package Acceptance Telegram deferral for direct %s release checks", (releaseProfile) => { const { result } = runReleaseChecksInputValidation(releaseProfile, "true"); expect(result.status).toBe(1); expect(result.stderr).toContain( "skip_package_telegram_e2e is allowed only for release_profile=beta.", ); }, ); it.each(["stable", "full"])( "rejects Package Acceptance Telegram deferral for umbrella %s validation", (releaseProfile) => { const result = runFullReleaseInputValidation(releaseProfile, "true"); expect(result.status).toBe(1); expect(result.stderr).toContain( "skip_package_telegram_e2e is allowed only for release_profile=beta.", ); }, ); it("allows Package Acceptance Telegram deferral only for beta validation", () => { const direct = runReleaseChecksInputValidation("beta", "true"); const umbrella = runFullReleaseInputValidation("beta", "true"); expect(direct.result.status, direct.result.stderr).toBe(0); expect(readFileSync(direct.outputPath, "utf8")).toContain("skip_package_telegram_e2e=true"); expect(umbrella.status, umbrella.stderr).toBe(0); }); it.each(["stable", "full"])( "preserves normal %s validation when Telegram deferral is false", (releaseProfile) => { const direct = runReleaseChecksInputValidation(releaseProfile, "false"); const umbrella = runFullReleaseInputValidation(releaseProfile, "false"); expect(direct.result.status, direct.result.stderr).toBe(0); expect(readFileSync(direct.outputPath, "utf8")).toContain("skip_package_telegram_e2e=false"); expect(umbrella.status, umbrella.stderr).toBe(0); }, ); it.each([ ["beta", "all", "false", "false", "false"], ["beta", "all", "true", "true", "true"], ["stable", "all", "false", "true", "true"], ["full", "all", "false", "true", "true"], ["beta", "qa", "false", "false", "true"], ["beta", "qa-live", "false", "false", "true"], ])( "normalizes QA-live scheduling for profile=%s group=%s soak=%s", (releaseProfile, rerunGroup, runReleaseSoak, expectedSoak, expectedScheduled) => { const { outputPath, result } = runReleaseChecksInputValidation( releaseProfile, "false", rerunGroup, runReleaseSoak, ); expect(result.status, result.stderr).toBe(0); const output = readFileSync(outputPath, "utf8"); expect(output).toContain(`run_release_soak=${expectedSoak}\n`); expect(output).toContain(`qa_live_scheduled=${expectedScheduled}\n`); }, ); it("schedules only the selected QA-live lane for an all-group QA filter", () => { const { outputPath, result } = runReleaseChecksInputValidation( "beta", "false", "all", "false", "qa-live-telegram", ); expect(result.status, result.stderr).toBe(0); const output = readFileSync(outputPath, "utf8"); expect(output).toContain("qa_live_scheduled=true\n"); expect(output).toContain("qa_live_telegram_enabled=true\n"); for (const lane of ["matrix", "buzz", "discord", "whatsapp", "slack"]) { expect(output).toContain(`qa_live_${lane}_enabled=false\n`); } }); it("does not schedule QA-live for an all-group repo live filter without soak", () => { const { outputPath, result } = runReleaseChecksInputValidation( "beta", "false", "all", "false", "repo-e2e", ); expect(result.status, result.stderr).toBe(0); const output = readFileSync(outputPath, "utf8"); expect(output).toContain("qa_live_scheduled=false\n"); expect(output).toContain("repo_live_suite_filter=repo-e2e\n"); }); it("does not let a QA-live filter override an unrelated rerun group", () => { const { outputPath, result } = runReleaseChecksInputValidation( "beta", "false", "install-smoke", "false", "qa-live-telegram", ); expect(result.status, result.stderr).toBe(0); const output = readFileSync(outputPath, "utf8"); expect(output).toContain("qa_live_scheduled=false\n"); expect(output).toContain("qa_live_telegram_enabled=true\n"); }); it("summarizes Telegram deferral only when Package Acceptance is scheduled", () => { const scheduled = runFullReleaseTargetSummary("release-checks", "true"); const unrelated = runFullReleaseTargetSummary("ci", "true"); expect(scheduled.result.status, scheduled.result.stderr).toBe(0); expect(scheduled.summary).toContain( "Package Telegram E2E: deferred by `skip_package_telegram_e2e`", ); expect(unrelated.result.status, unrelated.result.stderr).toBe(0); expect(unrelated.summary).toContain("Package Telegram E2E: skipped by rerun group"); expect(unrelated.summary).not.toContain( "Package Telegram E2E: deferred by `skip_package_telegram_e2e`", ); }); it("includes package acceptance in release checks", () => { const workflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const packageAcceptanceWorkflow = parse(readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8")) as { on?: { workflow_call?: { inputs?: Record }; }; }; const packageAcceptanceJob = workflowJob( RELEASE_CHECKS_WORKFLOW, "package_acceptance_release_checks", ); const releaseChecksTargetSummary = workflowStep( workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target"), "Summarize validated ref", ); const dockerAcceptanceJob = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "docker_acceptance"); expect(workflow).toContain("package_acceptance_release_checks:"); expect(packageAcceptanceWorkflow.on?.workflow_call?.inputs).toHaveProperty( "allow_frozen_target_scenario_omissions", ); expect(packageAcceptanceJob.with).toMatchObject({ allow_frozen_target_scenario_omissions: "${{ inputs.allow_frozen_target_scenario_omissions }}", artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}", artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}", artifact_name: "${{ needs.prepare_release_package.outputs.artifact_name }}", artifact_run_attempt: "${{ needs.prepare_release_package.outputs.artifact_run_attempt }}", artifact_run_id: "${{ needs.prepare_release_package.outputs.artifact_run_id }}", package_file_name: "${{ needs.prepare_release_package.outputs.package_file_name }}", package_sha256: "${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.release_package_spec == '') && needs.prepare_release_package.outputs.package_sha256 || '' }}", package_source_sha: "${{ needs.prepare_release_package.outputs.source_sha }}", package_version: "${{ needs.prepare_release_package.outputs.package_version }}", suite_profile: "custom", }); expect(releaseChecksTargetSummary.env).toMatchObject({ SKIP_PACKAGE_TELEGRAM_E2E: "${{ steps.inputs.outputs.skip_package_telegram_e2e }}", }); expect(releaseChecksTargetSummary.run).toContain("Package Acceptance Telegram E2E deferred:"); expect(dockerAcceptanceJob.with).toMatchObject({ allow_frozen_target_scenario_omissions: "${{ inputs.allow_frozen_target_scenario_omissions || false }}", enable_prepublish_plugin_registry: '${{ contains(fromJSON(\'["artifact","ref"]\'), inputs.source) }}', prepublish_plugin_registry_manifest_sha256: "${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}", }); expect(workflow).toContain( "candidate_artifact_json cannot be combined with release package specs.", ); expect(workflow).toContain( "live_repo_e2e_release_checks:\n name: Run repo/live E2E validation\n needs: [resolve_target]", ); expect(workflow).toContain( "docker_e2e_release_checks:\n name: Run Docker release-path validation\n needs: [resolve_target, prepare_release_package]", ); expect(workflow).toContain("include_release_path_suites: false"); expect(workflow).toContain("include_release_path_suites: true"); expect(workflow).toContain( "allow_frozen_target_scenario_omissions: ${{ inputs.allow_frozen_target_scenario_omissions }}", ); expect(workflow).toContain("uses: ./.github/workflows/package-acceptance.yml"); expect(workflow).toContain( "source: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec != '' || needs.resolve_target.outputs.release_package_spec != '') && 'npm' || 'artifact' }}", ); expect(workflow).toContain( "package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || needs.resolve_target.outputs.release_package_spec || 'openclaw@beta' }}", ); expect(workflow).toContain(".artifacts/docker-e2e-package/package-candidate.json"); expect(workflow).toContain( "artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}", ); expect(workflow).toContain( "package_sha256: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.release_package_spec == '') && needs.prepare_release_package.outputs.package_sha256 || '' }}", ); expect(workflow).toContain("suite_profile: custom"); expect(String(packageAcceptanceJob.with?.docker_lanes ?? "").split(/\s+/u)).toEqual([ "release-typed-onboarding", "doctor-switch", "update-channel-switch", "skill-install", "update-corrupt-plugin", "upgrade-survivor", "published-upgrade-survivor", "root-managed-vps-upgrade", "update-restart-auth", "plugins-offline", "plugin-update", "plugin-binding-command-escape", ]); expect(workflow).toContain( "published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}", ); expect(workflow).toContain( "published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}", ); expect(readWorkflow(RELEASE_CHECKS_WORKFLOW).on?.workflow_dispatch?.inputs).toMatchObject({ skip_package_telegram_e2e: { default: false, type: "boolean", }, }); expect(packageAcceptanceJob.with).toMatchObject({ telegram_mode: "${{ needs.resolve_target.outputs.skip_package_telegram_e2e == 'true' && 'none' || 'mock-openai' }}", }); expect(packageAcceptanceJob.with).toMatchObject({ telegram_advisory: "${{ needs.resolve_target.outputs.release_profile == 'beta' }}", }); expect(workflow).not.toContain("telegram_scenarios:"); expect(workflow).toContain("ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}"); expect(workflow).toContain("ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}"); expect(workflow).toContain( "OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}", ); expect(workflow).toContain( "OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}", ); expect(workflow).toContain("rerun_group:"); expect(workflow).toContain("live_suite_filter:"); expect(workflow).toContain("repo_live_suite_filter:"); expect(workflow).toContain('repo_filter_tokens+=("$token")'); expect(workflow).toContain( 'repo_live_suite_filter="$(IFS=,; printf \'%s\' "${repo_filter_tokens[*]-}")"', ); expect(workflow).toContain("cross_os_suite_filter:"); expect(workflow).toContain("advisory: false"); expect(workflow).toContain( "suite_filter: ${{ needs.resolve_target.outputs.cross_os_suite_filter }}", ); expect(workflow).toContain( "live_suite_filter: ${{ needs.resolve_target.outputs.repo_live_suite_filter }}", ); expect(workflow).toContain( "if: needs.resolve_target.outputs.cross_os_scheduled == 'true' || needs.resolve_target.outputs.docker_release_scheduled == 'true' || needs.resolve_target.outputs.rerun_group == 'package'", ); expect(workflow).toContain( "if: needs.resolve_target.outputs.docker_release_scheduled == 'true'", ); expect(workflow).toContain( 'if [[ "$release_profile" == "stable" || "$release_profile" == "full" ]]; then\n run_release_soak=true', ); expect(workflow).toContain("forced on for release_profile=stable and full"); expect(workflow).toContain("- live-e2e"); expect(workflow).toContain("- qa-live"); expect(workflow).toContain("disabled_required_lanes=()"); expect(workflow).toContain("live_suite_filter explicitly requested disabled QA live lane(s)"); expect(workflow).toContain("OPENCLAW_RELEASE_QA_*_LIVE_CI_ENABLED"); expect(workflow).not.toContain( "QA release-check lanes are advisory and do not block release validation.", ); }); it("prefers fresh Claude OAuth credentials for direct Anthropic live provider lanes", () => { const hydrateScript = readFileSync(CI_HYDRATE_LIVE_AUTH_SCRIPT, "utf8"); expect(hydrateScript).toContain(" ANTHROPIC_OAUTH_TOKEN \\"); expect(hydrateScript).toContain("access_token=\"$(jq -r '.claudeAiOauth.accessToken // empty'"); expect(hydrateScript).toContain('export ANTHROPIC_OAUTH_TOKEN="$access_token"'); expect(hydrateScript).toContain('local min_remaining_ms="$(( 90 * 60 * 1000 ))"'); expect(hydrateScript).toContain( 'printf \'ANTHROPIC_OAUTH_TOKEN=%s\\n\' "$access_token" >>"$GITHUB_ENV"', ); for (const jobName of [ "validate_live_models_docker", "validate_live_models_docker_targeted", "validate_live_provider_suites", ]) { expect(workflowJob(LIVE_E2E_WORKFLOW, jobName).env?.ANTHROPIC_OAUTH_TOKEN).toBe( "${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}", ); } }); it("routes release Matrix through the QA Lab selector", () => { const releaseWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const releaseTelegramWorkflow = readFileSync(RELEASE_TELEGRAM_QA_WORKFLOW, "utf8"); const qaWorkflow = readFileSync(".github/workflows/qa-live-transports-convex.yml", "utf8"); const releaseJob = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_release_checks"); expect(releaseJob.uses).toBe("./.github/workflows/qa-live-transports-convex.yml"); expect(releaseJob.secrets).toEqual({ OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}", OPENCLAW_QA_CONVEX_SECRET_CI: "${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}", OPENCLAW_QA_CONVEX_SITE_URL: "${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}", }); expect(releaseJob.permissions).toEqual({ contents: "read", "pull-requests": "read" }); expect(releaseJob.if).toBe( "needs.resolve_target.outputs.qa_live_scheduled == 'true' && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true'", ); expect(releaseJob.with).toMatchObject({ expected_sha: "${{ needs.resolve_target.outputs.revision }}", fail_fast: "${{ fromJSON(needs.resolve_target.outputs.fail_fast) }}", run_matrix: true, }); for (const lane of ["mock_parity", "buzz", "telegram", "discord", "whatsapp", "slack"]) { expect(releaseJob.with?.[`run_${lane}`]).toBeUndefined(); } expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_mock_parity").if).toBe( "inputs.expected_sha == '' || inputs.run_mock_parity", ); expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_matrix").if).toBe( "inputs.expected_sha == '' || inputs.run_matrix", ); for (const channel of ["telegram", "discord", "whatsapp", "slack"]) { expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, `run_live_${channel}`).if).toBe( `inputs.expected_sha == '' || inputs.run_${channel}`, ); } expect(releaseWorkflow).not.toContain("qa_live_matrix_release_checks"); expect(releaseWorkflow).not.toContain("Run QA Lab live Matrix lane"); expect(releaseWorkflow).not.toContain("pnpm openclaw qa matrix"); expect(qaWorkflow).toContain("pnpm openclaw qa matrix"); expect(qaWorkflow).toContain('if [[ "$FAIL_FAST" == "true" ]]'); expect(qaWorkflow).toContain('trusted_reason="repository-branch"'); expect(qaWorkflow).toContain('"${selected_revision}" != "${EXPECTED_SHA}"'); expect(qaWorkflow).toContain("EXPECTED_SHA: ${{ inputs.expected_sha }}"); expect( workflowStep( workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "authorize_actor"), "Require maintainer-level repository access", ).env?.EXPECTED_SHA, ).toBe("${{ inputs.expected_sha }}"); expect(qaWorkflow).toContain('(process.env.EXPECTED_SHA ?? "") !== ""'); expect(qaWorkflow).not.toContain('"${{ inputs.expected_sha }}" !== ""'); expect(qaWorkflow).toContain('if [[ -n "${EXPECTED_SHA}" ]]; then'); const matrixJob = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_matrix"); expect(matrixJob["timeout-minutes"]).toBe(90); expect(workflowStep(matrixJob, "Run Matrix live lane").run).toContain( "--provider-mode mock-openai", ); const matrixSpecificInputs = Object.keys( readWorkflow(QA_LIVE_TRANSPORTS_WORKFLOW).on?.workflow_call?.inputs ?? {}, ).filter((input) => input.startsWith("matrix_")); expect(matrixSpecificInputs).toEqual([]); expect(workflowStep(matrixJob, "Upload Matrix QA artifacts").with?.name).toBe( "${{ inputs.expected_sha != '' && format('release-qa-live-matrix-{0}', inputs.expected_sha) || format('qa-live-matrix-{0}-{1}', github.run_id, github.run_attempt) }}", ); expect(matrixJob["continue-on-error"]).toBeUndefined(); expect(matrixJob.strategy).toBeUndefined(); expect(workflowStep(matrixJob, "Run Matrix live lane").env).toEqual({ FAIL_FAST: "${{ inputs.fail_fast }}", OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}", OPENCLAW_LIVE_OPENAI_KEY: "${{ secrets.OPENAI_API_KEY }}", OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1", }); expect(releaseTelegramWorkflow).toContain( 'echo "Telegram live lane failed on attempt ${attempt}; retrying once..." >&2', ); }); it("routes release Buzz through the QA Lab selector", () => { const releaseJob = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_buzz_release_checks"); expect(releaseJob.uses).toBe("./.github/workflows/qa-live-transports-convex.yml"); expect(releaseJob.secrets).toEqual({ OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}", OPENCLAW_QA_CONVEX_SECRET_CI: "${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}", OPENCLAW_QA_CONVEX_SITE_URL: "${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}", }); expect(releaseJob.permissions).toEqual({ contents: "read", "pull-requests": "read" }); expect(releaseJob.if).toBe( "needs.resolve_target.outputs.qa_live_scheduled == 'true' && needs.resolve_target.outputs.qa_live_buzz_enabled == 'true'", ); expect(releaseJob.with).toMatchObject({ buzz_scenario: "channel-canary,channel-mention-gating", expected_sha: "${{ needs.resolve_target.outputs.revision }}", run_buzz: true, }); const buzzJob = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_buzz"); expect(buzzJob.if).toBe("inputs.run_buzz"); const resolveBuzz = workflowStep(buzzJob, "Resolve Buzz QA runner"); expect(resolveBuzz.run).toContain('runner?.commandName === "buzz"'); expect(resolveBuzz.run).toContain("selected ref does not declare the Buzz QA runner"); expect(workflowStep(buzzJob, "Validate required Buzz QA credential env").if).toBe( "steps.resolve_buzz.outputs.available == 'true'", ); expect(workflowStep(buzzJob, "Build private QA runtime").if).toBe( "steps.resolve_buzz.outputs.available == 'true'", ); expect(workflowStep(buzzJob, "Run Buzz live lane").if).toBe( "steps.resolve_buzz.outputs.available == 'true'", ); expect(workflowStep(buzzJob, "Upload Buzz QA artifacts").with?.name).toBe( "${{ inputs.expected_sha != '' && format('release-qa-live-buzz-{0}-{1}', inputs.expected_sha, github.run_attempt) || format('qa-live-buzz-{0}-{1}', github.run_id, github.run_attempt) }}", ); expect(workflowStep(buzzJob, "Upload Buzz QA artifacts").with?.path).toBe( "${{ steps.resolve_buzz.outputs.output_dir }}", ); const requireBuzz = workflowStep(buzzJob, "Require requested Buzz QA runner"); expect(requireBuzz.if).toBe( "always() && inputs.expected_sha == '' && steps.resolve_buzz.outcome == 'success' && steps.resolve_buzz.outputs.available != 'true'", ); expect(requireBuzz.run).toContain( "The selected ref does not declare the requested Buzz QA runner.", ); expect(requireBuzz.run).toContain("exit 1"); }); it("runs QA-live on soak or explicit QA groups, not beta all by default", () => { const workflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const resolveTarget = workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target"); const liveJobs = [ ["qa_live_release_checks", "qa_live_matrix_enabled"], ["qa_live_buzz_release_checks", "qa_live_buzz_enabled"], ["qa_live_telegram_release_checks", "qa_live_telegram_enabled"], ["qa_live_discord_release_checks", "qa_live_discord_enabled"], ["qa_live_whatsapp_release_checks", "qa_live_whatsapp_enabled"], ["qa_live_slack_release_checks", "qa_live_slack_enabled"], ] as const; const selection = "needs.resolve_target.outputs.qa_live_scheduled == 'true'"; expect(resolveTarget.outputs?.qa_live_scheduled).toBe( "${{ steps.inputs.outputs.qa_live_scheduled }}", ); for (const [jobName, enabledOutput] of liveJobs) { expect(workflowJob(RELEASE_CHECKS_WORKFLOW, jobName).if).toBe( `${selection} && needs.resolve_target.outputs.${enabledOutput} == 'true'`, ); } const verifyStep = workflowStep( workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"), "Verify release check results", ); expect(verifyStep.env?.QA_LIVE_TELEGRAM_SELECTED).toBe( `\${{ ${selection} && needs.resolve_target.outputs.qa_live_telegram_enabled == 'true' }}`, ); const kickoffSummary = workflowStep(resolveTarget, "Summarize validated ref"); expect(kickoffSummary.env?.QA_LIVE_SCHEDULED).toBe( "${{ steps.inputs.outputs.qa_live_scheduled }}", ); expect(kickoffSummary.run).toContain("- QA-live scheduled:"); expect(kickoffSummary.run).toContain("- QA-live lane eligibility:"); expect(kickoffSummary.run).not.toContain("- QA live lanes:"); expect(workflow).not.toContain('contains(fromJSON(\'["qa","qa-live"]\')'); }); it("runs live transport lanes nightly while release checks stay gated", () => { const releaseWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const qaWorkflow = readFileSync(QA_LIVE_TRANSPORTS_WORKFLOW, "utf8"); for (const channel of ["DISCORD", "WHATSAPP", "SLACK"]) { const lower = channel.toLowerCase(); expect(releaseWorkflow).toContain( `RELEASE_QA_${channel}_LIVE_CI_ENABLED: \${{ vars.OPENCLAW_RELEASE_QA_${channel}_LIVE_CI_ENABLED || 'false' }}`, ); expect(releaseWorkflow).toContain(`qa_live_${lower}_enabled="$qa_live_${lower}_ci_enabled"`); expect(releaseWorkflow).toContain( `needs.resolve_target.outputs.qa_live_${lower}_enabled == 'true'`, ); expect(releaseWorkflow).not.toContain( `vars.OPENCLAW_RELEASE_QA_${channel}_LIVE_CI_ENABLED == 'true'`, ); expect(qaWorkflow).not.toContain(`OPENCLAW_QA_${channel}_LIVE_CI_ENABLED`); } }); it("requires QA live evidence artifacts when lanes run", () => { const cases = [ ["run_mock_parity", "Upload parity artifacts", "always()"], [ "run_live_runtime_token_efficiency", "Upload live runtime token-efficiency artifacts", "always() && steps.run_lane.outputs.output_dir != ''", ], ["run_live_matrix", "Upload Matrix QA artifacts", "always()"], ["run_live_buzz", "Upload Buzz QA artifacts", "always()"], ["run_live_telegram", "Upload Telegram QA artifacts", "always()"], ["run_live_discord", "Upload Discord QA artifacts", "always()"], ["run_live_whatsapp", "Upload WhatsApp QA artifacts", "always()"], ["run_live_slack", "Upload Slack QA artifacts", "always()"], ] as const; for (const [jobName, stepName, uploadCondition] of cases) { const uploadStep = workflowStep(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, jobName), stepName); expect(uploadStep.if, jobName).toBe(uploadCondition); expect(uploadStep.with?.["if-no-files-found"], jobName).toBe("error"); } }); it("preserves the primary runtime token-efficiency failure", () => { const job = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_runtime_token_efficiency"); const runStep = workflowStep(job, "Run live core runtime-pair lane"); const reportStep = workflowStep(job, "Generate live runtime token-efficiency report"); expect(runStep.run).toContain('mkdir -p "${output_dir}"'); expect(runStep.run).toContain( "printf 'Runtime token-efficiency lane started.\\n' > \"${output_dir}/runtime-lane-started.txt\"", ); expect(reportStep.if).toBe("steps.run_lane.outcome == 'success'"); }); it("runs the core gateway restart pair on its pinned live model", () => { const job = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_runtime_token_efficiency"); const credentialStep = workflowStep(job, "Validate required QA credential env"); const runStep = workflowStep(job, "Run pinned GPT-5.4 gateway restart runtime pair"); const stepNames = job.steps?.map((step) => step.name) ?? []; expect(credentialStep.run).toContain('if [[ -z "${OPENAI_API_KEY:-}" ]]'); expect(credentialStep.run).toContain("exit 1"); expect(runStep.run).toContain("--provider-mode live-frontier"); expect(runStep.run).toContain("--scenario gateway-restart-multi-live"); expect(runStep.run).toContain("--model openai/gpt-5.4"); expect(runStep.run).toContain("--alt-model openai/gpt-5.4"); expect(runStep.run).toContain("--runtime-pair openclaw,codex"); expect(runStep.run).toContain( "steps.run_lane.outputs.output_dir }}/gateway-restart-gpt-5.4-runtime-pair", ); expect(runStep.run).not.toContain("--allow-failures"); expect(stepNames.indexOf("Run pinned GPT-5.4 gateway restart runtime pair")).toBeLessThan( stepNames.indexOf("Generate live runtime token-efficiency report"), ); }); it("requires release-check QA evidence artifacts when lanes run", () => { const cases = [ ["qa_lab_parity_lane_release_checks", "Upload parity lane artifacts"], ["qa_lab_parity_report_release_checks", "Upload parity artifacts"], ["qa_lab_runtime_parity_release_checks", "Upload runtime parity artifacts"], ["qa_live_discord_release_checks", "Upload Discord QA artifacts"], ["qa_live_whatsapp_release_checks", "Upload WhatsApp QA artifacts"], ["qa_live_slack_release_checks", "Upload Slack QA artifacts"], ] as const; for (const [jobName, stepName] of cases) { const uploadStep = workflowStep(workflowJob(RELEASE_CHECKS_WORKFLOW, jobName), stepName); expect(uploadStep.if, jobName).toBe("always()"); expect(uploadStep.uses, jobName).toBe(UPLOAD_ARTIFACT_V7); expect(uploadStep.with?.["if-no-files-found"], jobName).toBe("error"); } const telegramUpload = workflowStep( workflowJob(RELEASE_TELEGRAM_QA_WORKFLOW, "run_telegram"), "Upload Telegram QA artifacts", ); expect(telegramUpload.if).toContain("always()"); expect(telegramUpload.uses).toBe(UPLOAD_ARTIFACT_V7); expect(telegramUpload.with?.["if-no-files-found"]).toBe("error"); const runtimeCoverageUpload = workflowStep( workflowJob(RELEASE_CHECKS_WORKFLOW, "runtime_tool_coverage_release_checks"), "Upload runtime tool coverage artifacts", ); expect(runtimeCoverageUpload.if).toContain("always()"); expect(runtimeCoverageUpload.if).toContain( "steps.verify_runtime_parity_status.outputs.ready == 'true'", ); expect(runtimeCoverageUpload.uses).toBe(UPLOAD_ARTIFACT_V7); expect(runtimeCoverageUpload.with?.["if-no-files-found"]).toBe("error"); }); it("runs canonical runtime-pair lanes in parallel and preserves one gate", () => { const laneJob = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_lab_runtime_pair_lane_release_checks"); const collectorJob = workflowJob( RELEASE_CHECKS_WORKFLOW, "qa_lab_runtime_parity_release_checks", ); expect(laneJob.strategy?.["fail-fast"]).toBe(false); expect(laneJob.strategy?.matrix?.lane).toContain('["core","soak"]'); expect(laneJob.strategy?.matrix?.lane).toContain('["core"]'); const runtimePairRun = workflowStep(laneJob, "Run runtime-pair lane").run; expect(runtimePairRun).toContain('--runtime-pair-lane "$RUNTIME_PAIR_LANE"'); expect(runtimePairRun).toContain("--runtime-parity-tier standard"); expect(runtimePairRun).toContain("--runtime-parity-tier soak"); expect(runtimePairRun).toContain("Frozen candidate cannot select runtime-pair lane"); expect(workflowStep(laneJob, "Run runtime-pair lane")["continue-on-error"]).toBe(true); const runtimePairValidation = workflowStep(laneJob, "Validate runtime-pair lane").run; expect(runtimePairValidation).toContain("validator_args+=(--require-explicit-gap)"); expect(runtimePairValidation).toContain('--target-sha "$RELEASE_CHECK_TARGET_SHA"'); expect(runtimePairValidation).toContain('--lane "$RUNTIME_PAIR_LANE"'); expect(runtimePairValidation).toContain( 'node --import tsx trusted-suite-validator/scripts/validate-qa-runtime-pair-summary.mts "${validator_args[@]}"', ); const coreRestartRun = workflowStep(laneJob, "Run OpenClaw core restart proof").run; expect(coreRestartRun).toContain("--scenario gateway-restart-inflight-run"); expect(coreRestartRun).toContain('--output-dir ".artifacts/qa-e2e/openclaw-core-restart"'); const trustedValidatorCheckout = workflowStep( laneJob, "Checkout trusted validator after candidate suite", ); expect(trustedValidatorCheckout.with).toMatchObject({ ref: "${{ github.sha }}", path: "trusted-suite-validator", "persist-credentials": false, }); const runtimePairStepNames = (laneJob.steps ?? []).map((step) => step.name); expect(runtimePairStepNames.indexOf("Run runtime-pair lane")).toBeLessThan( runtimePairStepNames.indexOf("Checkout trusted validator after candidate suite"), ); expect(workflowStep(laneJob, "Generate runtime-pair lane report")["continue-on-error"]).toBe( true, ); const runtimePairReport = workflowStep(laneJob, "Validate runtime-pair lane report").run; expect(runtimePairReport).toContain( '--report-summary "$report_dir/qa-runtime-parity-summary.json"', ); expect(runtimePairReport).toContain( '--report-markdown "$report_dir/qa-runtime-parity-report.md"', ); expect(runtimePairReport).toContain("validator_args+=(--require-explicit-gap)"); expect(runtimePairReport).toContain( "node --import tsx trusted-report-validator/scripts/validate-qa-runtime-pair-summary.mts", ); expect(runtimePairStepNames.indexOf("Generate runtime-pair lane report")).toBeLessThan( runtimePairStepNames.indexOf("Checkout trusted validator after candidate report"), ); const recordedOutcomes = workflowStep(laneJob, "Record runtime-pair lane status").env?.[ "RELEASE_CHECK_STEP_OUTCOMES" ]; expect(recordedOutcomes).toContain("steps.runtime_parity_validation.outcome"); expect(recordedOutcomes).toContain("steps.generate_runtime_parity_report.outcome"); expect(recordedOutcomes).not.toContain("steps.candidate_runtime_pair.outcome"); expect(recordedOutcomes).not.toContain("steps.candidate_runtime_parity_report.outcome"); expect(workflowStep(laneJob, "Upload runtime-pair lane artifacts").with?.name).toContain( "${{ matrix.lane }}", ); expect(collectorJob.needs).toEqual([ "resolve_target", "qa_lab_runtime_pair_lane_release_checks", ]); expect(collectorJob.name).toBe("Verify QA Lab runtime-pair lanes"); expect(workflowStep(collectorJob, "Resolve runtime-pair lane artifacts").run).toContain( "qa_lab_runtime_pair_lane_release_checks|core", ); expect(workflowStep(collectorJob, "Resolve runtime-pair lane artifacts").run).toContain( "qa_lab_runtime_pair_lane_release_checks|soak", ); expect(workflowStep(collectorJob, "Download runtime-pair lane artifacts").with).toMatchObject({ "artifact-ids": "${{ steps.resolve_runtime_pair_artifacts.outputs.payload_ids }}", "merge-multiple": true, }); expect(workflowStep(collectorJob, "Download runtime-pair lane artifacts").if).toBe( "always() && steps.resolve_runtime_pair_artifacts.outcome == 'success'", ); expect(workflowStep(collectorJob, "Download runtime-pair lane statuses").if).toBe( "always() && steps.resolve_runtime_pair_artifacts.outcome == 'success'", ); expect(workflowStep(collectorJob, "Verify runtime-pair lane statuses").run).toContain( "resolve-release-check-artifacts.sh validate", ); expect(workflowStep(collectorJob, "Upload runtime parity artifacts").with?.name).toBe( "release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }}", ); }); it("requires live proof evidence artifacts when proof jobs run", () => { const cases = [ { workflowPath: MANTIS_DISCORD_SMOKE_WORKFLOW, jobName: "run_discord_smoke", stepName: "Upload Mantis artifacts", }, { workflowPath: MANTIS_DISCORD_STATUS_REACTIONS_WORKFLOW, jobName: "run_status_reactions", stepName: "Upload Mantis status reaction artifacts", }, { workflowPath: MANTIS_DISCORD_THREAD_ATTACHMENT_WORKFLOW, jobName: "run_thread_attachment", stepName: "Upload Mantis thread attachment artifacts", }, { workflowPath: MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW, jobName: "run_slack_desktop", stepName: "Upload Mantis Slack desktop artifacts", }, { workflowPath: MANTIS_TELEGRAM_DESKTOP_PROOF_WORKFLOW, jobName: "run_telegram_desktop_proof", stepName: "Upload Mantis Telegram desktop artifacts", }, { workflowPath: MANTIS_TELEGRAM_LIVE_WORKFLOW, jobName: "run_telegram_live", stepName: "Upload Mantis Telegram artifacts", }, { workflowPath: MANTIS_WEB_UI_CHAT_PROOF_WORKFLOW, jobName: "run_web_ui_chat", stepName: "Upload Mantis web UI chat artifacts", }, { workflowPath: NPM_TELEGRAM_WORKFLOW, jobName: "run_package_telegram_e2e", stepName: "Upload npm Telegram E2E artifacts", }, ]; for (const item of cases) { const label = `${item.workflowPath} ${item.jobName}`; const uploadStep = workflowStep(workflowJob(item.workflowPath, item.jobName), item.stepName); expect(uploadStep.if, label).toContain("always()"); expect(uploadStep.uses, label).toBe(UPLOAD_ARTIFACT_V7); expect(uploadStep.with?.["if-no-files-found"], label).toBe("error"); } }); it("bounds Mantis Crabbox source retrieval", () => { const cases = [ [MANTIS_DISCORD_STATUS_REACTIONS_WORKFLOW, "run_status_reactions"], [MANTIS_DISCORD_THREAD_ATTACHMENT_WORKFLOW, "run_thread_attachment"], [MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW, "run_slack_desktop"], [MANTIS_TELEGRAM_DESKTOP_PROOF_WORKFLOW, "run_telegram_desktop_proof"], [MANTIS_TELEGRAM_LIVE_WORKFLOW, "run_telegram_live"], ] as const; for (const [workflowPath, jobName] of cases) { const installStep = workflowStep(workflowJob(workflowPath, jobName), "Install Crabbox CLI"); expect(installStep.run, workflowPath).toMatch( /timeout --signal=TERM --kill-after=10s 120s git (?:clone|-C .* fetch)/u, ); } }); it("maps every supported Slack approval checkpoint scenario family", () => { const workflow = readFileSync(MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW, "utf8"); expectTextToIncludeAll(workflow, [ 'endswith("-exec-native")', 'endswith("-plugin-native")', 'startswith("slack-codex-")', 'expected_result="Slack approval checkpoint passes for $scenario_label"', ]); }); it("fails Docker E2E release lanes when summary artifacts are missing", () => { const cases = [ { jobName: "validate_docker_e2e", summaryStep: "Summarize Docker E2E chunk", uploadStep: "Upload Docker E2E chunk artifacts", }, { jobName: "validate_docker_lanes", summaryStep: "Summarize targeted Docker E2E lanes", uploadStep: "Upload targeted Docker E2E artifacts", }, { jobName: "validate_docker_openwebui", summaryStep: "Summarize Open WebUI Docker E2E chunk", uploadStep: "Upload Open WebUI Docker E2E artifacts", }, ]; for (const item of cases) { const job = workflowJob(LIVE_E2E_WORKFLOW, item.jobName); const summaryStep = workflowStep(job, item.summaryStep); const uploadStep = workflowStep(job, item.uploadStep); expect(summaryStep.run, item.jobName).toContain("summary missing:"); expect(summaryStep.run, item.jobName).toContain("exit 1"); expect(uploadStep.with?.["if-no-files-found"], item.jobName).toBe("error"); } }); it("isolates Open WebUI release coverage on a lean large-disk runner", () => { const job = workflowJob(LIVE_E2E_WORKFLOW, "validate_docker_openwebui"); const setupNode = workflowStep(job, "Setup Node environment"); expect(job.if).toBe( "inputs.include_openwebui && inputs.docker_lanes == '' && (inputs.release_test_profile == 'stable' || inputs.release_test_profile == 'full')", ); expect(job["runs-on"]).toBe("blacksmith-32vcpu-ubuntu-2404"); expect(job.env?.OPENCLAW_DOCKER_ALL_RELEASE_PROFILE).toBe("${{ inputs.release_test_profile }}"); expect(setupNode.with).toMatchObject({ "install-bun": "false", "install-deps": "false", "use-actions-cache": "false", }); }); it("names package acceptance Telegram as artifact-backed package validation", () => { const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); expect(workflow).toContain("package_telegram:"); expect(workflow).toContain("docker_acceptance_registry,"); expect(workflow).toContain("PACKAGE_TELEGRAM_RESULT:"); expect(workflow).toContain("package_telegram=${PACKAGE_TELEGRAM_RESULT}"); expect(workflow).not.toContain("npm_telegram:"); }); it.each([ { expectedOutput: undefined, expectedStatus: 0, name: "accepts Telegram result success when enabled=true", params: { telegramEnabled: true, telegramResult: "success" }, }, { expectedOutput: undefined, expectedStatus: 0, name: "accepts Telegram result skipped when enabled=false", params: { telegramEnabled: false, telegramResult: "skipped" }, }, { expectedOutput: "::error::package_telegram ended with skipped", expectedStatus: 1, name: "rejects a skipped Telegram lane when package acceptance enabled it", params: { telegramEnabled: true, telegramResult: "skipped" }, }, { expectedOutput: "::error::No Docker acceptance transport ran", expectedStatus: 1, name: "rejects package acceptance when no Docker transport ran", params: { dockerArtifactResult: "skipped", dockerRegistryResult: "skipped", telegramEnabled: false, telegramResult: "skipped", }, }, { expectedOutput: "::warning::package_telegram ended with skipped; package acceptance is advisory for this caller.", expectedStatus: 0, name: "preserves advisory handling for an unexpectedly skipped Telegram lane", params: { advisory: true, telegramEnabled: true, telegramResult: "skipped" }, }, ] as const)("$name", ({ expectedOutput, expectedStatus, params }) => { const result = runPackageAcceptanceSummary(params); expect(result.status).toBe(expectedStatus); if (expectedOutput) { expect(result.stdout).toContain(expectedOutput); } else { expect(result.stderr).toBe(""); } }); it("allows beta callers to make only Telegram package acceptance advisory", () => { const telegramResult = runPackageAcceptanceSummary({ telegramAdvisory: true, telegramEnabled: true, telegramResult: "failure", }); const dockerResult = runPackageAcceptanceSummary({ dockerArtifactResult: "failure", telegramAdvisory: true, telegramEnabled: true, telegramResult: "success", }); expect(telegramResult.status).toBe(0); expect(telegramResult.stdout).toContain( "::warning::package_telegram ended with failure; package acceptance is advisory for this caller.", ); expect(dockerResult.status).toBe(1); expect(dockerResult.stdout).toContain("::error::docker_acceptance ended with failure"); }); it("gives release build steps enough Node heap", () => { for (const workflowPath of [LIVE_E2E_WORKFLOW, RELEASE_CHECKS_WORKFLOW]) { const jobs = readWorkflow(workflowPath).jobs ?? {}; for (const [jobName, job] of Object.entries(jobs)) { for (const step of job.steps ?? []) { if (step.run === "pnpm build") { expect(step.env, `${workflowPath}:${jobName}:${step.name}`).toEqual({ NODE_OPTIONS: "--max-old-space-size=8192", }); } } } } }); it("runs full release children from the trusted workflow ref", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); const workflowInputs = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW).on?.workflow_dispatch ?.inputs; const resolveTargetJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"); const evidenceReuseJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "evidence_reuse"); const releaseChecksJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_checks"); const npmTelegramJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "npm_telegram"); const performanceJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"); const summaryJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"); const targetSummaryStep = workflowStep(resolveTargetJob, "Summarize target"); const evidenceReuseStep = workflowStep(evidenceReuseJob, "Find reusable validation evidence"); const releaseChecksDispatchStep = workflowStep( releaseChecksJob, "Dispatch and monitor release checks", ); const dispatchStep = workflowStep(npmTelegramJob, "Dispatch and monitor npm Telegram E2E"); const verificationStep = workflowStep(summaryJob, "Verify child workflow results"); const manifestStep = workflowStep(summaryJob, "Write release validation manifest"); expect(workflowInputs).toMatchObject({ skip_package_telegram_e2e: { default: false, type: "boolean", }, }); expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}"); expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1'); expect(npmTelegramJob.name).toBe("Run package Telegram E2E"); expect(npmTelegramJob.needs).toEqual(["resolve_target", "evidence_reuse"]); expect(npmTelegramJob["timeout-minutes"]).toBe( "${{ inputs.release_profile == 'full' && 360 || 120 }}", ); expect(performanceJob["timeout-minutes"]).toBe(360); expect(npmTelegramJob.if).toContain( 'contains(fromJSON(\'["all","npm-telegram"]\'), inputs.rerun_group)', ); expect(npmTelegramJob.if).toContain("needs.evidence_reuse.outputs.reuse != 'true'"); expect(evidenceReuseStep.env).toMatchObject({ ALLOW_UNRELEASED_CHANGELOG: "${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}", NPM_TELEGRAM_PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec }}", NPM_TELEGRAM_PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}", NPM_TELEGRAM_SCENARIO: "${{ inputs.npm_telegram_scenario }}", SKIP_PACKAGE_TELEGRAM_E2E: "${{ inputs.skip_package_telegram_e2e }}", }); expectTextToIncludeAll(evidenceReuseStep.run, [ "npmTelegramPackageSpec: $npmTelegramPackageSpec", "npmTelegramProviderMode: $npmTelegramProviderMode", "npmTelegramScenario: $npmTelegramScenario", "skipPackageTelegramE2e: $skipPackageTelegramE2e", "allowUnreleasedChangelog: $allowUnreleasedChangelog", ]); expect(targetSummaryStep.env).toMatchObject({ SKIP_PACKAGE_TELEGRAM_E2E: "${{ inputs.skip_package_telegram_e2e }}", }); expectTextToIncludeAll(targetSummaryStep.run, [ "Validation SHA:", "Package Acceptance Telegram E2E deferred:", "Package Telegram E2E: deferred by \\`skip_package_telegram_e2e\\`", ]); expect(releaseChecksDispatchStep.env).toMatchObject({ SKIP_PACKAGE_TELEGRAM_E2E: "${{ inputs.skip_package_telegram_e2e }}", }); expect(releaseChecksDispatchStep.run).toContain( '-f skip_package_telegram_e2e="$SKIP_PACKAGE_TELEGRAM_E2E"', ); expect(dispatchStep.env).toEqual({ CHILD_WORKFLOW_KIND: "npm-telegram", CHILD_WORKFLOW_REF: "${{ github.ref_name }}", FAIL_FAST: "${{ inputs.fail_fast }}", GH_TOKEN: "${{ github.token }}", PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec || inputs.release_package_spec }}", PARENT_WORKFLOW_SHA: "${{ github.sha }}", PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}", SCENARIO: "${{ inputs.npm_telegram_scenario }}", TARGET_SHA: "${{ needs.resolve_target.outputs.sha }}", }); expect(manifestStep.env).toMatchObject({ ALLOW_UNRELEASED_CHANGELOG: "${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}", TARGET_REF: "${{ startsWith(github.ref, 'refs/heads/release-ci/') && needs.resolve_target.outputs.sha || inputs.ref }}", NPM_TELEGRAM_PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec }}", NPM_TELEGRAM_PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}", NPM_TELEGRAM_SCENARIO: "${{ inputs.npm_telegram_scenario }}", SKIP_PACKAGE_TELEGRAM_E2E: "${{ inputs.skip_package_telegram_e2e }}", }); expectTextToIncludeAll(manifestStep.run, [ "npmTelegramPackageSpec: $npmTelegramPackageSpec", "npmTelegramProviderMode: $npmTelegramProviderMode", "npmTelegramScenario: $npmTelegramScenario", "skipPackageTelegramE2e: $skipPackageTelegramE2e", "allowUnreleasedChangelog: $allowUnreleasedChangelog", ]); expect(verificationStep.env).toMatchObject({ SKIP_PACKAGE_TELEGRAM_E2E: "${{ inputs.skip_package_telegram_e2e }}", }); expect(verificationStep.run).toContain("Package Telegram E2E deferred:"); expectTextToIncludeAll(dispatchStep.run, [ 'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-npm-telegram"', 'dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"', 'dispatch_and_wait npm-telegram-beta-e2e.yml "$dispatch_run_name" "${args[@]}"', ".display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF", "The dispatch was not retried to avoid creating a duplicate child.", 'if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then', '-f harness_ref="$TARGET_SHA"', 'args=(-f package_spec="$PACKAGE_SPEC"', 'args+=(-f scenario="$SCENARIO")', ]); expect(dispatchStep.run).not.toContain("package_artifact"); expectTextToIncludeAll(workflow, [ "child_rerun_group=all", '-f rerun_group="$child_rerun_group"', 'args+=(-f live_suite_filter="$LIVE_SUITE_FILTER")', 'args+=(-f cross_os_suite_filter="$CROSS_OS_SUITE_FILTER")', 'case "$RERUN_GROUP" in', "release-checks|install-smoke|cross-os|live-e2e|package|qa|qa-parity|qa-live)", "cancel-in-progress: false", "Verify release checks accepted Tideclaw alpha advisory lanes", "release_checks_advisory_only", "release_check_blocking_job", 'if [[ "$RERUN_GROUP" == "npm-telegram" || ( "$RERUN_GROUP" == "all"', "npm_telegram_required=1", "Reused evidence did not record the required npm Telegram child run.", 'check_child "npm_telegram" "" "$npm_telegram_required"', 'if [[ "$RELEASE_PROFILE" == "beta" && "$1" == "Run package acceptance / Telegram package acceptance / "* ]]; then', 'or (.name | startswith("Run QA Lab runtime-pair lane ("))', 'or .name == "Run QA Lab live Discord lane"', 'or (.name | startswith("Run package acceptance / Telegram package acceptance / ")))', "is a package-safety Tideclaw alpha release-check lane", '"Run package acceptance" | \\', '"Run package acceptance / "*)', 'check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 1', "gh run cancel", "NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}", "Sorry. Your account was suspended", 'gh_with_retry run view "$run_id" --json status,conclusion,url,attempt,headSha,jobs', ]); expect(workflow).not.toContain("force-cancel"); expect(workflow).not.toContain("workflow_ref:"); expect(workflow).not.toContain("inputs.workflow_ref"); }); it("documents the full-release Telegram package path in operator summaries", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); const releaseDocs = readFileSync("docs/reference/RELEASING.md", "utf8"); const fullReleaseDocs = readFileSync("docs/reference/full-release-validation.md", "utf8"); expectTextToIncludeAll(workflow, [ "Published-package Telegram E2E:", "Package Telegram E2E: deferred by \\`skip_package_telegram_e2e\\`", "Package Telegram E2E: OpenClaw Release Checks Package Acceptance", "Package Telegram E2E: focused rerun requires \\`release_package_spec\\` or \\`npm_telegram_package_spec\\`", ]); expect(releaseDocs).toContain( "Focused `npm-telegram` reruns require `release_package_spec` or", ); expectTextToIncludeAll(fullReleaseDocs, [ "cross_os_suite_filter", "QA release-check failures block normal release validation", "input capture fails", "skipping the lane", "does not duplicate that", "canonical Package Acceptance Telegram E2E", "| `npm-telegram` | Published-package Telegram E2E; requires `release_package_spec` or `npm_telegram_package_spec`. |", ]); }); it("lets npm Telegram consume current-run or release-run package artifacts", () => { const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"); const currentRunDownload = workflowStep(job, "Download package-under-test artifact"); const releaseRunDownload = workflowStep( job, "Download package-under-test artifact from release run", ); const validateStep = workflowStep(job, "Validate inputs and secrets"); const identityStep = workflowStep(job, "Validate package artifact identity"); const runStep = workflowStep(job, "Run package Telegram E2E"); expect(currentRunDownload).toEqual({ if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id == github.run_id", name: "Download package-under-test artifact", uses: DOWNLOAD_ARTIFACT_V8, with: { "artifact-ids": "${{ inputs.package_artifact_id }}", "github-token": "${{ github.token }}", path: ".artifacts/telegram-package-under-test", "run-id": "${{ inputs.package_artifact_run_id }}", }, }); expect(releaseRunDownload).toEqual({ if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id != github.run_id", name: "Download package-under-test artifact from release run", uses: DOWNLOAD_ARTIFACT_V8, with: { "artifact-ids": "${{ inputs.package_artifact_id }}", "github-token": "${{ github.token }}", path: ".artifacts/telegram-package-under-test", "run-id": "${{ inputs.package_artifact_run_id }}", }, }); expectTextToIncludeAll(validateStep.run, [ 'if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then', "Artifact-backed Telegram E2E requires all artifact identity fields or none.", "package_spec must be openclaw@alpha", "Artifact-backed Telegram E2E requires the complete immutable artifact and package identity tuple.", ]); expect(identityStep.env).toMatchObject({ ARTIFACT_DIGEST: "${{ inputs.package_artifact_digest }}", ARTIFACT_ID: "${{ inputs.package_artifact_id }}", ARTIFACT_NAME: "${{ inputs.package_artifact_name }}", ARTIFACT_RUN_ATTEMPT: "${{ inputs.package_artifact_run_attempt }}", ARTIFACT_RUN_ID: "${{ inputs.package_artifact_run_id }}", }); expectTextToIncludeAll(identityStep.run, [ "actions/artifacts/${ARTIFACT_ID}", '--arg digest "sha256:${ARTIFACT_DIGEST}"', "actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}", 'if [[ "$ARTIFACT_RUN_ID" == "$GITHUB_RUN_ID" ]]', '.status == "pending" or .status == "queued" or .status == "requested" or .status == "waiting" or .status == "in_progress"', ".conclusion == null", "Package Telegram artifact predates the active producer run attempt.", '.status == "completed"', '.conclusion == "success"', "artifact_created_at <= attempt_started_at", "artifact_created_at > attempt_completed_at", "Package Telegram artifact creation time is outside the declared producer run attempt.", "Package Telegram artifact producer run attempt does not match the requested tuple.", ]); expect(runStep.env).toMatchObject({ PACKAGE_FILE_NAME: "${{ inputs.package_file_name || '' }}", PACKAGE_SHA256: "${{ inputs.package_sha256 || '' }}", PACKAGE_SOURCE_SHA: "${{ inputs.package_source_sha || '' }}", PACKAGE_VERSION: "${{ inputs.package_version || '' }}", }); expectTextToIncludeAll(runStep.run, [ 'declared_package_tgz="${package_dir}/${PACKAGE_FILE_NAME}"', 'manifest="${package_dir}/preflight-manifest.json"', 'candidate_manifest="${package_dir}/package-candidate.json"', 'find "${package_dir}" -type f -name "*.tgz"', "package artifact manifest contains duplicate package metadata", "Array.isArray(manifest.corePackageTarballs)", "manifest.corePackageTarballs === undefined", "package artifact tarball set does not match preflight manifest", "package candidate manifest does not match the OpenClaw tarball", "Package Telegram artifact SHA-256 differs from package_sha256.", "package candidate digest mismatch", "Package Telegram artifact tarball differs from package_file_name.", "Package Telegram artifact source SHA/version differs from the declared identity.", 'export OPENCLAW_NPM_TELEGRAM_PACKAGE_DIR="${package_dir}"', 'export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgz}"', ]); }); it("accepts immutable artifacts produced earlier in the active workflow attempt", () => { const result = runNpmTelegramArtifactValidation({ currentRunId: "123", producerConclusion: null, producerRunId: "123", producerStatus: "in_progress", }); expect(result.status, result.stderr).toBe(0); }); it("accepts active artifacts while GitHub still reports the workflow as queued", () => { const result = runNpmTelegramArtifactValidation({ currentRunId: "123", producerConclusion: null, producerRunId: "123", producerStatus: "queued", }); expect(result.status, result.stderr).toBe(0); }); it("accepts active artifacts while GitHub still reports the workflow as pending", () => { const result = runNpmTelegramArtifactValidation({ currentRunId: "123", producerConclusion: null, producerRunId: "123", producerStatus: "pending", }); expect(result.status, result.stderr).toBe(0); }); it("rejects queued artifacts after GitHub assigns a conclusion", () => { const result = runNpmTelegramArtifactValidation({ currentRunId: "123", producerConclusion: "success", producerRunId: "123", producerStatus: "queued", }); expect(result.status).toBe(1); expect(result.stderr).toContain( "Current-run Package Telegram artifact is not from the active workflow attempt.", ); }); it("keeps completed external producer attempts success-gated", () => { const result = runNpmTelegramArtifactValidation({ currentRunId: "456", producerConclusion: "success", producerRunId: "123", producerStatus: "completed", }); expect(result.status, result.stderr).toBe(0); }); it("rejects partial npm Telegram artifact identity instead of falling back to npm", () => { const result = runNpmTelegramInputValidation({ PACKAGE_ARTIFACT_ID: "123", }); expect(result.status).toBe(1); expect(result.stderr).toContain( "Artifact-backed Telegram E2E requires all artifact identity fields or none.", ); }); it("uses bounded Convex lease waits instead of GitHub concurrency for CI Telegram consumers", () => { const telegramJobs = [ [NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e", "Run package Telegram E2E", "600000"], [RELEASE_TELEGRAM_QA_WORKFLOW, "run_telegram", "Run Telegram live lane", "600000"], [QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_telegram", "Run Telegram live lane", "1800000"], [ ".github/workflows/mantis-telegram-live.yml", "run_telegram_live", "Run Telegram live scenario and capture desktop evidence", "1800000", ], ] as const; for (const [workflowPath, jobName, stepName, acquireTimeoutMs] of telegramJobs) { const job = workflowJob(workflowPath, jobName); expect(job.concurrency).toBeUndefined(); const step = workflowStep(job, stepName); expect(step.env?.OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS).toBe(acquireTimeoutMs); } }); it("keeps release QA and repo E2E lanes off scarce 32-core runners", () => { const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8"); const liveE2eWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8"); for (const jobName of [ "qa_lab_parity_lane_release_checks", "qa_lab_parity_report_release_checks", ]) { expect(releaseChecksWorkflow).toMatch( new RegExp(`${jobName}:[\\s\\S]*?runs-on: ubuntu-24\\.04`, "u"), ); } for (const jobName of [ "trusted_identity", "build_candidate", "attest_candidate", "run_telegram", "advisory_status", ]) { expect(workflowJob(RELEASE_TELEGRAM_QA_WORKFLOW, jobName)["runs-on"]).toBe("ubuntu-24.04"); } for (const jobName of [ "run_mock_parity", "run_live_matrix", "run_live_telegram", "run_live_discord", "run_live_whatsapp", "run_live_slack", "run_live_runtime_token_efficiency", ]) { expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, jobName)["runs-on"]).toBe( "blacksmith-16vcpu-ubuntu-2404", ); } expectTextToIncludeAll(liveE2eWorkflow, [ "OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=180000", "OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000", ]); }); describe("release check artifact resolver", () => { const runId = "123456"; const targetSha = "a".repeat(40); const pair = (job: string, variant: string, slug: string): ReleaseCheckArtifactPair => ({ job, payloadBase: `release-payload-${slug}-${targetSha}-${runId}`, statusBase: `release-status-${slug}-${targetSha}-${runId}`, variant, }); const artifactsFor = ( artifactPair: ReleaseCheckArtifactPair, attempt: number, firstId: number, options: { expiredPayload?: boolean; expiredStatus?: boolean } = {}, ): ReleaseCheckArtifact[] => [ releaseCheckArtifact({ expired: options.expiredStatus, id: firstId, name: `${artifactPair.statusBase}-${attempt}`, runId, }), releaseCheckArtifact({ expired: options.expiredPayload, id: firstId + 1, name: `${artifactPair.payloadBase}-${attempt}`, runId, }), ]; it.each([ { artifacts: [1, 2].flatMap((attempt, index) => artifactsFor(pair("qa_job", "candidate", "candidate"), attempt, index * 10 + 1), ), consumerAttempt: "2", expectedAttempt: 2, name: "selects the current producer attempt", }, { artifacts: artifactsFor(pair("qa_job", "candidate", "candidate"), 1, 1), consumerAttempt: "2", expectedAttempt: 1, name: "carries attempt 1 into consumer attempt 2", }, { artifacts: [2, 10].flatMap((attempt, index) => artifactsFor(pair("qa_job", "candidate", "candidate"), attempt, index * 10 + 1), ), consumerAttempt: "10", expectedAttempt: 10, name: "orders producer attempts numerically", }, { artifacts: [2, 3].flatMap((attempt, index) => artifactsFor(pair("qa_job", "candidate", "candidate"), attempt, index * 10 + 1), ), consumerAttempt: "2", expectedAttempt: 2, name: "excludes future producer attempts", }, ])("$name", ({ artifacts, consumerAttempt, expectedAttempt }) => { const result = runReleaseCheckArtifactResolve({ artifacts, consumerAttempt, pairs: [pair("qa_job", "candidate", "candidate")], runId, targetSha, }); expect(result.result.status, result.result.stderr).toBe(0); expect(result.selection).toHaveLength(1); expect(result.selection[0]?.producer_attempt).toBe(expectedAttempt); }); it("selects candidate and baseline attempts independently", () => { const candidate = pair("qa_lab_parity_lane_release_checks", "candidate", "candidate"); const baseline = pair("qa_lab_parity_lane_release_checks", "baseline", "baseline"); const result = runReleaseCheckArtifactResolve({ artifacts: [...artifactsFor(candidate, 1, 1), ...artifactsFor(baseline, 2, 11)], consumerAttempt: "2", pairs: [candidate, baseline], runId, targetSha, }); expect(result.result.status, result.result.stderr).toBe(0); expect( Object.fromEntries( result.selection.map((selection) => [selection.variant, selection.producer_attempt]), ), ).toEqual({ baseline: 2, candidate: 1 }); }); it("selects core and soak attempts independently", () => { const core = pair("qa_lab_runtime_pair_lane_release_checks", "core", "core"); const soak = pair("qa_lab_runtime_pair_lane_release_checks", "soak", "soak"); const result = runReleaseCheckArtifactResolve({ artifacts: [...artifactsFor(core, 2, 1), ...artifactsFor(soak, 1, 11)], consumerAttempt: "2", pairs: [core, soak], runId, targetSha, }); expect(result.result.status, result.result.stderr).toBe(0); expect( Object.fromEntries( result.selection.map((selection) => [selection.variant, selection.producer_attempt]), ), ).toEqual({ core: 2, soak: 1 }); }); it("fails when the latest producer attempt has no complete pair", () => { const candidate = pair("qa_job", "candidate", "candidate"); const result = runReleaseCheckArtifactResolve({ artifacts: [ ...artifactsFor(candidate, 1, 1), releaseCheckArtifact({ id: 11, name: `${candidate.statusBase}-2`, runId, }), ], consumerAttempt: "2", pairs: [candidate], runId, targetSha, }); expect(result.result.status).toBe(1); expect(result.result.stderr).toContain( `requires exactly one ${candidate.payloadBase}-2 artifact; found 0`, ); expect(result.result.stderr.trimEnd()).toMatch( /\[resolve-release-check-artifacts\] FAILED \(exit 1\)$/u, ); }); it("fails on duplicate artifacts at the latest producer attempt", () => { const candidate = pair("qa_job", "candidate", "candidate"); const artifacts = artifactsFor(candidate, 2, 1); artifacts.push( releaseCheckArtifact({ id: 11, name: `${candidate.statusBase}-2`, runId, }), ); const result = runReleaseCheckArtifactResolve({ artifacts, consumerAttempt: "2", pairs: [candidate], runId, targetSha, }); expect(result.result.status).toBe(1); expect(result.result.stderr).toContain( `requires exactly one ${candidate.statusBase}-2 artifact; found 2`, ); }); it.each([ { artifacts: (candidate: ReleaseCheckArtifactPair) => [ ...artifactsFor(candidate, 1, 1), releaseCheckArtifact({ id: 11, name: `${candidate.statusBase}-broken`, runId, }), ], expected: "has malformed producer attempt", name: "malformed newer evidence", }, { artifacts: (candidate: ReleaseCheckArtifactPair) => [ ...artifactsFor(candidate, 1, 1), ...artifactsFor(candidate, 2, 11, { expiredPayload: true }), ], expected: "is expired or has invalid expiry metadata", name: "expired newer evidence", }, ])("does not fall back past $name", ({ artifacts, expected }) => { const candidate = pair("qa_job", "candidate", "candidate"); const result = runReleaseCheckArtifactResolve({ artifacts: artifacts(candidate), consumerAttempt: "2", pairs: [candidate], runId, targetSha, }); expect(result.result.status).toBe(1); expect(result.result.stderr).toContain(expected); }); it.each([ { mutate: (text: string) => text.replace("status=success", "status=success\nstatus=failure"), name: "duplicate status fields", }, { mutate: (text: string) => text.replace("run_attempt=2", "run_attempt=bogus"), name: "malformed status metadata", }, ])("rejects $name", ({ mutate }) => { const candidate = pair("qa_job", "candidate", "candidate"); const resolved = runReleaseCheckArtifactResolve({ artifacts: artifactsFor(candidate, 2, 1), consumerAttempt: "2", pairs: [candidate], runId, targetSha, }); expect(resolved.result.status, resolved.result.stderr).toBe(0); const validated = runReleaseCheckArtifactValidation({ selection: resolved.selection, statusText: (selection) => mutate(releaseCheckStatusText(selection)), }); expect(validated.result.status).toBe(1); }); it("sets runtime parity ready=false for validated non-success evidence", () => { const runtimePair = pair("qa_lab_runtime_parity_release_checks", "", "runtime-parity"); const resolved = runReleaseCheckArtifactResolve({ artifacts: artifactsFor(runtimePair, 2, 1), consumerAttempt: "2", pairs: [runtimePair], runId, targetSha, }); expect(resolved.result.status, resolved.result.stderr).toBe(0); const workdir = tempDirs.make("runtime-parity-ready-"); const trustedScript = resolve( workdir, "trusted-release-check-artifacts/scripts/github/resolve-release-check-artifacts.sh", ); mkdirSync(resolve(trustedScript, ".."), { recursive: true }); symlinkSync(resolve(REPO_ROOT, RELEASE_CHECK_ARTIFACT_RESOLVER), trustedScript); const selectionFile = resolve(workdir, "selection.json"); writeFileSync(selectionFile, JSON.stringify(resolved.selection)); const statusDir = resolve(workdir, ".artifacts/release-check-status"); mkdirSync(statusDir, { recursive: true }); const selection = resolved.selection[0]!; writeFileSync( resolve( statusDir, `${selection.job}-${selection.run_id}-${selection.producer_attempt}.env`, ), releaseCheckStatusText(selection, "failure"), ); const outputFile = resolve(workdir, "github-output"); const runtimeCoverage = workflowJob( RELEASE_CHECKS_WORKFLOW, "runtime_tool_coverage_release_checks", ); const script = workflowStep( runtimeCoverage, "Verify runtime parity producer status", ).run?.replace( "${{ steps.resolve_runtime_parity_artifacts.outputs.selection_file }}", selectionFile, ); expect(script).toBeTruthy(); const result = spawnSync("bash", ["-c", script!], { cwd: workdir, encoding: "utf8", env: { GITHUB_OUTPUT: outputFile, PATH: process.env.PATH, }, }); expect(result.status, result.stderr).toBe(0); expect(readFileSync(outputFile, "utf8")).toContain("ready=false"); }); }); it("keeps release QA status artifacts blocking in the verifier", () => { const advisoryJobNames = [ "qa_lab_parity_lane_release_checks", "qa_lab_parity_report_release_checks", "qa_lab_runtime_parity_release_checks", "qa_live_discord_release_checks", "qa_live_whatsapp_release_checks", "qa_live_slack_release_checks", ]; for (const jobName of advisoryJobNames) { const job = workflowJob(RELEASE_CHECKS_WORKFLOW, jobName); expect(job["continue-on-error"], jobName).toBe(true); const recordStep = workflowStep(job, "Record advisory status"); expect(recordStep.if, jobName).toBe("always()"); expect(recordStep.run, jobName).toContain("status_path="); expect(recordStep.run, jobName).toContain(".artifacts/release-check-status"); expect(recordStep.run, jobName).toContain("GITHUB_RUN_ID"); expect(recordStep.run, jobName).toContain("GITHUB_RUN_ATTEMPT"); expect(recordStep.run, jobName).toContain("target_sha="); expect(recordStep.run, jobName).toContain("variant="); expect(recordStep.env?.RELEASE_CHECK_TARGET_SHA, jobName).toBe( "${{ needs.resolve_target.outputs.revision }}", ); expect(recordStep.env?.RELEASE_CHECK_STEP_OUTCOMES, jobName).toContain("upload_"); const uploadStep = workflowStep(job, "Upload advisory status"); expect(uploadStep.if, jobName).toBe("always()"); expect(uploadStep.uses, jobName).toBe(UPLOAD_ARTIFACT_V7); expect(uploadStep.with?.name, jobName).toContain("release-check-status-"); expect(uploadStep.with?.name, jobName).toContain( "${{ github.run_id }}-${{ github.run_attempt }}", ); expect(uploadStep.with?.path, jobName).toMatch( /^\.artifacts\/release-check-status\/.+\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}\.env$/u, ); expect(uploadStep.with?.["if-no-files-found"], jobName).toBe("error"); } for (const [jobName, stepName] of [ ["qa_lab_parity_lane_release_checks", "Upload parity lane artifacts"], ["qa_lab_parity_report_release_checks", "Upload parity artifacts"], ["qa_lab_runtime_pair_lane_release_checks", "Upload runtime-pair lane artifacts"], ["qa_lab_runtime_parity_release_checks", "Upload runtime parity artifacts"], ["qa_live_discord_release_checks", "Upload Discord QA artifacts"], ["qa_live_whatsapp_release_checks", "Upload WhatsApp QA artifacts"], ["qa_live_slack_release_checks", "Upload Slack QA artifacts"], ] as const) { const upload = workflowStep(workflowJob(RELEASE_CHECKS_WORKFLOW, jobName), stepName); expect(upload.with?.name, `${jobName}/${stepName}`).toContain( "${{ github.run_id }}-${{ github.run_attempt }}", ); } for (const jobName of [ "qa_lab_parity_report_release_checks", "qa_lab_runtime_parity_release_checks", "runtime_tool_coverage_release_checks", "summary", ]) { const checkout = workflowStep( workflowJob(RELEASE_CHECKS_WORKFLOW, jobName), "Checkout trusted release artifact resolver", ); expect(checkout.with).toMatchObject({ path: "trusted-release-check-artifacts", ref: "${{ github.sha }}", "sparse-checkout": RELEASE_CHECK_ARTIFACT_RESOLVER, "sparse-checkout-cone-mode": false, }); } const telegramCaller = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_telegram_release_checks"); const telegramDispatch = workflowStep(telegramCaller, "Dispatch and await trusted Telegram QA"); expect(telegramDispatch.run).toContain('workflow="openclaw-release-telegram-qa.yml"'); expect(telegramDispatch.run).toContain('--repo "$GITHUB_REPOSITORY"'); expect(telegramDispatch.run).toContain("--ref main"); expect(telegramDispatch.run).toContain( '-f expected_trusted_workflow_sha="$expected_trusted_workflow_sha"', ); expect(telegramDispatch.run).toContain( '[[ "$child_head_sha" == "$expected_trusted_workflow_sha" ]]', ); expect(telegramCaller["continue-on-error"]).toBeUndefined(); expect(telegramCaller["timeout-minutes"]).toBe(210); const telegramStatus = workflowJob(RELEASE_TELEGRAM_QA_WORKFLOW, "advisory_status"); expect(telegramStatus["continue-on-error"]).toBeUndefined(); const telegramRecord = workflowStep(telegramStatus, "Record advisory status"); expect(telegramRecord.run?.trim()).toBe( "set -euo pipefail\nnode scripts/release-telegram-qa.mjs advisory-status", ); const telegramStatusUpload = workflowStep(telegramStatus, "Upload advisory status"); expect(telegramStatusUpload.if).toBe("always()"); expect(telegramStatusUpload.uses).toBe(UPLOAD_ARTIFACT_V7); expect(telegramStatusUpload.with?.name).toBe( "release-check-status-qa-live-telegram-${{ inputs.target_sha }}-${{ github.run_id }}-${{ github.run_attempt }}", ); expect(telegramStatusUpload.with?.path).toContain( "${{ steps.record_status.outputs.status_file }}", ); expect(telegramStatusUpload.with?.["if-no-files-found"]).toBe("error"); const telegramRequire = workflowStep( telegramStatus, "Require successful Telegram release check", ); expect(telegramRequire.if).toBe("always()"); expect(telegramRequire.run).toContain('[[ "$STATUS" == "success" ]]'); const summary = workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"); expect(summary.needs).toContain("resolve_target"); expect(summary.permissions?.actions).toBe("read"); expect(summary.permissions?.contents).toBe("read"); const resolveStep = workflowStep(summary, "Resolve advisory evidence artifacts"); expect(resolveStep["continue-on-error"]).toBe(true); expect(resolveStep.run).toContain( "trusted-release-check-artifacts/scripts/github/resolve-release-check-artifacts.sh", ); expect(resolveStep.run).toContain('--consumer-attempt "$GITHUB_RUN_ATTEMPT"'); expect(resolveStep.run).toContain("qa_lab_parity_lane_release_checks|candidate"); expect(resolveStep.run).toContain("qa_lab_parity_lane_release_checks|baseline"); const downloadStep = workflowStep(summary, "Download advisory status artifacts"); expect(downloadStep["continue-on-error"]).toBe(true); expect(downloadStep.uses).toBe(DOWNLOAD_ARTIFACT_V8); expect(downloadStep.with?.["artifact-ids"]).toBe( "${{ steps.resolve_advisory_evidence.outputs.status_ids }}", ); expect(downloadStep.with?.["merge-multiple"]).toBe(true); expect(downloadStep.with?.pattern).toBeUndefined(); const verifyStep = workflowStep(summary, "Verify release check results"); expect(verifyStep.env).toMatchObject({ QA_LIVE_BUZZ_RELEASE_CHECKS_RESULT: "${{ needs.qa_live_buzz_release_checks.result }}", QA_LIVE_RELEASE_CHECKS_RESULT: "${{ needs.qa_live_release_checks.result }}", RELEASE_CHECK_RUN_ATTEMPT: "${{ github.run_attempt }}", RELEASE_CHECK_RUN_ID: "${{ github.run_id }}", RELEASE_CHECK_TARGET_SHA: "${{ needs.resolve_target.outputs.revision }}", RESOLVE_ADVISORY_EVIDENCE_OUTCOME: "${{ steps.resolve_advisory_evidence.outcome }}", VALIDATE_ADVISORY_STATUSES_OUTCOME: "${{ steps.validate_advisory_statuses.outcome }}", }); expectTextToIncludeAll(verifyStep.run, [ "release_check_result()", "validated_status()", "advisory-evidence-validated.json", "missing or duplicate validated status", "Advisory evidence resolution or validation failed", 'elif [[ "$fallback" != "success" && "$fallback" != "skipped" ]]; then', 'elif [[ "$fallback" == "success" ]]; then', "advisory_status_override_allowed()", 'if advisory_status_override_allowed "$name"; then', "::warning::${name} ended with ${result}; Tideclaw alpha treats non-package-safety release-check lanes as advisory.", "::error::${name} ended with ${result}", '"qa_live_release_checks=${QA_LIVE_RELEASE_CHECKS_RESULT}"', '"qa_live_buzz_release_checks=${QA_LIVE_BUZZ_RELEASE_CHECKS_RESULT}"', ]); expect(verifyStep.run).not.toContain("qa_live_matrix_release_checks"); expect(verifyStep.run).not.toContain( "QA release-check lanes are advisory and do not block release validation.", ); expect(verifyStep.run).not.toContain("expected_status_artifact_count"); expect(verifyStep.run).not.toContain("actual_status_count"); const runtimeCoverage = workflowJob( RELEASE_CHECKS_WORKFLOW, "runtime_tool_coverage_release_checks", ); expect(workflowStep(runtimeCoverage, "Resolve runtime parity artifacts").run).toContain( "trusted-release-check-artifacts/scripts/github/resolve-release-check-artifacts.sh", ); expect( workflowStep(runtimeCoverage, "Download runtime parity status").with?.["artifact-ids"], ).toBe("${{ steps.resolve_runtime_parity_artifacts.outputs.status_ids }}"); expectTextToIncludeAll( workflowStep(runtimeCoverage, "Verify runtime parity producer status").run, ["resolve-release-check-artifacts.sh validate", "ready=false", "ready=true"], ); expect( workflowStep(runtimeCoverage, "Download runtime parity artifacts").with?.["artifact-ids"], ).toBe("${{ steps.resolve_runtime_parity_artifacts.outputs.payload_ids }}"); }); it.each([ { emptyStderr: true, expected: [], name: "accepts a successful dispatched Telegram child", params: { currentAttempt: "2", currentResult: "success" }, status: 0, }, ...(["cancelled", "failure", "skipped"] as const).map((currentResult) => ({ emptyStderr: false, expected: [`::error::qa_live_telegram_release_checks ended with ${currentResult}`], name: `rejects a ${currentResult} selected Telegram child`, params: { currentAttempt: "2", currentResult, telegramSelected: true }, status: 1, })), { emptyStderr: false, expected: [], name: "accepts a skipped unselected Telegram dispatch", params: { currentAttempt: "2", currentResult: "skipped", telegramSelected: false }, status: 0, }, { emptyStderr: false, expected: ["::error::resolve_target ended with failure"], name: "keeps target resolution blocking before release children", params: { currentAttempt: "2", currentResult: "skipped", resolveResult: "failure", telegramSelected: false, }, status: 1, }, { emptyStderr: false, expected: ["qa_live_telegram_release_checks ended with cancelled", "Tideclaw alpha"], name: "keeps a cancelled Telegram child non-blocking for Tideclaw alpha", params: { currentAttempt: "2", currentResult: "cancelled", workflowRef: "refs/heads/tideclaw/alpha/2026-07-10-1200Z", }, status: 0, }, ] as const)("$name", ({ emptyStderr, expected, params, status }) => { const result = runReleaseChecksSummary(params); const output = `${result.stdout}\n${result.stderr}`; expect(result.status).toBe(status); if (emptyStderr) { expect(result.stderr).toBe(""); } for (const snippet of expected ?? []) { expect(output).toContain(snippet); } }); it.each(["cancelled", "failure"] as const)( "does not mask a later %s advisory status with an older successful job result", (status) => { const result = runReleaseChecksSummary({ currentAttempt: "2", currentResult: "skipped", discordResult: "success", telegramSelected: false, validatedStatuses: [ { job: "qa_live_discord_release_checks", status, variant: "", }, ], }); expect(result.status).toBe(1); expect(`${result.stdout}\n${result.stderr}`).toContain( `::error::qa_live_discord_release_checks ended with ${status}`, ); }, ); it("summarizes start delay separately from execution time in full validation", () => { const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); const parsedWorkflow = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW); const summaryJob = parsedWorkflow.jobs?.summary; const manifestStep = workflowStep(summaryJob ?? {}, "Write release validation manifest"); expect(workflow).toContain("### Slowest jobs: ${label}"); expect(workflow).toContain("### Longest start delays: ${label}"); expect(workflow).toContain("Write release validation manifest"); expect(workflow).toContain("PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }}"); expect(workflow).toContain("Upload release validation manifest"); expect(workflow).toContain("Failed child detail: ${label}"); expect(workflow).toContain("actions/runs/${run_id}/artifacts?per_page=100"); expect(workflow).toContain("full-release-validation-${{ github.run_id }}"); expect(workflow).toContain("| Job | Result | Start delay minutes | Run minutes |"); expect(workflow).toContain( 'gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100"', ); expect(workflow).toContain("(.started_at | ts) - (.created_at | ts)"); expect(workflow).not.toContain('gh run view "$run_id" --json createdAt,jobs'); expect(manifestStep.env?.PERFORMANCE_RUN_ID).toBe("${{ needs.performance.outputs.run_id }}"); expect(manifestStep.run).toContain('--arg performanceRunId "$PERFORMANCE_RUN_ID"'); }); it("wires evidence attempts into the acceptance gate", () => { const releaseResolveJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target"); const releaseRun = workflowStep(releaseResolveJob, "Resolve full release validation run"); const releaseManifest = workflowStep( releaseResolveJob, "Download full release validation manifest", ); const npmPublishJob = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm"); const npmRun = workflowStep(npmPublishJob, "Verify full release validation evidence"); const npmManifest = workflowStep(npmPublishJob, "Download full release validation manifest"); expect(releaseRun).toMatchObject({ id: "full_run", env: { FULL_RELEASE_VALIDATION_RUN_ID: "${{ inputs.full_release_validation_run_id }}", FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "${{ inputs.full_release_validation_run_attempt }}", }, }); expect(releaseRun.run).toContain( 'run_endpoint+="/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}"', ); expect(releaseResolveJob.outputs?.full_release_validation_run_attempt).toBe( "${{ steps.full_run.outputs.attempt }}", ); expect(releaseManifest.with).toMatchObject({ name: "full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ steps.full_run.outputs.attempt }}", "run-id": "${{ inputs.full_release_validation_run_id }}", }); expect(npmRun.env).toMatchObject({ FULL_RELEASE_VALIDATION_RUN_ID: "${{ inputs.full_release_validation_run_id }}", FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "${{ inputs.full_release_validation_run_attempt }}", }); expect(npmRun.run).toContain( "actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}", ); expect(npmManifest.with).toMatchObject({ name: "full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ inputs.full_release_validation_run_attempt }}", "run-id": "${{ inputs.full_release_validation_run_id }}", }); }); it("keeps release publish artifacts and release-note ordering wired", () => { const resolveJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target"); const publishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"); const resolveFullRun = workflowStep(resolveJob, "Resolve full release validation run"); const resolveDownload = workflowStep(resolveJob, "Download full release validation manifest"); const trustedTooling = workflowStep(resolveJob, "Download trusted release validation tooling"); const validateManifest = workflowStep(resolveJob, "Validate full release validation manifest"); const publishDownload = workflowStep(publishJob, "Download full release validation manifest"); const publishOrchestration = workflowStep(publishJob, "Dispatch publish workflows"); const npmPublishJob = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm"); const npmCheckout = workflowStep(npmPublishJob, "Checkout"); const npmFullRun = workflowStep(npmPublishJob, "Verify full release validation evidence"); const npmDownload = workflowStep(npmPublishJob, "Download full release validation manifest"); const npmTarget = workflowStep(npmPublishJob, "Verify full release validation target"); expect(resolveFullRun.id).toBe("full_run"); expect(resolveFullRun.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe( "${{ inputs.full_release_validation_run_attempt }}", ); expect(resolveJob.outputs?.full_release_validation_run_attempt).toBe( "${{ steps.full_run.outputs.attempt }}", ); expect(resolveDownload.with?.name).toBe( "full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ steps.full_run.outputs.attempt }}", ); expect(trustedTooling.env?.WORKFLOW_SHA).toBe("${{ github.sha }}"); expect(validateManifest.env).toMatchObject({ RUN_JSON_FILE: "${{ runner.temp }}/full-release-validation-run.json", TRUSTED_MAIN_REF: "refs/remotes/origin/main", VALIDATOR_FILE: "${{ runner.temp }}/release-validation-tooling/validate-full-release-validation-evidence.mjs", STRICT_VALIDATOR_FILE: "${{ runner.temp }}/release-validation-tooling/release-ci-summary.mjs", }); expect(validateManifest.run).toContain( 'MANIFEST_FILE="$manifest" node "$VALIDATOR_FILE" < "$RUN_JSON_FILE"', ); expect(publishDownload.with?.name).toBe( "full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ needs.resolve_release_target.outputs.full_release_validation_run_attempt }}", ); expect(publishOrchestration.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe( "${{ needs.resolve_release_target.outputs.full_release_validation_run_attempt }}", ); expect(publishOrchestration.run).toContain('"${validation_target_sha}" != "${TARGET_SHA}"'); expect(npmFullRun.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe( "${{ inputs.full_release_validation_run_attempt }}", ); expect(npmDownload.with?.name).toBe( "full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ inputs.full_release_validation_run_attempt }}", ); expect(npmTarget.env?.FULL_RELEASE_VALIDATION_RUN_ID).toBeUndefined(); expect(npmTarget.run).not.toContain( "node scripts/openclaw-npm-extended-stable-release.mjs verify-manifest", ); expect(npmCheckout.with?.["fetch-depth"]).toBe( "${{ inputs.preflight_run_id != '' && 1 || 0 }}", ); const publishSteps = publishJob.steps ?? []; const setupIndex = publishSteps.findIndex((step) => step.name === "Setup Node environment"); const notesIndex = publishSteps.findIndex( (step) => step.name === "Prepare GitHub release notes", ); const androidApprovalIndex = publishSteps.findIndex( (step) => step.name === "Write Android release approval", ); const dispatchIndex = publishSteps.findIndex( (step) => step.name === "Dispatch publish workflows", ); expect(setupIndex).toBeGreaterThan(-1); expect(notesIndex).toBeGreaterThan(setupIndex); expect(androidApprovalIndex).toBeGreaterThan(notesIndex); expect(dispatchIndex).toBeGreaterThan(notesIndex); expect(publishSteps[notesIndex]?.if).toBe("${{ inputs.publish_openclaw_npm }}"); const publishRun = publishOrchestration.run ?? ""; const createReleaseIndex = publishRun.lastIndexOf("create_or_update_github_release"); const verifyReleaseIndex = publishRun.lastIndexOf("verify_published_release"); const appendProofIndex = publishRun.lastIndexOf("append_release_proof_to_github_release"); const finalizeJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "finalize_github_release"); const finalizeRelease = workflowStep(finalizeJob, "Publish the verified draft release"); expect(createReleaseIndex).toBeGreaterThanOrEqual(0); expect(verifyReleaseIndex).toBeGreaterThan(createReleaseIndex); expect(appendProofIndex).toBeGreaterThan(verifyReleaseIndex); expect(finalizeJob.needs).toEqual(["publish", "publish_docker"]); expect(finalizeJob.if).toContain("needs.publish_docker.result == 'success'"); expect(finalizeRelease.run).toContain('gh release edit "${RELEASE_TAG}"'); }); it("accepts tag-matched frozen release branches in OpenClaw npm preflight", () => { const preflight = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "preflight_openclaw_npm"); const metadata = workflowStep(preflight, "Validate release metadata"); expect(metadata.run).toContain("git merge-base --is-ancestor"); expect(metadata.run).toContain('RELEASE_BRANCH_NAME="release/${BASH_REMATCH[1]}"'); expect(metadata.run).toContain( 'git fetch --no-tags origin "+refs/heads/${RELEASE_BRANCH_NAME}:${RELEASE_BRANCH_REF}"', ); expect(metadata.run).toContain('[[ "${RELEASE_REF}" == *"-alpha."* ]]'); }); it("gates stable GitHub publication on the Windows Hub release asset contract", () => { const releaseWorkflow = readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8"); const windowsWorkflow = readFileSync(WINDOWS_NODE_RELEASE_WORKFLOW, "utf8"); const releaseDocs = readFileSync("docs/reference/RELEASING.md", "utf8"); const releaseSkill = readFileSync(RELEASE_MAINTAINER_SKILL, "utf8"); expect(releaseWorkflow).toContain( "Stable OpenClaw publish requires an explicit windows_node_tag.", ); expect(releaseWorkflow).toContain( "Stable OpenClaw publish requires candidate-approved windows_node_installer_digests.", ); expect(releaseWorkflow).toContain("promote_windows_release_assets()"); expect(releaseWorkflow).toContain("dispatch_workflow windows-node-release.yml"); expect(releaseWorkflow).toContain("verify_windows_release_asset_contract"); expect(releaseWorkflow).toContain("Validate stable Windows source release"); expect(releaseWorkflow).toContain("id: windows_source"); expect(releaseWorkflow).toContain( "windows_node_installer_digests: ${{ steps.windows_source.outputs.installer_digests }}", ); expect(releaseWorkflow).toContain( "APPROVED_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}", ); expect(releaseWorkflow).toContain("no longer matches its candidate-approved digest"); expect(releaseWorkflow).toContain( "WINDOWS_NODE_INSTALLER_DIGESTS: ${{ needs.resolve_release_target.outputs.windows_node_installer_digests }}", ); expect(releaseWorkflow).toContain( '-f expected_installer_digests="${WINDOWS_NODE_INSTALLER_DIGESTS}"', ); expect(releaseWorkflow).toContain("missing prevalidated Windows installer digests"); expect(releaseWorkflow).toContain("does not match its pinned digest"); expect(releaseWorkflow).toContain( "Stable release OpenClawCompanion asset names do not exactly match the current contract", ); expect(releaseWorkflow).toContain('select(.name | startswith("OpenClawCompanion-"))'); expect(releaseWorkflow).toContain( "Windows checksum manifest does not exactly match the installer asset contract", ); expect(releaseWorkflow).toContain("Windows checksum manifest contains malformed entries"); expect(releaseWorkflow).toContain("([.[].name] | unique | length) == length"); expect(releaseWorkflow).toContain("Windows checksum manifest does not match pinned digest"); expect(releaseWorkflow).toContain( "Windows source release ${WINDOWS_NODE_TAG} must contain exactly one required asset", ); expect(releaseWorkflow.indexOf("Validate stable Windows source release")).toBeLessThan( releaseWorkflow.indexOf("\n publish:\n"), ); const createDraftCall = releaseWorkflow.lastIndexOf( "\n create_or_update_github_release\n", ); const promoteWindowsCall = releaseWorkflow.lastIndexOf( "\n if promote_windows_release_assets; then\n", ); expect(createDraftCall).toBeGreaterThan(-1); expect(promoteWindowsCall).toBeGreaterThan(createDraftCall); expect(releaseWorkflow).toContain("finalize_github_release:"); expect(windowsWorkflow).not.toContain("default: latest"); expect(windowsWorkflow).toContain("expected_installer_digests:"); expect(windowsWorkflow).toContain("expected_installer_digests must contain exactly"); expect(windowsWorkflow).toContain("must be an explicit openclaw-windows-node release tag"); expect(windowsWorkflow).toContain("$installerPatterns = @("); expect(windowsWorkflow).toContain("Every matched installer is signature-checked"); expect(windowsWorkflow).toContain("Get-ChildItem -LiteralPath dist -File"); expect(windowsWorkflow).toContain( "Downloaded Windows source asset does not match pinned digest", ); expect(windowsWorkflow).toContain( "--repo openclaw/openclaw-windows-node --json tagName,isDraft,isPrerelease,assets,url", ); expect(windowsWorkflow).toContain( "Windows source release must contain exactly one required asset", ); expect(windowsWorkflow).toContain( "Windows source release asset digest does not match the pinned digest", ); expect(windowsWorkflow).toContain( "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US", ); expect(windowsWorkflow).toContain("has unexpected signer subject"); expect(windowsWorkflow).toContain("OpenClawCompanion-SHA256SUMS.txt"); expect(windowsWorkflow).toContain("Verify promoted release asset contract"); expect(windowsWorkflow).toContain( "Promoted OpenClawCompanion asset names do not exactly match the current contract", ); expect(windowsWorkflow).toContain( "$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets", ); expect(windowsWorkflow).toContain("Promoted Windows SHA-256 manifest does not match"); expect(windowsWorkflow).toContain("Promoted Windows release asset checksum mismatch"); expect(releaseDocs).toContain( "the selected `windows_node_tag`, its saved `windows_node_installer_digests`,", ); expect(releaseDocs).toContain( "candidate-approved `windows_node_installer_digests`, and verify the canonical", ); expect(releaseSkill).toContain( "candidate-approved installer digest map as `windows_node_installer_digests`.", ); }); it("gates stable GitHub publication on the signed Android APK contract", () => { const releaseWorkflow = readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8"); const androidWorkflow = readFileSync(ANDROID_RELEASE_WORKFLOW, "utf8"); const androidDocs = readFileSync("docs/platforms/android.md", "utf8"); const releaseDocs = readFileSync("docs/reference/RELEASING.md", "utf8"); const approvalScript = readFileSync("scripts/validate-release-publish-approval.mjs", "utf8"); expect(androidWorkflow).toContain("environment: android-release"); expect(androidWorkflow).toContain( "actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1", ); expect(androidWorkflow).toContain("repositories: apps-signing"); expect(androidWorkflow).toContain("permission-contents: read"); expect(androidWorkflow).toContain("--mode materialize"); expect(androidWorkflow).not.toContain("APPS_SIGNING_DEPLOY_KEY"); expect(androidWorkflow).toContain("MATCH_PASSWORD"); expect(androidWorkflow).toContain("scripts/validate-release-publish-approval.mjs"); expect(releaseWorkflow).toContain("Write Android release approval"); expect(releaseWorkflow).toContain("Attest Android release approval"); expect(releaseWorkflow).toContain("Upload Android release approval"); expect(releaseWorkflow).toContain("android-release-approval-${{ github.run_id }}"); expect(releaseWorkflow).toContain("parentRunId: process.env.RELEASE_PUBLISH_RUN_ID"); expect(releaseWorkflow).toContain("releaseTag: process.env.RELEASE_TAG"); expect(releaseWorkflow).toContain("targetSha: process.env.TARGET_SHA"); expect(androidWorkflow).toContain("Download parent release approval"); expect(androidWorkflow).toContain( "android-release-approval-${{ inputs.release_publish_run_id }}", ); expect(androidWorkflow).toContain( '--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/openclaw-release-publish.yml"', ); expect(androidWorkflow).toContain('--source-ref "refs/heads/${EXPECTED_WORKFLOW_BRANCH}"'); expect(approvalScript).toContain( "Attested Android release approval does not match this run request.", ); expect(androidWorkflow).toContain('--artifact", "third-party'); expect(androidWorkflow).toContain("OpenClaw-Android.apk"); expect(androidWorkflow).toContain("OpenClaw-Android-SHA256SUMS.txt"); expect(androidWorkflow).toContain("actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6"); expect(androidWorkflow).toContain("--signer-workflow"); expect(androidWorkflow).toContain('--source-ref "refs/tags/${RELEASE_TAG}"'); expect(androidWorkflow).toContain("--deny-self-hosted-runners"); expect(androidWorkflow).toContain("--verify-apk"); expect(androidWorkflow).toContain('expected_source_ref="refs/tags/${RELEASE_TAG}"'); expect(androidWorkflow).toContain("release_target_sha must be a full lowercase commit SHA"); expect(androidWorkflow).toContain("does not match ${RELEASE_TAG} (${tag_sha})"); expect(androidWorkflow).toContain( "must resolve to the same source commit as ${fallback_base_tag}", ); expect(androidWorkflow).toContain("FALLBACK_ANDROID_BASE_TAG"); expect(androidWorkflow).toContain("FALLBACK_ANDROID_BASE_SHA"); expect(androidWorkflow).toContain('--source-digest "${FALLBACK_ANDROID_BASE_SHA}"'); expect(androidWorkflow).toContain("steps.release_source.outputs.fallback_base_tag == ''"); expect(androidWorkflow).toContain( "OPENCLAW_BUILD_TIMESTAMP: ${{ steps.release_approval.outputs.build_timestamp }}", ); expect(androidWorkflow).toContain("GIT_COMMIT: ${{ inputs.release_target_sha }}"); expect(androidWorkflow).toContain("--json tagName,isDraft,isPrerelease,createdAt,assets,url"); expect(androidWorkflow).toContain("release_created_at="); expect(androidWorkflow).toContain( "Reusing verified Android APK from ${FALLBACK_ANDROID_BASE_TAG}", ); expect(androidWorkflow).toContain("Existing Android release asset ${asset_name} differs"); expect(androidWorkflow).not.toContain("--clobber"); expect(releaseWorkflow).toContain("promote_android_release_asset()"); expect(releaseWorkflow).toContain("is_android_release()"); expect(androidWorkflow).toContain("requires a final or correction OpenClaw release tag"); expect(androidWorkflow).toContain("previous_version_code"); expect(androidWorkflow).toContain("must exceed ${previous_tag} versionCode"); expect(androidWorkflow).toContain("standalone channel bootstrap"); expect(releaseWorkflow).toContain( 'dispatch_workflow_at_ref "${RELEASE_TAG}" "${TARGET_SHA}" android-release.yml', ); expect(releaseWorkflow).toContain('-f release_target_sha="${TARGET_SHA}"'); expect(releaseWorkflow).toContain("verify_android_release_asset_contract"); expect(releaseWorkflow).toContain("Android release APK digest does not match"); expect(releaseWorkflow).toContain("Android APK asset contract: verified"); const createDraftCall = releaseWorkflow.lastIndexOf( "\n create_or_update_github_release\n", ); const promoteAndroidCall = releaseWorkflow.lastIndexOf( "\n if promote_android_release_asset; then\n", ); expect(createDraftCall).toBeGreaterThan(-1); expect(promoteAndroidCall).toBeGreaterThan(createDraftCall); expect(releaseWorkflow).toContain("finalize_github_release:"); expect(androidDocs).toContain("github.com/openclaw/openclaw/releases"); expect(androidDocs).not.toContain("releases/latest/download/OpenClaw-Android.apk"); expect(androidDocs).toContain("gh attestation verify OpenClaw-Android.apk"); expect(androidDocs).toContain('--source-ref "refs/tags/${release_tag}"'); expect(releaseDocs).toContain("signed standalone Android APK"); }); it("rejects malformed Windows checksum manifest lines before parsing entries", () => { const releaseWorkflow = readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8"); const validateManifestLinesIndex = releaseWorkflow.indexOf("all(.[]; test("); const parseManifestLinesIndex = releaseWorkflow.indexOf("map(capture("); expect(validateManifestLinesIndex).toBeGreaterThan(-1); expect(parseManifestLinesIndex).toBeGreaterThan(validateManifestLinesIndex); expect(releaseWorkflow).toContain('else error("malformed Windows checksum manifest entry")'); }); it("rejects unsafe direct Windows recovery before uploading assets", () => { const windowsWorkflow = readFileSync(WINDOWS_NODE_RELEASE_WORKFLOW, "utf8"); const classifyStableReleaseIndex = windowsWorkflow.indexOf("$stableRelease = -not ("); const rejectPrereleaseSourceIndex = windowsWorkflow.indexOf( "if ($stableRelease -and $sourceRelease.isPrerelease)", ); const rejectUnexpectedTargetAssetsIndex = windowsWorkflow.indexOf( "Target OpenClaw release contains unexpected OpenClawCompanion assets before upload", ); const uploadAssetsIndex = windowsWorkflow.indexOf("gh release upload $env:RELEASE_TAG"); expect(classifyStableReleaseIndex).toBeGreaterThan(-1); expect(rejectPrereleaseSourceIndex).toBeGreaterThan(classifyStableReleaseIndex); expect(windowsWorkflow).not.toContain("-not $targetRelease.isPrerelease"); expect(rejectUnexpectedTargetAssetsIndex).toBeGreaterThan(-1); expect(uploadAssetsIndex).toBeGreaterThan(rejectUnexpectedTargetAssetsIndex); }); it("publish requires the credentialed authorization path", () => { for (const [workflowPath, authorizationJobName, gatedJobName, expectedBranch] of [ [ PLUGIN_NPM_RELEASE_WORKFLOW, "validate_release_publish_approval", "publish_plugins_npm", "${{ github.ref_name }}", ], [ PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "validate_release_publish_approval", "pack_plugins_clawhub_artifacts", "${{ inputs.release_publish_branch || github.ref_name }}", ], [ OPENCLAW_NPM_RELEASE_WORKFLOW, "validate_publish_request", "publish_openclaw_npm", "${{ github.ref_name }}", ], [ ".github/workflows/plugin-clawhub-new.yml", "validate_release_publish_approval", "publish_bootstrap_plugins", "${{ inputs.release_publish_branch }}", ], ] as const) { const authorizationJob = workflowJob(workflowPath, authorizationJobName); const authorization = workflowStep(authorizationJob, "Validate release publish approval run"); const gatedJob = workflowJob(workflowPath, gatedJobName); const needs = Array.isArray(gatedJob.needs) ? gatedJob.needs : [gatedJob.needs]; expect(needs, workflowPath).toContain(authorizationJobName); expect(authorization.env, workflowPath).toMatchObject({ EXPECTED_WORKFLOW_BRANCH: expectedBranch, RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}", }); expectTextToIncludeAll(authorization.run, [ '${GITHUB_ACTOR}" != "github-actions[bot]"', "validate-release-publish-approval.mjs", ]); } for (const [workflowPath, publishJobName, environment] of [ [PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm", "npm-release"], [OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm", "npm-release"], [ ".github/workflows/plugin-clawhub-new.yml", "publish_bootstrap_plugins", "clawhub-plugin-bootstrap", ], ] as const) { expect(workflowJob(workflowPath, publishJobName).environment, workflowPath).toBe(environment); } const clawHubApproval = workflowJob( PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "approve_plugins_clawhub_release", ); const clawHubPublish = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "publish_plugins_clawhub"); expect(clawHubApproval.environment).toBe("clawhub-plugin-release"); expect(clawHubPublish.needs).toContain("approve_plugins_clawhub_release"); const bootstrapWorkflow = ".github/workflows/plugin-clawhub-new.yml"; const authorizationJob = workflowJob(bootstrapWorkflow, "validate_release_publish_approval"); const approvalDownload = workflowStep( authorizationJob, "Download parent ClawHub bootstrap approval", ); const authorization = workflowStep(authorizationJob, "Validate release publish approval run"); expect(authorizationJob.permissions).toMatchObject({ actions: "read", attestations: "read", contents: "read", }); expect(approvalDownload.with).toMatchObject({ name: "clawhub-bootstrap-approval-${{ inputs.release_publish_run_id }}-${{ inputs.release_publish_run_attempt }}", "run-id": "${{ inputs.release_publish_run_id }}", }); expect(authorization.env).toMatchObject({ APPROVAL_PATH: "${{ runner.temp }}/clawhub-bootstrap-approval/approval.json", CHILD_WORKFLOW_SHA: "${{ github.sha }}", EXPECTED_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}", EXPECTED_WORKFLOW_BRANCH: "${{ inputs.release_publish_branch }}", RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}", RELEASE_TARGET_SHA: "${{ needs.resolve_bootstrap_plan.outputs.ref_revision }}", }); expectTextToIncludeAll(authorization.run, [ '${GITHUB_ACTOR}" != "github-actions[bot]"', "actions/runs/${RELEASE_PUBLISH_RUN_ID}/attempts/${EXPECTED_RUN_ATTEMPT}", '--source-ref "${EXPECTED_WORKFLOW_REF}"', '--source-digest "${EXPECTED_WORKFLOW_SHA}"', "validate-release-publish-approval.mjs", ]); }); it("keeps release publication ownership and artifact boundaries wired", () => { const packageJson = JSON.parse(readFileSync(PACKAGE_JSON, "utf8")) as { scripts?: Record; }; const releasePublishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"); const releaseSteps = releasePublishJob.steps ?? []; const clawHubApproval = workflowJob( PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "approve_plugins_clawhub_release", ); const clawHubPublish = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "publish_plugins_clawhub"); const clawHubBootstrapValidation = workflowJob( ".github/workflows/plugin-clawhub-new.yml", "validate_bootstrap_artifact", ); const clawHubBootstrapPublish = workflowJob( ".github/workflows/plugin-clawhub-new.yml", "publish_bootstrap_plugins", ); const postpublishEvidence = workflowStep(releasePublishJob, "Upload postpublish evidence"); expect(packageJson.scripts).toMatchObject({ "release:verify-beta": "node --import tsx scripts/release-verify-beta.ts", "release:candidate": "node --import tsx scripts/release-candidate-checklist.mts", "release:beta": "node --import tsx scripts/release-candidate-checklist.mts", "release:fast-pretag-check": "bash scripts/release-fast-pretag-check.sh", }); expect(workflowStep(releasePublishJob, "Setup Node environment").with).toMatchObject({ "install-bun": "false", "install-deps": "false", }); expect(workflowStep(releasePublishJob, "Checkout trusted release tooling")).toBeDefined(); expect( workflowStep(releasePublishJob, "Install trusted release tooling dependencies"), ).toBeDefined(); expect(workflowStep(releasePublishJob, "Resolve ClawHub release plan")).toBeDefined(); expect(workflowStep(releasePublishJob, "Dispatch publish workflows")).toBeDefined(); expect(clawHubApproval.environment).toBe("clawhub-plugin-release"); expect(clawHubPublish.needs).toEqual([ "preview_plugins_clawhub", "pack_plugins_clawhub_artifacts", "approve_plugins_clawhub_release", ]); expect(clawHubPublish.uses).toBe( "openclaw/clawhub/.github/workflows/package-publish.yml@d8096dfc039e86ab942ddf9ef117d04849fd84c1", ); expect(clawHubPublish.permissions).toMatchObject({ actions: "read", contents: "read", "id-token": "write", }); expect(clawHubBootstrapValidation.environment).toBe("clawhub-plugin-bootstrap"); expect(clawHubBootstrapPublish.environment).toBe("clawhub-plugin-bootstrap"); const bootstrapSteps = clawHubBootstrapPublish.steps ?? []; const bootstrapDownload = workflowStep( clawHubBootstrapPublish, "Download and verify immutable ClawHub bootstrap artifact", ); const bootstrapRehash = workflowStep( clawHubBootstrapPublish, "Rehash immutable ClawHub bootstrap artifacts", ); const bootstrapRegistry = workflowStep( clawHubBootstrapPublish, "Reconfirm configure-only registry bytes before credentials", ); const bootstrapTag = workflowStep( clawHubBootstrapPublish, "Reconfirm release tag before credentials", ); const bootstrapCredentials = workflowStep( clawHubBootstrapPublish, "Write ClawHub token config", ); expect(bootstrapDownload.run).toContain("clawhub-bootstrap-artifact.mjs download"); expect(bootstrapDownload.run).toContain('--target-sha "${TARGET_SHA}"'); expect(bootstrapDownload.run).toContain('--workflow-sha "${WORKFLOW_SHA}"'); expect(bootstrapTag.run).toContain('rev-parse "${RELEASE_TAG}^{commit}"'); expect(bootstrapSteps.indexOf(bootstrapDownload)).toBeLessThan( bootstrapSteps.indexOf(bootstrapRehash), ); expect(bootstrapSteps.indexOf(bootstrapRehash)).toBeLessThan( bootstrapSteps.indexOf(bootstrapRegistry), ); expect(bootstrapSteps.indexOf(bootstrapRegistry)).toBeLessThan( bootstrapSteps.indexOf(bootstrapTag), ); expect(bootstrapSteps.indexOf(bootstrapTag)).toBeLessThan( bootstrapSteps.indexOf(bootstrapCredentials), ); expect(postpublishEvidence.if).toContain("always()"); expect(postpublishEvidence.if).toContain("inputs.publish_openclaw_npm"); expect(postpublishEvidence.with).toMatchObject({ "if-no-files-found": "error", name: "openclaw-release-postpublish-evidence-${{ inputs.tag }}", path: "${{ runner.temp }}/openclaw-release-postpublish-evidence", }); expect(postpublishEvidence.uses).toBe(UPLOAD_ARTIFACT_V7); const notesIndex = releaseSteps.findIndex( (step) => step.name === "Prepare GitHub release notes", ); const dispatchIndex = releaseSteps.findIndex( (step) => step.name === "Dispatch publish workflows", ); const evidenceIndex = releaseSteps.findIndex( (step) => step.name === "Upload postpublish evidence", ); expect(notesIndex).toBeGreaterThan(-1); expect(dispatchIndex).toBeGreaterThan(notesIndex); expect(evidenceIndex).toBeGreaterThan(dispatchIndex); const clawHubReleaseSource = readFileSync(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "utf8"); const clawHubBootstrapSource = readFileSync(".github/workflows/plugin-clawhub-new.yml", "utf8"); expect(clawHubReleaseSource).not.toContain("secrets.CLAWHUB_TOKEN"); expect(clawHubReleaseSource).not.toContain("clawhub_token:"); expect(clawHubBootstrapSource).toContain("secrets.CLAWHUB_TOKEN"); }); it("bounds the npm registry tarball download used for release resume", () => { const publishRun = workflowStep(workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"), "Dispatch publish workflows") .run ?? ""; const resolvePublishState = shellFunctionSource( publishRun, "resolve_openclaw_npm_publish_state", ); const registryDownload = resolvePublishState.match( /curl -fsSL[\s\S]*?"\$\{published_tarball_url\}"/u, )?.[0]; expect(registryDownload).toContain("--connect-timeout 10"); expect(registryDownload).toContain("--max-time 120"); expect(registryDownload).toContain("--retry 3"); expect(registryDownload).toContain("--retry-max-time 180"); expect(registryDownload).toContain('-o "${published_tarball_path}"'); }); it("fails closed when child environment identity or approval mutation fails", () => { const publishRun = workflowStep(workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"), "Dispatch publish workflows") .run ?? ""; const verifyChild = shellFunctionSource(publishRun, "verify_child_run_sha"); const approvePending = shellFunctionSource(publishRun, "approve_pending_deployments"); const approveChild = shellFunctionSource(publishRun, "approve_child_publish_environment"); const waitForRun = shellFunctionSource(publishRun, "wait_for_run"); const expectedSha = "a".repeat(40); const mutationFailure = spawnSync( "bash", [ "-c", ` set -uo pipefail GITHUB_REPOSITORY=openclaw/openclaw gh() { if [[ "$1" == "run" && "$2" == "view" ]]; then printf '%s\\n' '{"headSha":"${expectedSha}","url":"https://example.invalid/run/123"}' return 0 fi if [[ "$1" == "api" && "$2" == "-X" && "$3" == "GET" ]]; then printf '%s\\n' '[{"environment":{"id":7,"name":"clawhub-plugin-bootstrap"},"current_user_can_approve":true}]' return 0 fi if [[ "$1" == "api" && "$2" == "-X" && "$3" == "POST" ]]; then return 42 fi return 99 } ${verifyChild} ${approvePending} status=0 approve_pending_deployments plugin-clawhub-new.yml 123 "${expectedSha}" || status=$? [[ "$status" -eq 2 ]] `, ], { encoding: "utf8" }, ); expect(mutationFailure.status, mutationFailure.stderr).toBe(0); const mismatchedApprovedSha = spawnSync( "bash", [ "-c", ` set -uo pipefail GITHUB_REPOSITORY=openclaw/openclaw GITHUB_STEP_SUMMARY=/dev/null gh() { if [[ "$1" == "run" && "$2" == "view" ]]; then printf '%s\\n' '{"headSha":"${"b".repeat(40)}","url":"https://example.invalid/run/123"}' return 0 fi if [[ "$1" == "run" && "$2" == "cancel" ]]; then return 0 fi return 99 } print_failed_run_summary() { :; } ${verifyChild} ${approvePending} ${approveChild} status=0 approve_child_publish_environment plugin-clawhub-new.yml 123 "${expectedSha}" || status=$? [[ "$status" -eq 2 ]] `, ], { encoding: "utf8" }, ); expect(mismatchedApprovedSha.status, mismatchedApprovedSha.stderr).toBe(0); const mismatchedWaitSha = spawnSync( "bash", [ "-c", ` set -uo pipefail GITHUB_REPOSITORY=openclaw/openclaw gh() { if [[ "$1" == "run" && "$2" == "view" ]]; then printf '%s\\n' '{"headSha":"${"b".repeat(40)}","url":"https://example.invalid/run/123"}' return 0 fi if [[ "$1" == "run" && "$2" == "cancel" ]]; then return 0 fi return 99 } ${verifyChild} ${waitForRun} status=0 wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$? [[ "$status" -eq 1 ]] `, ], { encoding: "utf8" }, ); expect(mismatchedWaitSha.status, mismatchedWaitSha.stderr).toBe(0); }); it("keeps release workflow setup aligned", () => { const releaseChecks = readWorkflow(RELEASE_CHECKS_WORKFLOW); const installSmoke = readWorkflow(INSTALL_SMOKE_REUSABLE_WORKFLOW); const crossOs = readWorkflow(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW); const liveE2e = readWorkflow(LIVE_E2E_WORKFLOW); const qaLive = readWorkflow(QA_LIVE_TRANSPORTS_WORKFLOW); const releaseWorkflowPaths = [ FULL_RELEASE_VALIDATION_WORKFLOW, RELEASE_CHECKS_WORKFLOW, RELEASE_TELEGRAM_QA_WORKFLOW, CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, LIVE_E2E_WORKFLOW, NPM_TELEGRAM_WORKFLOW, ".github/workflows/openclaw-release-publish.yml", ".github/workflows/android-release.yml", ".github/workflows/openclaw-npm-release.yml", ".github/workflows/macos-release.yml", ".github/workflows/plugin-clawhub-release.yml", PACKAGE_ACCEPTANCE_WORKFLOW, PLUGIN_NPM_RELEASE_WORKFLOW, ]; for (const workflowPath of releaseWorkflowPaths) { const workflow = readWorkflow(workflowPath); expect(workflow.env?.NODE_VERSION, workflowPath).toBe("24.16.0"); expect(workflow.env?.PNPM_VERSION, workflowPath).toBeUndefined(); } expect(releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"]).toBe(15); expect( workflowStep( workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package"), "Setup Node environment", ).with?.["install-deps"], ).toBe("true"); expect(installSmoke.jobs?.preflight?.["timeout-minutes"]).toBe(15); expect(installSmoke.jobs?.["install-smoke-fast"]?.["timeout-minutes"]).toBe(120); expect(installSmoke.jobs?.root_dockerfile_image?.["timeout-minutes"]).toBe(60); expect(installSmoke.jobs?.root_dockerfile_image_ready?.["timeout-minutes"]).toBe(5); expect(installSmoke.jobs?.qr_package_install_smoke?.["timeout-minutes"]).toBe(30); expect(installSmoke.jobs?.root_dockerfile_smokes?.["timeout-minutes"]).toBe(90); expect(installSmoke.jobs?.installer_smoke?.["timeout-minutes"]).toBe(150); expect(installSmoke.jobs?.bun_global_install_smoke?.["timeout-minutes"]).toBe(60); expect(installSmoke.jobs?.["docker-e2e-fast"]?.["timeout-minutes"]).toBe(12); expect(crossOs.jobs?.prepare?.["timeout-minutes"]).toBe(90); expect(crossOs.jobs?.cross_os_release_checks?.["timeout-minutes"]).toBe(60); expect(qaLive.jobs?.authorize_actor?.["timeout-minutes"]).toBe(10); expect(qaLive.jobs?.validate_selected_ref?.["timeout-minutes"]).toBe(30); expect(liveE2e.jobs?.validate_live_suite_filter?.["timeout-minutes"]).toBe(10); expect(liveE2e.jobs?.plan_release_workflow_matrices?.["timeout-minutes"]).toBe(10); expect(liveE2e.jobs?.validate_release_live_cache?.["timeout-minutes"]).toBe(20); expect(readFileSync(LIVE_E2E_WORKFLOW, "utf8")).toContain( "timeout --foreground --kill-after=30s 8m pnpm test:live:cache", ); expect(readFileSync(LIVE_E2E_WORKFLOW, "utf8")).toContain("live-cache attempt ${attempt}/2"); }); it("keeps known bounded dominant child paths below parent monitors", () => { const fullRelease = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW); const pluginPrerelease = readWorkflow(PLUGIN_PRERELEASE_WORKFLOW); const liveE2e = readWorkflow(LIVE_E2E_WORKFLOW); const releaseChecks = readWorkflow(RELEASE_CHECKS_WORKFLOW); const installSmoke = readWorkflow(INSTALL_SMOKE_REUSABLE_WORKFLOW); const crossOs = readWorkflow(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW); const packageAcceptance = readWorkflow(PACKAGE_ACCEPTANCE_WORKFLOW); const qaLive = readWorkflow(QA_LIVE_TRANSPORTS_WORKFLOW); const profiles = ["beta", "stable", "full"] as const; const ciPreflight = workflowJob(CI_WORKFLOW, "preflight"); const ciIos = workflowJob(CI_WORKFLOW, "ios-build"); const ciGate = workflowJob(CI_WORKFLOW, "ci-gate"); expect(jobNeeds(ciIos)).toEqual(["preflight"]); expect(jobNeeds(ciGate)).toEqual(expect.arrayContaining(["preflight", "ios-build"])); const ciPath = [ timeoutForProfile(ciPreflight["timeout-minutes"], "beta"), timeoutForProfile(ciIos["timeout-minutes"], "beta"), timeoutForProfile(ciGate["timeout-minutes"], "beta"), ]; expect(ciPath).toEqual([20, 150, 5]); const ciChildTimeout = ciPath.reduce((total, timeout) => total + timeout, 0); expect(ciChildTimeout).toBe(175); const ciParentTimeout = timeoutForProfile( fullRelease.jobs?.normal_ci?.["timeout-minutes"], "beta", ); expect(ciChildTimeout).toBeLessThanOrEqual(ciParentTimeout); expect(ciParentTimeout - ciChildTimeout).toBeGreaterThanOrEqual(60); expect(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"]).toBe(30); const pluginMonitorTimeout = fullRelease.jobs?.plugin_prerelease?.["timeout-minutes"]; const pluginChildTimeouts = Object.fromEntries( profiles.map((profile) => [ profile, pluginPrereleaseTimeoutFloor(pluginPrerelease, liveE2e, profile), ]), ) as Record<(typeof profiles)[number], number>; const pluginParentTimeouts = Object.fromEntries( profiles.map((profile) => [profile, timeoutForProfile(pluginMonitorTimeout, profile)]), ) as Record<(typeof profiles)[number], number>; expect(pluginChildTimeouts).toEqual({ beta: 175, stable: 175, full: 205 }); expect(pluginParentTimeouts).toEqual({ beta: 240, stable: 240, full: 300 }); for (const profile of profiles) { expect( pluginParentTimeouts[profile] - pluginChildTimeouts[profile], `plugin-prerelease:${profile}`, ).toBeGreaterThanOrEqual(60); } const releasePackageJob = workflowJob( RELEASE_CHECKS_WORKFLOW, "package_acceptance_release_checks", ); expect(jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package"))).toEqual([ "resolve_target", ]); expect(jobNeeds(releasePackageJob)).toEqual(["resolve_target", "prepare_release_package"]); expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "package_integrity"))).toEqual([ "resolve_package", ]); expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "docker_acceptance"))).toEqual([ "resolve_package", "package_integrity", ]); expect(jobNeeds(workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"))).toEqual([ "validate_selected_ref", ]); expect(jobNeeds(workflowJob(LIVE_E2E_WORKFLOW, "docker_e2e_image_ready"))).toEqual([ "prepare_docker_e2e_image", ]); expect(jobNeeds(workflowJob(LIVE_E2E_WORKFLOW, "validate_docker_lanes"))).toEqual( expect.arrayContaining([ "validate_selected_ref", "prepare_docker_e2e_image", "docker_e2e_image_ready", ]), ); expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "summary"))).toContain( "docker_acceptance", ); expect(jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"))).toContain( "package_acceptance_release_checks", ); const releasePackagePaths = Object.fromEntries( profiles.map((profile) => [ profile, [ timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], profile), timeoutForProfile( releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"], profile, ), timeoutForProfile(packageAcceptance.jobs?.resolve_package?.["timeout-minutes"], profile), timeoutForProfile( packageAcceptance.jobs?.package_integrity?.["timeout-minutes"], profile, ), timeoutForProfile(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"], profile), timeoutForProfile(liveE2e.jobs?.prepare_docker_e2e_image?.["timeout-minutes"], profile), timeoutForProfile(liveE2e.jobs?.docker_e2e_image_ready?.["timeout-minutes"], profile), Math.max( ...evaluatedJobTimeouts( LIVE_E2E_WORKFLOW, "validate_docker_lanes", workflowJob(LIVE_E2E_WORKFLOW, "validate_docker_lanes"), ), ), timeoutForProfile(packageAcceptance.jobs?.summary?.["timeout-minutes"], profile), timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], profile), ], ]), ) as Record<(typeof profiles)[number], number[]>; expect(releasePackagePaths).toEqual({ beta: [30, 15, 60, 10, 30, 60, 5, 90, 5, 5], stable: [30, 15, 60, 10, 30, 60, 5, 90, 5, 5], full: [30, 15, 60, 10, 30, 90, 5, 90, 5, 5], }); const releaseChecksParent = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_checks"); expect(releaseChecksParent["runs-on"]).toBe("blacksmith-4vcpu-ubuntu-2404"); expect(releaseChecksParent["timeout-minutes"]).toBe(420); const releasePackageTimeouts = { beta: releasePackagePaths.beta.reduce((total, timeout) => total + timeout, 0), stable: releasePackagePaths.stable.reduce((total, timeout) => total + timeout, 0), full: releasePackagePaths.full.reduce((total, timeout) => total + timeout, 0), }; expect(releasePackageTimeouts).toEqual({ beta: 310, stable: 310, full: 340 }); for (const [profile, childTimeout] of Object.entries(releasePackageTimeouts)) { expect(childTimeout, `release-package:${profile}`).toBeLessThanOrEqual(420); expect(420 - childTimeout, `release-package:${profile}`).toBeGreaterThanOrEqual(60); } const releaseSummary = workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"); const releaseCrossOs = workflowJob(RELEASE_CHECKS_WORKFLOW, "cross_os_release_checks"); expect(jobNeeds(releaseCrossOs)).toEqual(["resolve_target", "prepare_release_package"]); expect(jobNeeds(workflowJob(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, "prepare"))).toEqual([]); expect( jobNeeds(workflowJob(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, "cross_os_release_checks")), ).toEqual(["prepare"]); expect(jobNeeds(releaseSummary)).toContain("cross_os_release_checks"); const releaseCrossOsPath = [ timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"), timeoutForProfile(releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"], "stable"), timeoutForProfile(crossOs.jobs?.prepare?.["timeout-minutes"], "stable"), timeoutForProfile(crossOs.jobs?.cross_os_release_checks?.["timeout-minutes"], "stable"), timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"), ]; expect(releaseCrossOsPath).toEqual([30, 15, 90, 60, 5]); const releaseInstall = workflowJob(RELEASE_CHECKS_WORKFLOW, "install_smoke_release_checks"); expect(jobNeeds(releaseInstall)).toEqual(["resolve_target"]); expect(jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "root_dockerfile_image"))).toEqual( ["preflight"], ); expect( jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "root_dockerfile_image_ready")), ).toEqual(["preflight", "root_dockerfile_image"]); expect(jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke"))).toEqual([ "preflight", "root_dockerfile_image", "root_dockerfile_image_ready", ]); expect(jobNeeds(releaseSummary)).toContain("install_smoke_release_checks"); const releaseInstallPath = [ timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"), timeoutForProfile(installSmoke.jobs?.preflight?.["timeout-minutes"], "stable"), timeoutForProfile(installSmoke.jobs?.root_dockerfile_image?.["timeout-minutes"], "stable"), timeoutForProfile( installSmoke.jobs?.root_dockerfile_image_ready?.["timeout-minutes"], "stable", ), timeoutForProfile(installSmoke.jobs?.installer_smoke?.["timeout-minutes"], "stable"), timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"), ]; expect(releaseInstallPath).toEqual([30, 15, 60, 5, 150, 5]); const releaseQaLive = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_release_checks"); expect(jobNeeds(releaseQaLive)).toEqual(["resolve_target"]); expect(jobNeeds(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "validate_selected_ref"))).toEqual([ "authorize_actor", ]); expect(jobNeeds(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_matrix"))).toEqual([ "authorize_actor", "validate_selected_ref", ]); expect(jobNeeds(releaseSummary)).toContain("qa_live_release_checks"); const releaseQaLivePath = [ timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"), timeoutForProfile(qaLive.jobs?.authorize_actor?.["timeout-minutes"], "stable"), timeoutForProfile(qaLive.jobs?.validate_selected_ref?.["timeout-minutes"], "stable"), timeoutForProfile(qaLive.jobs?.run_live_matrix?.["timeout-minutes"], "stable"), timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"), ]; expect(releaseQaLivePath).toEqual([30, 10, 30, 90, 5]); for (const [pathName, path] of [ ["cross-os", releaseCrossOsPath], ["install", releaseInstallPath], ["qa-live", releaseQaLivePath], ] as const) { const childTimeout = path.reduce((total, timeout) => total + timeout, 0); expect(childTimeout, `release-checks:${pathName}`).toBeLessThanOrEqual(420); expect(420 - childTimeout, `release-checks:${pathName}`).toBeGreaterThanOrEqual(60); } expect(releaseCrossOsPath.reduce((total, timeout) => total + timeout, 0)).toBe(200); expect(releaseInstallPath.reduce((total, timeout) => total + timeout, 0)).toBe(265); expect(releaseQaLivePath.reduce((total, timeout) => total + timeout, 0)).toBe(165); expect( jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_telegram_release_checks")), ).toEqual(["resolve_target"]); expect(jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"))).toContain( "qa_live_telegram_release_checks", ); const releaseTelegramPath = [ timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "beta"), timeoutForProfile( releaseChecks.jobs?.qa_live_telegram_release_checks?.["timeout-minutes"], "beta", ), timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "beta"), ]; expect(releaseTelegramPath).toEqual([30, 210, 5]); const releaseTelegramTimeout = releaseTelegramPath.reduce( (total, timeout) => total + timeout, 0, ); expect(releaseTelegramTimeout).toBe(245); expect(420 - releaseTelegramTimeout).toBeGreaterThanOrEqual(60); const npmTelegramChildTimeout = timeoutForProfile( workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e")["timeout-minutes"], "beta", ); expect(npmTelegramChildTimeout).toBe(60); for (const profile of profiles) { const parentTimeout = timeoutForProfile( fullRelease.jobs?.npm_telegram?.["timeout-minutes"], profile, ); expect(parentTimeout).toBe(profile === "full" ? 360 : 120); expect( parentTimeout - npmTelegramChildTimeout, `npm-telegram:${profile}`, ).toBeGreaterThanOrEqual(60); } const performanceResolve = workflowJob(PERFORMANCE_WORKFLOW, "resolve_target"); const performanceKova = workflowJob(PERFORMANCE_WORKFLOW, "kova"); const performanceSource = workflowJob(PERFORMANCE_WORKFLOW, "source_performance"); const performancePublish = workflowJob(PERFORMANCE_WORKFLOW, "publish"); const performanceArtifactGuard = workflowJob(PERFORMANCE_WORKFLOW, "artifact_only_guard"); expect(jobNeeds(performanceKova)).toEqual(["resolve_target"]); expect(jobNeeds(performanceSource)).toEqual(["resolve_target"]); expect(jobNeeds(performancePublish)).toEqual(["resolve_target", "kova", "source_performance"]); expect(jobNeeds(performanceArtifactGuard)).toEqual(["resolve_target", "kova", "publish"]); expect(performancePublish.if).toContain("inputs.publish_reports == true"); expect(performanceArtifactGuard.if).toContain("inputs.publish_reports != true"); expect(timeoutForProfile(performanceSource["timeout-minutes"], "beta")).toBeLessThanOrEqual( timeoutForProfile(performanceKova["timeout-minutes"], "beta"), ); const performanceArtifactPath = [ timeoutForProfile(performanceResolve["timeout-minutes"], "beta"), timeoutForProfile(performanceKova["timeout-minutes"], "beta"), timeoutForProfile(performanceArtifactGuard["timeout-minutes"], "beta"), ]; const performancePublishPath = [ timeoutForProfile(performanceResolve["timeout-minutes"], "beta"), timeoutForProfile(performanceKova["timeout-minutes"], "beta"), timeoutForProfile(performancePublish["timeout-minutes"], "beta"), ]; expect(performanceArtifactPath).toEqual([10, 240, 5]); expect(performancePublishPath).toEqual([10, 240, 30]); expect(performanceArtifactPath.reduce((total, timeout) => total + timeout, 0)).toBe(255); expect(performancePublishPath.reduce((total, timeout) => total + timeout, 0)).toBe(280); const performanceParent = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"); expect(performanceParent["timeout-minutes"]).toBe(360); expect( workflowStep(performanceParent, "Dispatch and monitor OpenClaw Performance").run, ).toContain("-f publish_reports=false"); for (const [pathName, path] of [ ["artifact-only", performanceArtifactPath], ["publish", performancePublishPath], ] as const) { const childTimeout = path.reduce((total, timeout) => total + timeout, 0); expect(childTimeout, `performance:${pathName}`).toBeLessThanOrEqual(360); expect(360 - childTimeout, `performance:${pathName}`).toBeGreaterThanOrEqual(60); } const prepareReleaseCandidate = workflowJob( FULL_RELEASE_VALIDATION_WORKFLOW, "prepare_release_candidate", ); expect(jobNeeds(workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "evidence_reuse"))).toEqual([ "resolve_target", ]); expect(jobNeeds(prepareReleaseCandidate)).toEqual(["resolve_target", "evidence_reuse"]); expect(prepareReleaseCandidate.with?.prepare_only).toBe(true); expect(jobNeeds(releaseChecksParent)).toEqual([ "resolve_target", "evidence_reuse", "prepare_release_candidate", ]); expect(jobNeeds(workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"))).toContain( "release_checks", ); const fullParentPath = [ timeoutForProfile(fullRelease.jobs?.resolve_target?.["timeout-minutes"], "full"), timeoutForProfile(fullRelease.jobs?.evidence_reuse?.["timeout-minutes"], "full"), timeoutForProfile(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"], "full"), timeoutForProfile(liveE2e.jobs?.prepare_docker_e2e_image?.["timeout-minutes"], "full"), timeoutForProfile(liveE2e.jobs?.docker_e2e_image_ready?.["timeout-minutes"], "full"), timeoutForProfile(releaseChecksParent["timeout-minutes"], "full"), timeoutForProfile(fullRelease.jobs?.summary?.["timeout-minutes"], "full"), ]; expect(fullParentPath).toEqual([10, 10, 30, 90, 5, 420, 5]); const fullParentTimeoutFloor = fullParentPath.reduce((total, timeout) => total + timeout, 0); expect(fullParentTimeoutFloor).toBe(570); expect(FULL_RELEASE_WAIT_TIMEOUT_MINUTES - fullParentTimeoutFloor).toBeGreaterThanOrEqual(60); }); it("bounds every direct job in nested release workflows", () => { const boundedWorkflowPaths = [ RELEASE_CHECKS_WORKFLOW, INSTALL_SMOKE_REUSABLE_WORKFLOW, CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, LIVE_E2E_WORKFLOW, PACKAGE_ACCEPTANCE_WORKFLOW, QA_LIVE_TRANSPORTS_WORKFLOW, RELEASE_TELEGRAM_QA_WORKFLOW, NPM_TELEGRAM_WORKFLOW, ]; for (const path of boundedWorkflowPaths) { const jobs = readWorkflow(path).jobs ?? {}; expect(Object.keys(jobs).length, path).toBeGreaterThan(0); for (const [jobName, job] of Object.entries(jobs)) { if (job.uses) { // GitHub does not allow timeout-minutes on reusable-workflow caller jobs. expect(job["timeout-minutes"], `${path}:${jobName}`).toBeUndefined(); continue; } const evaluatedTimeouts = evaluatedJobTimeouts(path, jobName, job); expect(evaluatedTimeouts.length, `${path}:${jobName}`).toBeGreaterThan(0); for (const timeout of evaluatedTimeouts) { expect(Number.isFinite(timeout), `${path}:${jobName}`).toBe(true); expect(timeout, `${path}:${jobName}`).toBeGreaterThan(0); } } } }); it("pins every documented raw Full Release Validation caller to one exact SHA", () => { const nightly = readFileSync(".agents/skills/release-openclaw-nightly/SKILL.md", "utf8"); const liveUpdater = readFileSync(".agents/skills/openclaw-live-updater/SKILL.md", "utf8"); const releaseCi = readFileSync(".agents/skills/release-openclaw-ci/SKILL.md", "utf8"); const releaseCiNotes = readFileSync( ".agents/skills/release-openclaw-ci/references/release-ci-notes.md", "utf8", ); const testing = readFileSync(".agents/skills/openclaw-testing/SKILL.md", "utf8"); const parallels = readFileSync(".agents/skills/openclaw-parallels-smoke/SKILL.md", "utf8"); const maintainer = readFileSync(RELEASE_MAINTAINER_SKILL, "utf8"); const ciDocs = readFileSync("docs/ci.md", "utf8"); const fullReleaseDocs = readFileSync("docs/reference/full-release-validation.md", "utf8"); const releasingDocs = readFileSync("docs/reference/RELEASING.md", "utf8"); expect(nightly).toContain('-f expected_sha="$SHA"'); expectTextToIncludeAll(liveUpdater, [ 'MAIN_SHA=""', '-f ref="$MAIN_SHA"', '-f expected_sha="$MAIN_SHA"', ]); for (const text of [releaseCi, fullReleaseDocs, releasingDocs]) { expectTextToIncludeAll(text, [ 'RELEASE_SHA="$(git rev-parse HEAD)"', "-f ref=extended-stable/YYYY.M.33", '-f expected_sha="$RELEASE_SHA"', ]); } expectTextToIncludeAll(ciDocs, [ 'VALIDATION_SHA=""', '-f ref="$VALIDATION_SHA"', '-f expected_sha="$VALIDATION_SHA"', ]); for (const text of [releaseCi, releaseCiNotes, testing, parallels, ciDocs, maintainer]) { expect(text).toContain("Validation SHA + Tooling SHA"); } expect(releaseCi).toContain("release lifecycle ledger: Code SHA, Release SHA, and Tooling SHA"); }); it("executes shared release candidate identity validation with its JSON input", () => { const selectedSha = "a".repeat(40); const candidate = { packageArtifactName: "docker-e2e-package-456-1", packageArtifactId: "123", packageArtifactDigest: "b".repeat(64), packageArtifactRunId: "456", packageArtifactRunAttempt: "1", packageFileName: "openclaw-current.tgz", packageSourceSha: selectedSha, packageSha256: "c".repeat(64), packageVersion: "2026.7.2", imageArtifactName: "docker-e2e-shared-images-123-1", imageArtifactId: "789", imageArtifactDigest: "d".repeat(64), imageArtifactRunId: "456", imageArtifactRunAttempt: "1", imageArchiveSha256: "e".repeat(64), }; const validation = workflowStep( workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package"), "Validate shared release candidate identity", ).run; expect(validation).toBeDefined(); const binDir = resolve(tempDirs.make("release-candidate-validation-"), "bin"); mkdirSync(binDir, { recursive: true }); const ghPath = resolve(binDir, "gh"); writeFileSync( ghPath, `#!/bin/sh if [ "$#" -ne 4 ] || [ "$1" != "api" ] || [ "$2" != "--method" ] || [ "$3" != "GET" ]; then exit 1 fi case "$4" in */actions/artifacts/123) printf '%s\n' '{"id":123,"name":"docker-e2e-package-456-1","expired":false,"digest":"sha256:${"b".repeat(64)}","workflow_run":{"id":456}}' ;; */actions/artifacts/790) printf '%s\n' '{"id":790,"name":"docker-e2e-prepublish-plugin-registry-456-1","expired":false,"digest":"sha256:${"f".repeat(64)}","workflow_run":{"id":456}}' ;; */actions/runs/456/attempts/1) printf '%s\n' '{"id":456,"run_attempt":1}' ;; *) exit 1 ;; esac `, ); chmodSync(ghPath, 0o755); const validationEnv = { ...process.env, GH_TOKEN: "test-token", GITHUB_REPOSITORY: "openclaw/openclaw", PATH: `${binDir}:${process.env.PATH ?? ""}`, SELECTED_SHA: selectedSha, }; const valid = spawnSync("bash", ["-c", validation ?? ""], { encoding: "utf8", env: { ...validationEnv, CANDIDATE_ARTIFACT_JSON: JSON.stringify(candidate), }, }); expect(valid.status, valid.stderr).toBe(0); const registryCandidate = { ...candidate, prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-456-1", prepublishPluginRegistryArtifactId: "790", prepublishPluginRegistryArtifactDigest: "f".repeat(64), prepublishPluginRegistryArtifactRunId: "456", prepublishPluginRegistryArtifactRunAttempt: "1", prepublishPluginRegistryManifestSha256: "1".repeat(64), }; const validRegistry = spawnSync("bash", ["-c", validation ?? ""], { encoding: "utf8", env: { ...validationEnv, CANDIDATE_ARTIFACT_JSON: JSON.stringify(registryCandidate), }, }); expect(validRegistry.status, validRegistry.stderr).toBe(0); const partialRegistry = spawnSync("bash", ["-c", validation ?? ""], { encoding: "utf8", env: { ...validationEnv, CANDIDATE_ARTIFACT_JSON: JSON.stringify({ ...candidate, prepublishPluginRegistryArtifactId: "790", }), }, }); expect(partialRegistry.status).not.toBe(0); const mismatchedRegistryName = spawnSync("bash", ["-c", validation ?? ""], { encoding: "utf8", env: { ...validationEnv, CANDIDATE_ARTIFACT_JSON: JSON.stringify({ ...registryCandidate, prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-999-1", }), }, }); expect(mismatchedRegistryName.status).not.toBe(0); const mismatched = spawnSync("bash", ["-c", validation ?? ""], { encoding: "utf8", env: { ...validationEnv, CANDIDATE_ARTIFACT_JSON: JSON.stringify(candidate), SELECTED_SHA: "f".repeat(40), }, }); expect(mismatched.status).not.toBe(0); }); it("keeps release history checks blobless", () => { const fullHistoryCheckouts: Array<[string, string, string]> = [ [RELEASE_PUBLISH_WORKFLOW, "resolve_release_target", "Checkout release tag"], [ RELEASE_CHECKS_WORKFLOW, "resolve_target", "Checkout selected ref for reachability fallback", ], [RELEASE_CHECKS_WORKFLOW, "prepare_release_package", "Checkout trusted workflow ref"], [PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", "Checkout package workflow ref"], [PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm", "Checkout"], [PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub", "Checkout"], [OPENCLAW_NPM_RELEASE_WORKFLOW, "preflight_openclaw_npm", "Checkout"], [OPENCLAW_NPM_RELEASE_WORKFLOW, "validate_publish_request", "Checkout"], [ ".github/workflows/openclaw-cross-os-release-checks-reusable.yml", "prepare", "Checkout public source ref", ], ]; for (const [workflowPath, jobName, stepName] of fullHistoryCheckouts) { expect( workflowStep(workflowJob(workflowPath, jobName), stepName).with, workflowPath, ).toMatchObject({ "fetch-depth": 0, filter: "blob:none", }); } const metadataOnlyCheckouts: Array<[string, string, string]> = [ [RELEASE_PUBLISH_WORKFLOW, "resolve_release_target", "Checkout release tag"], [ RELEASE_CHECKS_WORKFLOW, "resolve_target", "Checkout selected ref for reachability fallback", ], ]; for (const [workflowPath, jobName, stepName] of metadataOnlyCheckouts) { expect(workflowStep(workflowJob(workflowPath, jobName), stepName).with).toMatchObject({ "sparse-checkout": "package.json", "sparse-checkout-cone-mode": false, }); } const clawHubPackJob = workflowJob( PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "pack_plugins_clawhub_artifacts", ); const clawHubPackTargetGuard = workflowStep(clawHubPackJob, "Validate target revision"); expect(clawHubPackTargetGuard.env?.TARGET_SHA).toBe( "${{ needs.preview_plugins_clawhub.outputs.ref_revision }}", ); expect(clawHubPackTargetGuard.run).toContain('[[ ! "${TARGET_SHA}" =~ ^[a-f0-9]{40}$ ]]'); expect(workflowStep(clawHubPackJob, "Checkout").with).toMatchObject({ ref: "${{ needs.preview_plugins_clawhub.outputs.ref_revision }}", "fetch-depth": 1, "persist-credentials": false, }); expect(clawHubPackJob.steps?.map((step) => step.name)).not.toContain( "Checkout target revision", ); }); it("validates the macOS release handoff before the GitHub release page exists", () => { const macosRelease = readWorkflow(".github/workflows/macos-release.yml"); const validateJob = workflowJob( ".github/workflows/macos-release.yml", "validate_macos_release_request", ); const stepNames = validateJob.steps?.map((step) => step.name) ?? []; const buildControlUi = validateJob.steps?.find((step) => step.name === "Build Control UI"); expect(stepNames).not.toContain("Ensure matching GitHub release exists"); expect(macosRelease.jobs?.validate_macos_release_request).toBeDefined(); expect(buildControlUi?.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1"); }); it("classifies fast pretag Control UI output as a release artifact", () => { const script = readFileSync("scripts/release-fast-pretag-check.sh", "utf8"); expect(script).toContain("OPENCLAW_CONTROL_UI_RELEASE_BUILD=1 pnpm ui:build"); }); it("keeps every tracked repository skill visible to Git-aware syncs", () => { const skillFiles = execFileSync("git", ["ls-files", ".agents/skills/*/SKILL.md"], { encoding: "utf8", }) .trim() .split("\n") .filter(Boolean); expect(skillFiles.length).toBeGreaterThan(0); const ignored = spawnSync("git", ["check-ignore", "--no-index", "--stdin"], { encoding: "utf8", input: `${skillFiles.join("\n")}\n`, }); expect(ignored.status).toBe(1); expect(ignored.stdout).toBe(""); expect(ignored.stderr).toBe(""); }); it("does not track generated node_modules entries", () => { const tracked = execFileSync("git", ["ls-files", "-z", "--", ":(glob)**/node_modules/**"], { encoding: "utf8", }); expect(tracked).toBe(""); }); it("keeps tracked sync metadata and QA Mantis sources visible to remote full syncs", () => { for (const path of [ ".github/release/clawhub-cli/package-lock.json", ".gitignore", "apps/android/.gitignore", "docs/reference/templates/IDENTITY.md", "docs/reference/templates/USER.md", "extensions/qa-lab/src/mantis/cli.ts", ]) { const result = spawnSync("git", ["check-ignore", "--no-index", path], { encoding: "utf8", }); expect(result.status).toBe(1); expect(result.stdout).toBe(""); expect(result.stderr).toBe(""); } }); });