Files
openclaw/test/scripts/openclaw-cross-os-release-checks.test.ts
Onur 900e291f31 CI: expand native release validation coverage (#67144)
* Actions: grant reusable release checks actions read

* Actions: use read-all for reusable release checks

* CI: add native cross-OS release checks

* CI: wire Discord smoke secrets for cross-OS checks

* CI: fix native cross-OS installer compatibility

* CI: skip empty pnpm cache saves in matrix jobs

* CI: honor workflow runner override envs

* CI: finish native cross-OS update checks

* CI: fix native cross-OS workflow regressions

* Installer: capture Windows npm stderr safely

* CI: harden cross-OS release checks

* CI: resolve reusable workflow harness ref

* CI: stabilize cross-OS dev update lanes

* CI: tighten release-check workflow semantics

* CI: repoint repaired git CLI on POSIX

* CI: repair native dev-update shell handoff

* CI: preserve real updater semantics

* CI: harden supported release-check refs

* CI: harden release-check refs and fresh mode

* CI: skip dev-update for immutable tag refs

* CI: repair fresh installer release checks

* CI: fix native release check installer lanes

* CI: install release checks from candidate artifacts

* CI: use Windows cmd shims in release checks

* Installer: run Windows npm shim via PowerShell

* CI: pin dev update verification to candidate sha

* CI: pin reusable harness and published installers

* CI: isolate Windows dev-update PATH validation

* CI: align Windows dev-update bootstrap validation

* CI: avoid Windows installer gateway flake

* CI: run cross-OS release checks via TypeScript

* CI: bootstrap tsx for release-check workflow

* CI: fix native release-check follow-ups

* CI: tighten dev-update release checks

* CI: peel annotated workflow refs

* CI: harden native release checks

* CI: fix release-check verifier drift

* CI: fix release-check workflow drift

* CI: fix release-check ref resolution

* CI: harden Windows release-check gateway startup

* CI: fix release-check fallback validation

* CI: harden cross-os release checks

* CI: pin dev-update release checks to candidate SHA

* CI: resolve remote dev target refs

* CI: detect cloned dev-update checkouts

* CI: harden Windows release-check launcher

* Windows: harden task fallback and runner overrides

* Release checks: preserve Windows PATH and baseline version reads

* CI: add release validation live lanes

* CI: expand live and e2e release coverage

* CI: add branch dispatch for live and e2e checks
2026-04-16 19:58:19 +02:00

487 lines
17 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { createServer as createNetServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
buildWindowsDevUpdateToolchainCheckScript,
buildWindowsFreshShellVersionCheckScript,
buildWindowsPathBootstrapScript,
canConnectToLoopbackPort,
buildDiscordSmokeGuildsConfig,
buildRealUpdateEnv,
isImmutableReleaseRef,
looksLikeReleaseVersionRef,
normalizeRequestedRef,
normalizeWindowsCommandShimPath,
normalizeWindowsInstalledCliPath,
parseArgs,
packageHasScript,
readInstalledVersion,
readRunnerOverrideEnv,
resolveExplicitBaselineVersion,
resolveDevUpdateVerificationRef,
resolveInstalledPrefixDirFromCliPath,
resolvePublishedInstallerUrl,
resolveRequestedSuites,
resolveRunnerMatrix,
resolveStaticFileContentType,
shouldExerciseManagedGatewayLifecycleAfterInstall,
shouldSkipInstallerDaemonHealthCheck,
shouldStopManagedGatewayBeforeManualFallback,
shouldRunMainChannelDevUpdate,
shouldUseManagedGatewayForInstallerRuntime,
shouldUseManagedGatewayService,
verifyDevUpdateStatus,
} from "../../scripts/openclaw-cross-os-release-checks.ts";
describe("scripts/openclaw-cross-os-release-checks", () => {
it("treats explicit empty-string args as values instead of boolean flags", () => {
expect(parseArgs(["--ubuntu-runner", "", "--mode", "both"])).toEqual({
"ubuntu-runner": "",
mode: "both",
});
});
it("detects release refs and keeps branch refs out of release-only logic", () => {
expect(looksLikeReleaseVersionRef("2026.4.5")).toBe(true);
expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-beta.1")).toBe(true);
expect(looksLikeReleaseVersionRef("v2026.4.5-beta.1")).toBe(true);
expect(looksLikeReleaseVersionRef("v2026.4.7-1")).toBe(true);
expect(looksLikeReleaseVersionRef("main")).toBe(false);
expect(looksLikeReleaseVersionRef("codex/cross-os-release-checks")).toBe(false);
});
it("normalizes full Git refs before suite and update decisions", () => {
expect(normalizeRequestedRef(" refs/heads/main ")).toBe("main");
expect(normalizeRequestedRef("refs/tags/v2026.4.14")).toBe("v2026.4.14");
expect(isImmutableReleaseRef("refs/tags/test-tag")).toBe(true);
expect(resolveRequestedSuites("both", "refs/tags/v2026.4.14")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
expect(resolveRequestedSuites("both", "refs/tags/test-tag")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
expect(shouldRunMainChannelDevUpdate("refs/heads/main")).toBe(true);
expect(shouldRunMainChannelDevUpdate("refs/tags/main")).toBe(false);
});
it("skips the dev-update suite for immutable release refs", () => {
expect(resolveRequestedSuites("both", "v2026.4.5")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
});
it("skips dev-update for non-main branch validation refs", () => {
expect(resolveRequestedSuites("both", "codex/cross-os-release-checks")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
});
it("keeps dev-update enabled for main validation refs", () => {
expect(resolveRequestedSuites("both", "main")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
"dev-update",
]);
});
it("skips dev-update for pinned commit refs", () => {
expect(resolveRequestedSuites("both", "08753a1d793c040b101c8a26c43445dbbab14995")).toEqual([
"packaged-fresh",
"installer-fresh",
"packaged-upgrade",
]);
});
it("builds a suite-aware runner matrix with the beefy Windows default", () => {
const matrix = resolveRunnerMatrix({
mode: "both",
ref: "main",
ubuntuRunner: "",
windowsRunner: "",
macosRunner: "",
varUbuntuRunner: "",
varWindowsRunner: "",
varMacosRunner: "",
});
expect(matrix.include).toHaveLength(12);
expect(matrix.include).toContainEqual(
expect.objectContaining({
os_id: "windows",
runner: "blacksmith-32vcpu-windows-2025",
suite: "dev-update",
lane: "upgrade",
}),
);
expect(matrix.include).toContainEqual(
expect.objectContaining({
os_id: "ubuntu",
suite: "installer-fresh",
lane: "fresh",
}),
);
});
it("can rebuild the Windows PATH with or without current-process entries", () => {
expect(buildWindowsPathBootstrapScript()).toContain("@($userPath, $machinePath, $env:Path)");
const persistedOnlyScript = buildWindowsPathBootstrapScript({
includeCurrentProcessPath: false,
});
expect(persistedOnlyScript).toContain("@($userPath, $machinePath)");
expect(persistedOnlyScript).not.toContain("@($userPath, $machinePath, $env:Path)");
});
it("prefers the freshly installed Windows CLI under npm's prefix before PATH lookup", () => {
const script = buildWindowsFreshShellVersionCheckScript({
expectedNeedle: "2026.4.14",
});
expect(script).toContain(buildWindowsPathBootstrapScript());
expect(script).not.toContain(
buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
);
expect(script).toContain("Get-Command npm.cmd -ErrorAction SilentlyContinue");
expect(script).toContain('$env:Path = "$npmPrefix;$env:Path"');
expect(script).toContain("(Join-Path $npmPrefix 'openclaw.cmd')");
expect(script).toContain("$cmd = Get-Command openclaw -ErrorAction Stop");
});
it("keeps Windows dev-update toolchain checks compatible with setup-node PATH shims", () => {
const script = buildWindowsDevUpdateToolchainCheckScript();
expect(script).toContain(buildWindowsPathBootstrapScript());
expect(script).not.toContain(
buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
);
expect(script).toContain("$pnpmPath = Resolve-CommandPath 'pnpm'");
expect(script).toContain("$corepackPath = Resolve-CommandPath 'corepack'");
expect(script).toContain("$npmPath = Resolve-CommandPath 'npm'");
});
it("prefers workflow-injected runner override env names over legacy ones", () => {
expect(
readRunnerOverrideEnv({
VAR_UBUNTU_RUNNER: "workflow-linux",
VAR_WINDOWS_RUNNER: "workflow-windows",
VAR_MACOS_RUNNER: "workflow-macos",
OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
}),
).toEqual({
varUbuntuRunner: "workflow-linux",
varWindowsRunner: "workflow-windows",
varMacosRunner: "workflow-macos",
});
});
it("falls back to legacy runner override env names when workflow vars are blank", () => {
expect(
readRunnerOverrideEnv({
VAR_UBUNTU_RUNNER: "",
VAR_WINDOWS_RUNNER: " ",
VAR_MACOS_RUNNER: "",
OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
}),
).toEqual({
varUbuntuRunner: "legacy-linux",
varWindowsRunner: "legacy-windows",
varMacosRunner: "legacy-macos",
});
});
it("serves installer scripts as UTF-8 text and package payloads as binary", () => {
expect(resolveStaticFileContentType("scripts/install.sh")).toBe("text/plain; charset=utf-8");
expect(resolveStaticFileContentType("scripts/install.ps1")).toBe("text/plain; charset=utf-8");
expect(resolveStaticFileContentType("openclaw-2026.4.14.tgz")).toBe("application/octet-stream");
});
it("uses the published installer URLs for native installer lanes", () => {
expect(resolvePublishedInstallerUrl("darwin")).toBe("https://openclaw.ai/install.sh");
expect(resolvePublishedInstallerUrl("linux")).toBe("https://openclaw.ai/install.sh");
expect(resolvePublishedInstallerUrl("win32")).toBe("https://openclaw.ai/install.ps1");
});
it("uses managed gateway services only on native Windows runners", () => {
expect(shouldUseManagedGatewayService("win32")).toBe(true);
expect(shouldUseManagedGatewayService("darwin")).toBe(false);
expect(shouldUseManagedGatewayService("linux")).toBe(false);
});
it("keeps the Windows installer runtime on the manual gateway after managed lifecycle checks", () => {
expect(shouldExerciseManagedGatewayLifecycleAfterInstall("win32")).toBe(true);
expect(shouldUseManagedGatewayForInstallerRuntime("win32")).toBe(false);
expect(shouldExerciseManagedGatewayLifecycleAfterInstall("darwin")).toBe(false);
expect(shouldUseManagedGatewayForInstallerRuntime("darwin")).toBe(false);
});
it("stops the managed gateway before the manual fallback only on Windows", () => {
expect(shouldStopManagedGatewayBeforeManualFallback("win32")).toBe(true);
expect(shouldStopManagedGatewayBeforeManualFallback("darwin")).toBe(false);
expect(shouldStopManagedGatewayBeforeManualFallback("linux")).toBe(false);
});
it("skips daemon health during installed onboarding only on native Windows", () => {
expect(shouldSkipInstallerDaemonHealthCheck("win32")).toBe(true);
expect(shouldSkipInstallerDaemonHealthCheck("darwin")).toBe(false);
expect(shouldSkipInstallerDaemonHealthCheck("linux")).toBe(false);
});
it("normalizes Windows installed CLI paths to the cmd shim", () => {
expect(
normalizeWindowsInstalledCliPath(
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
),
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
expect(
normalizeWindowsInstalledCliPath(
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`,
),
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
});
it("normalizes generic Windows PowerShell shims to cmd shims", () => {
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\pnpm.ps1`)).toBe(
String.raw`C:\Program Files\nodejs\pnpm.cmd`,
);
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\corepack.ps1`)).toBe(
String.raw`C:\Program Files\nodejs\corepack.cmd`,
);
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\node.exe`)).toBe(
String.raw`C:\Program Files\nodejs\node.exe`,
);
});
it("derives the installed prefix from resolved CLI paths", () => {
expect(
resolveInstalledPrefixDirFromCliPath(
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
"win32",
),
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm`);
expect(
resolveInstalledPrefixDirFromCliPath("/Users/runner/.npm-global/bin/openclaw", "darwin"),
).toBe("/Users/runner/.npm-global");
});
it("detects whether a managed gateway listener is still reachable on loopback", async () => {
const server = createNetServer();
await new Promise((resolvePromise) => {
server.listen(0, "127.0.0.1", resolvePromise);
});
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
expect(await canConnectToLoopbackPort(port)).toBe(true);
await new Promise((resolvePromise) => {
server.close(resolvePromise);
});
expect(await canConnectToLoopbackPort(port)).toBe(false);
});
it("writes Discord smoke config using the strict guild channel schema", () => {
expect(buildDiscordSmokeGuildsConfig("guild-123", "channel-456")).toEqual({
"guild-123": {
channels: {
"channel-456": {
enabled: true,
requireMention: false,
},
},
},
});
});
it("keeps the dev-update lane for main only", () => {
expect(shouldRunMainChannelDevUpdate("main")).toBe(true);
expect(shouldRunMainChannelDevUpdate("08753a1d793c040b101c8a26c43445dbbab14995")).toBe(false);
expect(shouldRunMainChannelDevUpdate(" codex/cross-os-release-checks-full-native-e2e ")).toBe(
false,
);
expect(shouldRunMainChannelDevUpdate("v2026.4.14")).toBe(false);
});
it("verifies main dev updates against the prepared source sha when available", () => {
expect(resolveDevUpdateVerificationRef("main")).toBe("main");
expect(
resolveDevUpdateVerificationRef("main", "08753a1d793c040b101c8a26c43445dbbab14995"),
).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
expect(
resolveDevUpdateVerificationRef(
"refs/heads/main",
"08753a1d793c040b101c8a26c43445dbbab14995",
),
).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
expect(resolveDevUpdateVerificationRef("codex/cross-os-release-checks-full-native-e2e")).toBe(
"codex/cross-os-release-checks-full-native-e2e",
);
});
it("drops the bundled plugin postinstall disable flag for real updater calls", () => {
expect(
buildRealUpdateEnv({
FOO: "bar",
OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL: "1",
}),
).toEqual({
FOO: "bar",
});
});
it("only treats pinned baseline specs as exact installer version assertions", () => {
expect(resolveExplicitBaselineVersion("")).toBe("");
expect(resolveExplicitBaselineVersion("openclaw@latest")).toBe("");
expect(resolveExplicitBaselineVersion("openclaw@2026.4.10")).toBe("2026.4.10");
expect(resolveExplicitBaselineVersion("2026.4.10")).toBe("2026.4.10");
});
it("reads an installed baseline version without requiring build metadata", () => {
const prefixDir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-installed-version-"));
try {
const packageRoot =
process.platform === "win32"
? join(prefixDir, "node_modules", "openclaw")
: join(prefixDir, "lib", "node_modules", "openclaw");
mkdirSync(packageRoot, { recursive: true });
writeFileSync(
join(packageRoot, "package.json"),
JSON.stringify({
name: "openclaw",
version: "2026.4.10",
}),
"utf8",
);
expect(readInstalledVersion(prefixDir)).toBe("2026.4.10");
} finally {
rmSync(prefixDir, { recursive: true, force: true });
}
});
it("treats missing package scripts as optional in older refs", () => {
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-cross-os-scripts-"));
try {
writeFileSync(
join(packageRoot, "package.json"),
JSON.stringify({
name: "openclaw",
scripts: {
build: "pnpm build",
},
}),
"utf8",
);
expect(packageHasScript(packageRoot, "build")).toBe(true);
expect(packageHasScript(packageRoot, "ui:build")).toBe(false);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("accepts a git main dev-channel update status payload", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
branch: "main",
},
},
channel: {
value: "dev",
},
}),
),
).not.toThrow();
});
it("accepts a git dev-channel payload for a requested non-main branch", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
branch: "codex/cross-os-release-checks-full-native-e2e",
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
},
},
channel: {
value: "dev",
},
}),
{ ref: "codex/cross-os-release-checks-full-native-e2e" },
),
).not.toThrow();
});
it("accepts a git dev-channel payload pinned to a prepared source sha", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
branch: "main",
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
},
},
channel: {
value: "dev",
},
}),
{ ref: "08753a1d793c040b101c8a26c43445dbbab14995" },
),
).not.toThrow();
});
it("accepts uppercase requested commit shas when update status reports lowercase", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "git",
git: {
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
},
},
channel: {
value: "dev",
},
}),
{ ref: "08753A1D793C040B101C8A26C43445DBBAB14995" },
),
).not.toThrow();
});
it("rejects update status payloads that are not on dev/main git", () => {
expect(() =>
verifyDevUpdateStatus(
JSON.stringify({
update: {
installKind: "package",
git: {
branch: "release",
},
},
channel: {
value: "stable",
},
}),
),
).toThrow("git install");
});
});