Files
openclaw/test/scripts/openclaw-cross-os-release-workflow.test.ts
Peter Steinberger c70aee247e refactor(scripts): migrate JavaScript tools to TypeScript (#121005)
* refactor(scripts): migrate JavaScript tools to TypeScript

* fix(ci): keep changed-scope preflight zero-install

* fix(ci): preserve zero-install script owners

* fix(ci): complete script migration follow-through

* fix(release): keep stable closeout zero-install

* fix(scripts): preserve standalone execution boundaries

* fix(scripts): repair standalone loader boundaries

* fix(scripts): normalize gateway observation ids

* fix(scripts): keep Docker packager standalone

* test(scripts): preserve rebase cleanup helpers

* test(sessions): use tracked temp directory
2026-08-09 07:21:35 -07:00

581 lines
28 KiB
TypeScript

// Openclaw Cross Os Release Workflow tests cover openclaw cross os release workflow script behavior.
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
const WORKFLOW_PATH = ".github/workflows/openclaw-cross-os-release-checks-reusable.yml";
const RELEASE_CHECKS_PATH = ".github/workflows/openclaw-release-checks.yml";
const WRAPPER_PATH = "scripts/github/run-openclaw-cross-os-release-checks.sh";
const SCRIPT_PATH = "scripts/openclaw-cross-os-release-checks.ts";
const HARNESS = "bash workflow/scripts/github/run-openclaw-cross-os-release-checks.sh";
const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash";
type WorkflowStep = {
env?: Record<string, unknown>;
id?: string;
if?: string;
name?: string;
run?: string;
uses?: string;
with?: Record<string, unknown>;
"working-directory"?: string;
};
type WorkflowJob = {
if?: string;
outputs?: Record<string, unknown>;
steps?: WorkflowStep[];
with?: Record<string, unknown>;
};
type Workflow = {
jobs: Record<string, WorkflowJob>;
on?: {
workflow_call?: { inputs?: Record<string, Record<string, unknown>> };
workflow_dispatch?: { inputs?: Record<string, Record<string, unknown>> };
};
};
function readWorkflow(path: string): Workflow {
return parse(readFileSync(path, "utf8")) as Workflow;
}
function job(workflow: Workflow, name: string): WorkflowJob {
const found = workflow.jobs[name];
expect(found, name).toBeDefined();
return found!;
}
function step(workflowJob: WorkflowJob, name: string): WorkflowStep {
const found = workflowJob.steps?.find((candidate) => candidate.name === name);
expect(found, name).toBeDefined();
return found!;
}
describe("cross-OS release checks workflow", () => {
it("runs the TypeScript release harness through the Windows-safe wrapper", () => {
const workflow = readFileSync(WORKFLOW_PATH, "utf8");
expect(workflow).toContain(HARNESS);
expect(workflow).toContain("suite_filter:");
expect(workflow).toContain('--suite-filter "${INPUT_SUITE_FILTER}"');
expect(workflow).not.toContain("TSX_VERSION");
});
it("retries only an interrupted Windows dashboard probe", () => {
const workflow = readWorkflow(WORKFLOW_PATH);
const consumer = job(workflow, "cross_os_release_checks");
const run = step(consumer, "Run cross-OS release checks").run;
expect(run).toContain("run_cross_os_release_checks() {");
expect(run).toContain("if run_cross_os_release_checks; then");
expect(run).toContain('"${OPENCLAW_RELEASE_CHECK_OS}" != "windows"');
expect(run).toContain('"$status" -ne 127');
expect(run).toContain('dashboard_log="${OUTPUT_DIR}/logs/${MODE}-dashboard.log"');
expect(run).toContain('-f "${OUTPUT_DIR}/summary.json"');
expect(run).toContain("attempt=.*url=http://127.0.0.1:");
expect(run).toContain("retrying Windows release checks after the outer process exited 127");
expect(run).toContain("run_cross_os_release_checks\n");
});
it("bounds npm baseline packing during prepare", () => {
const workflow = readWorkflow(WORKFLOW_PATH);
const baselineMetadata = step(job(workflow, "prepare"), "Capture baseline metadata");
expect(readFileSync(WORKFLOW_PATH, "utf8")).toContain(
"timeout --preserve-status 300s npm pack --ignore-scripts",
);
expect(baselineMetadata["working-directory"]).toBe("workflow");
expect(baselineMetadata.run).toContain(
'import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mts";',
);
expect(baselineMetadata.run).toContain("const entry = resolveNpmJsonEntries(payload).at(-1);");
});
it("keeps release artifact tarball filenames local before upload paths use them", () => {
const workflow = readFileSync(WORKFLOW_PATH, "utf8");
expect(workflow.match(/function resolveTarballFileName/g)).toHaveLength(1);
expect(workflow.match(/path\.win32\.basename\(fileName\)/g)).toHaveLength(2);
expect(workflow).toContain("candidate_file_name");
expect(workflow).toContain("Baseline npm pack filename");
expect(workflow).toContain("fileName !== path.basename(fileName)");
expect(workflow).toContain("fileName !== path.win32.basename(fileName)");
expect(workflow).toContain("process.stdout.write(`file_name=${fileName}\\n`);");
});
it("binds the prepared release package to an immutable artifact and package tuple", () => {
const release = readWorkflow(RELEASE_CHECKS_PATH);
const producer = job(release, "prepare_release_package");
expect(producer.outputs).toMatchObject({
artifact_digest:
"${{ steps.release_package_upload.outputs.artifact-digest || fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactDigest }}",
artifact_id:
"${{ steps.release_package_upload.outputs.artifact-id || fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactId }}",
artifact_name:
"${{ steps.artifact.outputs.name || fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactName }}",
artifact_run_attempt:
"${{ steps.artifact.outputs.run_attempt || fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactRunAttempt }}",
artifact_run_id:
"${{ steps.artifact.outputs.run_id || fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactRunId }}",
package_file_name:
"${{ steps.artifact.outputs.file_name || fromJSON(inputs.candidate_artifact_json || '{}').packageFileName }}",
package_sha256:
"${{ steps.package.outputs.sha256 || fromJSON(inputs.candidate_artifact_json || '{}').packageSha256 }}",
package_version:
"${{ steps.package.outputs.package_version || fromJSON(inputs.candidate_artifact_json || '{}').packageVersion }}",
prepublish_plugin_registry_json: "${{ steps.registry_identity.outputs.json }}",
source_sha:
"${{ steps.package.outputs.source_sha || fromJSON(inputs.candidate_artifact_json || '{}').packageSourceSha }}",
});
expect(step(producer, "Checkout trusted workflow ref").with).toMatchObject({
ref: "${{ github.sha }}",
"persist-credentials": false,
});
const metadata = step(producer, "Set artifact metadata");
expect(metadata.run).toContain(
"name=release-package-under-test-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}",
);
expect(metadata.run).toContain("file_name=openclaw-current.tgz");
expect(metadata.run).toContain("run_attempt=${GITHUB_RUN_ATTEMPT}");
expect(metadata.run).toContain("run_id=${GITHUB_RUN_ID}");
const upload = step(producer, "Upload release package artifact");
expect(upload.id).toBe("release_package_upload");
expect(upload.with).toMatchObject({
name: "${{ steps.artifact.outputs.name }}",
"if-no-files-found": "error",
});
const resolve = step(producer, "Resolve release package artifact");
expect(resolve.run).toContain("--resolve-provider-required-companion-packages");
expect(resolve.run).toContain(".requiredPrepublishPluginPackages");
expect(resolve.run).toContain("'$provider + $docker | unique | sort'");
expect(resolve.run).toContain("--plugin-registry-output-dir");
expect(resolve.run).toContain("--required-plugin-packages-json");
expect(resolve.run).toContain(
'source_args=(--source npm --package-spec "$RELEASE_PACKAGE_SPEC" --package-ref "$PACKAGE_REF")',
);
const registryUpload = step(producer, "Upload shared prerelease plugin registry artifact");
expect(registryUpload.with?.name).toBe(
"docker-e2e-prepublish-plugin-registry-${{ github.run_id }}-${{ github.run_attempt }}",
);
const binding = step(producer, "Validate release package artifact binding");
expect(binding.env).toMatchObject({
ARTIFACT_DIGEST: "${{ steps.release_package_upload.outputs.artifact-digest }}",
ARTIFACT_ID: "${{ steps.release_package_upload.outputs.artifact-id }}",
ARTIFACT_RUN_ATTEMPT: "${{ steps.artifact.outputs.run_attempt }}",
ARTIFACT_RUN_ID: "${{ steps.artifact.outputs.run_id }}",
PACKAGE_SHA256: "${{ steps.package.outputs.sha256 }}",
PACKAGE_SOURCE_SHA: "${{ steps.package.outputs.source_sha }}",
PACKAGE_VERSION: "${{ steps.package.outputs.package_version }}",
});
expect(binding.run).toContain('verify-upload "Release package"');
expect(binding.run).toContain('"$PACKAGE_SHA256" =~ ^[a-f0-9]{64}$');
expect(binding.run).toContain('"$PACKAGE_SOURCE_SHA" =~ ^[a-f0-9]{40}$');
const crossOs = job(release, "cross_os_release_checks");
expect(crossOs.with).toMatchObject({
candidate_artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}",
candidate_artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}",
candidate_artifact_name: "${{ needs.prepare_release_package.outputs.artifact_name }}",
candidate_artifact_run_attempt:
"${{ needs.prepare_release_package.outputs.artifact_run_attempt }}",
candidate_artifact_run_id: "${{ needs.prepare_release_package.outputs.artifact_run_id }}",
candidate_file_name: "${{ needs.prepare_release_package.outputs.package_file_name }}",
candidate_sha256: "${{ needs.prepare_release_package.outputs.package_sha256 }}",
candidate_source_sha: "${{ needs.prepare_release_package.outputs.source_sha }}",
candidate_version: "${{ needs.prepare_release_package.outputs.package_version }}",
prepublish_plugin_registry_json:
"${{ needs.prepare_release_package.outputs.prepublish_plugin_registry_json }}",
});
expect(crossOs.with?.required_companion_packages_json).toBeUndefined();
expect(job(release, "docker_e2e_release_checks").with).toMatchObject({
package_artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}",
package_artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}",
package_artifact_name: "${{ needs.prepare_release_package.outputs.artifact_name }}",
package_artifact_run_attempt:
"${{ needs.prepare_release_package.outputs.artifact_run_attempt }}",
package_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.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 }}",
prepublish_plugin_registry_artifact_id:
"${{ fromJSON(needs.prepare_release_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactId || '' }}",
prepublish_plugin_registry_manifest_sha256:
"${{ fromJSON(needs.prepare_release_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
});
expect(job(release, "package_acceptance_release_checks").with).toMatchObject({
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_source_sha: "${{ needs.prepare_release_package.outputs.source_sha }}",
package_version: "${{ needs.prepare_release_package.outputs.package_version }}",
workflow_ref: "${{ github.sha }}",
});
});
it("builds companion registries only for scheduled cross-OS or Docker consumers", () => {
const workflow = readWorkflow(RELEASE_CHECKS_PATH);
const resolveTarget = job(workflow, "resolve_target");
expect(resolveTarget.outputs).toMatchObject({
cross_os_scheduled: "${{ steps.inputs.outputs.cross_os_scheduled }}",
docker_release_scheduled: "${{ steps.inputs.outputs.docker_release_scheduled }}",
});
const capture = step(resolveTarget, "Capture selected inputs");
expect(capture.run).toContain("cross_os_scheduled=false");
expect(capture.run).toContain("docker_release_scheduled=false");
expect(capture.run).toContain('"$RELEASE_RERUN_GROUP_INPUT" == "cross-os"');
expect(capture.run).toContain('[[ -z "${repo_live_suite_filter// }" ]]');
const producer = job(workflow, "prepare_release_package");
expect(producer.if).toContain("needs.resolve_target.outputs.cross_os_scheduled == 'true'");
expect(producer.if).toContain(
"needs.resolve_target.outputs.docker_release_scheduled == 'true'",
);
const resolvePackage = step(producer, "Resolve release package artifact");
expect(resolvePackage.run).toContain('if [[ "$CROSS_OS_SCHEDULED" == "true" ]]');
expect(resolvePackage.run).toContain(
'if [[ "$DOCKER_RELEASE_SCHEDULED" == "true" && -z "${RELEASE_PACKAGE_SPEC// }" ]]',
);
expect(resolvePackage.run).toContain("registry_args=()");
expect(resolvePackage.run).toContain("if [[ \"$required_packages\" != '[]' ]]");
expect(job(workflow, "cross_os_release_checks").if).toBe(
"needs.resolve_target.outputs.cross_os_scheduled == 'true'",
);
expect(job(workflow, "docker_e2e_release_checks").if).toBe(
"needs.resolve_target.outputs.docker_release_scheduled == 'true'",
);
});
it("downloads and re-exports exact candidate artifacts only by immutable id", () => {
const workflow = readWorkflow(WORKFLOW_PATH);
for (const inputName of [
"candidate_artifact_digest",
"candidate_artifact_id",
"candidate_artifact_name",
"candidate_artifact_run_attempt",
"candidate_artifact_run_id",
"candidate_file_name",
"candidate_sha256",
"candidate_source_sha",
"candidate_version",
]) {
expect(workflow.on?.workflow_dispatch?.inputs?.[inputName], inputName).toMatchObject({
default: "",
type: "string",
});
expect(workflow.on?.workflow_call?.inputs?.[inputName], inputName).toMatchObject({
default: "",
type: "string",
});
}
expect(workflow.on?.workflow_dispatch?.inputs?.prepublish_plugin_registry_json).toMatchObject({
default: "",
type: "string",
});
expect(workflow.on?.workflow_call?.inputs?.prepublish_plugin_registry_json).toMatchObject({
default: "",
type: "string",
});
for (const inputName of [
"prepublish_plugin_registry_artifact_digest",
"prepublish_plugin_registry_artifact_id",
"prepublish_plugin_registry_artifact_name",
"prepublish_plugin_registry_artifact_run_attempt",
"prepublish_plugin_registry_artifact_run_id",
"prepublish_plugin_registry_manifest_sha256",
]) {
expect(workflow.on?.workflow_dispatch?.inputs?.[inputName], inputName).toBeUndefined();
expect(workflow.on?.workflow_call?.inputs?.[inputName], inputName).toBeUndefined();
}
expect(workflow.on?.workflow_call?.inputs?.required_companion_packages_json).toBeUndefined();
const prepare = job(workflow, "prepare");
expect(prepare.outputs).toMatchObject({
baseline_artifact_digest: "${{ steps.upload_baseline.outputs.artifact-digest }}",
baseline_artifact_id: "${{ steps.upload_baseline.outputs.artifact-id }}",
baseline_artifact_run_attempt: "${{ github.run_attempt }}",
baseline_artifact_run_id: "${{ github.run_id }}",
baseline_sha256: "${{ steps.baseline_metadata.outputs.sha256 }}",
candidate_artifact_digest: "${{ steps.upload_candidate.outputs.artifact-digest }}",
candidate_artifact_id: "${{ steps.upload_candidate.outputs.artifact-id }}",
candidate_artifact_run_attempt: "${{ github.run_attempt }}",
candidate_artifact_run_id: "${{ github.run_id }}",
candidate_sha256: "${{ steps.candidate_metadata.outputs.sha256 }}",
candidate_version: "${{ steps.candidate_metadata.outputs.version }}",
prepublish_plugin_registry_json: "${{ steps.registry_identity.outputs.json }}",
required_companion_packages_json: "${{ steps.provider_requirements.outputs.json }}",
source_sha: "${{ steps.candidate_metadata.outputs.source_sha }}",
});
for (const [jobName, workflowJob] of Object.entries(workflow.jobs)) {
for (const checkout of workflowJob.steps?.filter((candidate) =>
candidate.uses?.startsWith("actions/checkout@"),
) ?? []) {
expect(checkout.with?.["persist-credentials"], `${jobName}:${checkout.name}`).toBe(false);
}
}
const inputBinding = step(prepare, "Validate provided candidate artifact binding");
expect(inputBinding.env).toMatchObject({
ARTIFACT_DIGEST: "${{ inputs.candidate_artifact_digest }}",
ARTIFACT_ID: "${{ inputs.candidate_artifact_id }}",
ARTIFACT_RUN_ATTEMPT: "${{ inputs.candidate_artifact_run_attempt }}",
ARTIFACT_RUN_ID: "${{ inputs.candidate_artifact_run_id }}",
CANDIDATE_SHA256: "${{ inputs.candidate_sha256 }}",
CANDIDATE_SOURCE_SHA: "${{ inputs.candidate_source_sha }}",
CANDIDATE_VERSION: "${{ inputs.candidate_version }}",
});
expect(inputBinding.run).toContain('! "$ARTIFACT_ID" =~ ^[1-9][0-9]*$');
expect(inputBinding.run).toContain('! "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$');
expect(inputBinding.run).toContain(
'[[ "$ARTIFACT_NAME" == *"-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" ]]',
);
expect(inputBinding.run).toContain('verify-upload "Candidate"');
expect(inputBinding.run).toContain('"$CANDIDATE_SOURCE_SHA" != "$INPUT_REF"');
const inputDownload = step(prepare, "Download provided candidate artifact");
expect(inputDownload.with).toMatchObject({
"artifact-ids": "${{ inputs.candidate_artifact_id }}",
"run-id": "${{ inputs.candidate_artifact_run_id }}",
});
expect(inputDownload.with?.name).toBeUndefined();
expect(
prepare.steps?.findIndex(
(candidate) => candidate.name === "Validate provided candidate artifact binding",
),
).toBeLessThan(
prepare.steps?.findIndex(
(candidate) => candidate.name === "Download provided candidate artifact",
) ?? -1,
);
const resolve = step(prepare, "Resolve provided candidate package");
expect(step(prepare, "Install workflow validation dependencies")).toMatchObject({
if: "inputs.candidate_artifact_name != ''",
run: "pnpm install --frozen-lockfile --prefer-offline --ignore-scripts",
});
expect(resolve.run).toContain("resolve-openclaw-package-candidate.mts");
expect(resolve.run).toContain("--source artifact");
expect(resolve.run).toContain('--package-sha256 "$INPUT_CANDIDATE_SHA256"');
expect(resolve.run).toContain('"$actual_sha256" == "$INPUT_CANDIDATE_SHA256"');
expect(resolve.run).toContain('"$actual_source_sha" == "$INPUT_CANDIDATE_SOURCE_SHA"');
expect(resolve.run).toContain('"$actual_version" == "$INPUT_CANDIDATE_VERSION"');
const upload = step(prepare, "Upload candidate artifact");
expect(upload.id).toBe("upload_candidate");
expect(upload.with?.name).toBe(
"openclaw-cross-os-release-checks-candidate-${{ github.run_id }}-${{ github.run_attempt }}",
);
const baselineUpload = step(prepare, "Upload baseline artifact");
expect(baselineUpload.id).toBe("upload_baseline");
expect(baselineUpload.with?.name).toBe(
"openclaw-cross-os-release-checks-baseline-${{ github.run_id }}-${{ github.run_attempt }}",
);
const registryUpload = step(prepare, "Upload source-built prerelease companion registry");
expect(registryUpload.if).toBe(
"inputs.candidate_artifact_name == '' && steps.provider_requirements.outputs.json != '[]'",
);
const sourceRegistry = step(prepare, "Pack source-built prerelease companion registry");
expect(sourceRegistry.run).toContain("prepublish-plugin-registry-artifact.mjs create");
expect(sourceRegistry.run).toContain("--repo-root source");
expect(sourceRegistry.run).toContain('--required-packages-json "$REQUIRED_PACKAGES_JSON"');
const preparedUploadVerification = step(prepare, "Verify prepared artifact uploads");
expect(preparedUploadVerification.run).toContain('verify-upload "Candidate"');
expect(preparedUploadVerification.run).toContain('verify-upload "Baseline"');
expect(preparedUploadVerification.run).toContain('verify-upload "Prerelease plugin registry"');
const consumer = job(workflow, "cross_os_release_checks");
expect(consumer.steps?.map((candidate) => candidate.name)).not.toContain(
"Validate prepared candidate artifact binding",
);
for (const name of ["Download candidate artifact", "Retry candidate artifact download"]) {
const download = step(consumer, name);
expect(download.with?.["artifact-ids"], name).toBe(
"${{ needs.prepare.outputs.candidate_artifact_id }}",
);
expect(download.with?.["github-token"], name).toBe("${{ github.token }}");
expect(download.with?.["run-id"], name).toBe(
"${{ needs.prepare.outputs.candidate_artifact_run_id }}",
);
expect(download.with?.name, name).toBeUndefined();
}
for (const name of ["Download baseline artifact", "Retry baseline artifact download"]) {
const download = step(consumer, name);
expect(download.with?.["artifact-ids"], name).toBe(
"${{ needs.prepare.outputs.baseline_artifact_id }}",
);
expect(download.with?.["github-token"], name).toBe("${{ github.token }}");
expect(download.with?.["run-id"], name).toBe(
"${{ needs.prepare.outputs.baseline_artifact_run_id }}",
);
expect(download.with?.name, name).toBeUndefined();
}
const verify = step(consumer, "Verify release-check inputs");
expect(verify.env?.EXPECTED_CANDIDATE_SHA256).toBe(
"${{ needs.prepare.outputs.candidate_sha256 }}",
);
expect(verify.run).toContain('"$actual_sha256" != "$EXPECTED_CANDIDATE_SHA256"');
expect(verify.env?.EXPECTED_BASELINE_SHA256).toBe(
"${{ needs.prepare.outputs.baseline_sha256 }}",
);
expect(verify.run).toContain('"$actual_baseline_sha256" != "$EXPECTED_BASELINE_SHA256"');
});
it("passes and consumes the immutable prerelease companion registry generically", () => {
const workflow = readWorkflow(WORKFLOW_PATH);
const prepare = job(workflow, "prepare");
const sourceCheckout = step(prepare, "Checkout public source ref");
expect(sourceCheckout.if).toBe("inputs.candidate_artifact_name == ''");
expect(sourceCheckout.with?.ref).toBe("${{ inputs.ref }}");
expect(prepare.steps?.map((candidate) => candidate.name)).not.toEqual(
expect.arrayContaining([
"Install companion plugin build dependencies",
"Pack exact Codex companion plugin",
"Resolve exact Codex companion plugin",
"Upload generated prerelease plugin registry artifact",
]),
);
const consumer = job(workflow, "cross_os_release_checks");
const download = step(consumer, "Download prerelease plugin registry artifact");
expect(download.with).toMatchObject({
"artifact-ids":
"${{ fromJSON(needs.prepare.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactId || '' }}",
"run-id":
"${{ fromJSON(needs.prepare.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}",
});
const run = step(consumer, "Run cross-OS release checks");
expect(run.env).toMatchObject({
PLUGIN_REGISTRY_JSON: "${{ needs.prepare.outputs.prepublish_plugin_registry_json }}",
REQUIRED_COMPANION_PACKAGES_JSON:
"${{ needs.prepare.outputs.required_companion_packages_json }}",
});
expect(run.run).toContain('--plugin-registry-dir "$PLUGIN_REGISTRY_DIR"');
expect(run.run).toContain(
'"$(jq -r \'.prepublishPluginRegistryManifestSha256\' <<< "$PLUGIN_REGISTRY_JSON")"',
);
expect(run.run).not.toContain("--required-companion-packages-json");
expect(run.run).not.toContain("OPENCLAW_PLUGIN_INSTALL_OVERRIDES");
expect(JSON.stringify(workflow)).not.toContain("@openclaw/codex");
});
it("owns provider companion requirements and fails closed for direct candidates", () => {
const workflow = readWorkflow(WORKFLOW_PATH);
const prepare = job(workflow, "prepare");
const requirements = step(prepare, "Resolve provider-owned companion requirements");
expect(requirements.run).toContain("--resolve-provider-required-companion-packages");
expect(requirements.run).toContain('--provider "$PROVIDER"');
const contract = step(prepare, "Normalize companion registry contract");
expect(contract.run).toContain("(keys | sort) == ([");
expect(contract.run).toContain('all(.[]; type == "string")');
expect(contract.run).toContain(
"Prerelease companion registry must be one closed immutable JSON tuple.",
);
expect(contract.run).toContain(
"The selected provider requires a complete immutable companion registry tuple.",
);
expect(contract.run).toContain(
"Source-built candidates produce their own companion registry and reject a second tuple.",
);
expect(step(prepare, "Verify provided prerelease plugin registry upload").run).toContain(
'verify-upload "Prerelease plugin registry"',
);
const workflowSource = readFileSync(WORKFLOW_PATH, "utf8");
const optionalJsonParses =
workflowSource.match(/fromJSON\([^)]+ \|\| '\{\}'\)\.[A-Za-z]+ \|\| ''/gu) ?? [];
expect(optionalJsonParses).toHaveLength(9);
expect(workflowSource).not.toContain("fromJSON(steps.provided_registry.outputs.json).");
expect(workflowSource).not.toContain(
"fromJSON(needs.prepare.outputs.prepublish_plugin_registry_json).",
);
});
it.each([
["openai", ["@openclaw/codex"]],
["anthropic", []],
["minimax", []],
])("resolves provider-owned companions for %s", (provider, expected) => {
const result = spawnSync(
BASH_BIN,
[WRAPPER_PATH, "--resolve-provider-required-companion-packages", "--provider", provider],
{
cwd: process.cwd(),
encoding: "utf8",
env: { ...process.env, OPENCLAW_RELEASE_CHECKS_SCRIPT: SCRIPT_PATH },
},
);
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
expect(JSON.parse(result.stdout)).toEqual(expected);
});
it("executes the release harness directly with Node", () => {
const wrapper = readFileSync(WRAPPER_PATH, "utf8");
const script = readFileSync(SCRIPT_PATH, "utf8");
const packageJson = JSON.parse(readFileSync("package.json", "utf8")) as {
scripts: Record<string, string>;
};
expect(wrapper).toContain('exec "${node_cmd}" "${script_path}" "$@"');
expect(wrapper).not.toContain("npm");
expect(wrapper).not.toContain("tsx");
expect(wrapper).not.toContain("--import");
expect(script).toMatch(/^#!\/usr\/bin\/env node$/mu);
expect(script).not.toContain("--import tsx");
expect(packageJson.scripts["test:windows:ci"]).toContain(
"test/scripts/openclaw-cross-os-release-workflow.test.ts",
);
const result = spawnSync(
BASH_BIN,
[
WRAPPER_PATH,
"--resolve-matrix",
"--ref",
"test/native-node",
"--mode",
"fresh",
"--suite-filter",
"windows/packaged-fresh",
"--windows-runner",
"windows-2025",
],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
OPENCLAW_RELEASE_CHECKS_SCRIPT: SCRIPT_PATH,
},
},
);
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({
include: [
{
os_id: "windows",
display_name: "Windows",
runner: "windows-2025",
artifact_name: "windows",
suite: "packaged-fresh",
suite_label: "packaged fresh",
lane: "fresh",
},
],
});
});
});