fix(ci): harden ARM smoke and browser checks

This commit is contained in:
Vincent Koc
2026-06-03 07:30:04 -07:00
parent acacd32415
commit d3ab7e92ef
15 changed files with 473 additions and 54 deletions

View File

@@ -7,7 +7,23 @@ import { chromium } from "playwright";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const playwrightInstallArgs = ["--dir", "ui", "exec", "playwright", "install", "chromium"];
const playwrightInstallArgs = [
"--dir",
"ui",
"exec",
"playwright",
"install",
"chromium",
];
const playwrightInstallWithDepsArgs = [
"--dir",
"ui",
"exec",
"playwright",
"install",
"--with-deps",
"chromium",
];
const executableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH";
export const systemChromiumExecutableCandidates = [
"/snap/bin/chromium",
@@ -17,8 +33,22 @@ export const systemChromiumExecutableCandidates = [
"/usr/bin/google-chrome-stable",
];
export function resolveSystemChromiumExecutablePath(existsSync = existsSyncImpl) {
return systemChromiumExecutableCandidates.find((candidate) => existsSync(candidate)) ?? "";
export function canRunChromiumExecutable(executablePath, spawnSync = spawnSyncImpl) {
const result = spawnSync(executablePath, ["--version"], {
stdio: "ignore",
});
return result.status === 0;
}
export function resolveSystemChromiumExecutablePath(
existsSync = existsSyncImpl,
spawnSync = spawnSyncImpl,
) {
return (
systemChromiumExecutableCandidates.find(
(candidate) => existsSync(candidate) && canRunChromiumExecutable(candidate, spawnSync),
) ?? ""
);
}
export function resolvePlaywrightInstallRunner(options = {}) {
@@ -27,10 +57,23 @@ export function resolvePlaywrightInstallRunner(options = {}) {
comSpec: options.comSpec ?? env.ComSpec ?? env.COMSPEC,
npmExecPath: env === process.env ? env.npm_execpath : (env.npm_execpath ?? ""),
platform: options.platform,
pnpmArgs: playwrightInstallArgs,
pnpmArgs: options.withDeps ? playwrightInstallWithDepsArgs : playwrightInstallArgs,
});
}
export function shouldInstallPlaywrightSystemDependencies(options = {}) {
const env = options.env ?? process.env;
const platform = options.platform ?? process.platform;
const getuid = options.getuid ?? process.getuid;
if (platform !== "linux") {
return false;
}
if (typeof getuid === "function" && getuid() === 0) {
return true;
}
return env.CI === "true" || env.GITHUB_ACTIONS === "true";
}
export function isDirectScriptExecution(
argvEntry = process.argv[1],
modulePath = fileURLToPath(import.meta.url),
@@ -56,22 +99,25 @@ export function ensurePlaywrightChromium(options = {}) {
const spawnSync = options.spawnSync ?? spawnSyncImpl;
if (executableOverride) {
if (existsSync(executableOverride)) {
if (existsSync(executableOverride) && canRunChromiumExecutable(executableOverride, spawnSync)) {
return 0;
}
log(
`[ui-e2e] ${executableOverrideEnvKey} points to ${executableOverride}, but that browser does not exist.`,
`[ui-e2e] ${executableOverrideEnvKey} points to ${executableOverride}, but that browser is not runnable.`,
);
return 1;
}
if (existsSync(executablePath)) {
if (existsSync(executablePath) && canRunChromiumExecutable(executablePath, spawnSync)) {
return 0;
}
const systemExecutablePath =
options.systemExecutablePath ?? resolveSystemChromiumExecutablePath(existsSync);
if (systemExecutablePath) {
options.systemExecutablePath ?? resolveSystemChromiumExecutablePath(existsSync, spawnSync);
if (
systemExecutablePath &&
canRunChromiumExecutable(systemExecutablePath, spawnSync)
) {
log(`[ui-e2e] Using system Chromium at ${systemExecutablePath}.`);
return 0;
}
@@ -83,7 +129,7 @@ export function ensurePlaywrightChromium(options = {}) {
return 0;
}
log(`[ui-e2e] Playwright Chromium is missing at ${executablePath}; installing chromium.`);
log(`[ui-e2e] Playwright Chromium is not runnable at ${executablePath}; installing chromium.`);
const runner = resolvePlaywrightInstallRunner({
comSpec: options.comSpec,
env,
@@ -101,9 +147,38 @@ export function ensurePlaywrightChromium(options = {}) {
return status;
}
if (!existsSync(executablePath)) {
if (!existsSync(executablePath) || !canRunChromiumExecutable(executablePath, spawnSync)) {
if (shouldInstallPlaywrightSystemDependencies({
env,
getuid: options.getuid,
platform: options.platform,
})) {
log(
`[ui-e2e] Chromium is installed but still cannot start; installing Linux system dependencies.`,
);
const depsRunner = resolvePlaywrightInstallRunner({
comSpec: options.comSpec,
env,
platform: options.platform,
withDeps: true,
});
const depsResult = spawnSync(depsRunner.command, depsRunner.args, {
cwd: options.cwd ?? repoRoot,
env,
shell: depsRunner.shell,
stdio: options.stdio ?? "inherit",
windowsVerbatimArguments: depsRunner.windowsVerbatimArguments,
});
const depsStatus = depsResult.status ?? 1;
if (depsStatus !== 0) {
return depsStatus;
}
if (existsSync(executablePath) && canRunChromiumExecutable(executablePath, spawnSync)) {
return 0;
}
}
log(
`[ui-e2e] Playwright install completed but Chromium is still missing at ${executablePath}.`,
`[ui-e2e] Playwright install completed but Chromium is still not runnable at ${executablePath}.`,
);
return 1;
}

View File

@@ -5,9 +5,37 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/scripts/lib/docker-build.sh"
source "$ROOT_DIR/scripts/lib/docker-e2e-container.sh"
IMAGE_NAME="${OPENCLAW_CLEANUP_SMOKE_IMAGE:-openclaw-cleanup-smoke:local}"
PLATFORM="${OPENCLAW_CLEANUP_SMOKE_PLATFORM:-linux/amd64}"
DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_CLEANUP_SMOKE_DOCKER_TIMEOUT:-600s}}"
resolve_default_cleanup_platform() {
local host_arch
if [[ -n "${OPENCLAW_CLEANUP_SMOKE_PLATFORM:-}" ]]; then
printf "%s" "$OPENCLAW_CLEANUP_SMOKE_PLATFORM"
return
fi
host_arch="$(uname -m)"
if [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; then
case "$host_arch" in
arm64 | aarch64)
printf "linux/arm64"
return
;;
esac
printf "linux/amd64"
return
fi
case "$host_arch" in
arm64 | aarch64)
printf "linux/arm64"
;;
*)
printf "linux/amd64"
;;
esac
}
PLATFORM="$(resolve_default_cleanup_platform)"
echo "==> Build image: $IMAGE_NAME"
docker_build_run cleanup-build \
-t "$IMAGE_NAME" \

View File

@@ -14,23 +14,30 @@ run_install_smoke_container() {
}
resolve_default_smoke_platform() {
local host_os
local host_arch
if [[ -n "${OPENCLAW_INSTALL_SMOKE_PLATFORM:-}" ]]; then
printf "%s" "$OPENCLAW_INSTALL_SMOKE_PLATFORM"
return
fi
host_arch="$(uname -m)"
if [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; then
case "$host_arch" in
arm64 | aarch64)
printf "linux/arm64"
return
;;
esac
printf "linux/amd64"
return
fi
host_os="$(uname -s)"
host_arch="$(uname -m)"
if [[ "$host_os" == "Darwin" && "$host_arch" == "arm64" ]]; then
printf "linux/arm64"
return
fi
printf "linux/amd64"
case "$host_arch" in
arm64 | aarch64)
printf "linux/arm64"
;;
*)
printf "linux/amd64"
;;
esac
}
print_pack_audit() {

View File

@@ -30,6 +30,8 @@ const TEST_ENV_KEYS = [
"OPENCLAW_SKIP_PROVIDERS",
"OPENCLAW_TEST_MINIMAL_GATEWAY",
];
const GATEWAY_CONNECT_TIMEOUT_MS = 120_000;
const EXEC_APPROVAL_E2E_TIMEOUT_MS = 180_000;
type Cleanup = () => Promise<void> | void;
@@ -128,7 +130,8 @@ describe("gateway-hosted exec approvals", () => {
clientDisplayName: "approval operator",
mode: GATEWAY_CLIENT_MODES.TEST,
scopes: [ADMIN_SCOPE],
timeoutMs: 60_000,
requestTimeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
timeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
});
cleanup.push(() => disconnectGatewayClient(operator));
@@ -171,5 +174,5 @@ describe("gateway-hosted exec approvals", () => {
expect(outcome.status).toBe("completed");
expect(outcome.exitCode).toBe(0);
expect(outcome.aggregated).toBe("smoke");
}, 120_000);
}, EXEC_APPROVAL_E2E_TIMEOUT_MS);
});

View File

@@ -6,7 +6,7 @@ const mocks = vi.hoisted(() => ({
hookRunner: undefined as
| {
hasHooks: ReturnType<typeof vi.fn>;
runResolveExecEnv: ReturnType<typeof vi.fn>;
runResolveExecEnv?: ReturnType<typeof vi.fn>;
runBeforeToolCall?: ReturnType<typeof vi.fn>;
}
| undefined,
@@ -245,7 +245,7 @@ describe("exec resolve_exec_env hook wiring", () => {
expect(mocks.beforeToolCallParams[0]?.env).toEqual({
EXISTING: "request",
});
expect(mocks.hookRunner.runResolveExecEnv).toHaveBeenCalledTimes(1);
expect(mocks.hookRunner.runResolveExecEnv!).toHaveBeenCalledTimes(1);
expect(mocks.gatewayParams[0]?.requestedEnv).toEqual({
EXISTING: "request",
PLUGIN_SAFE: "yes",
@@ -293,7 +293,7 @@ describe("exec resolve_exec_env hook wiring", () => {
expect(mocks.beforeToolCallParams[0]?.env).toEqual({
REQUEST_SAFE: "request",
});
expect(mocks.hookRunner.runResolveExecEnv).toHaveBeenCalledTimes(1);
expect(mocks.hookRunner.runResolveExecEnv!).toHaveBeenCalledTimes(1);
expect(mocks.gatewayParams[0]?.requestedEnv).toEqual({
LAZY_PLUGIN_SAFE: "yes",
REQUEST_SAFE: "request",
@@ -335,13 +335,13 @@ describe("exec resolve_exec_env hook wiring", () => {
testExtensionContext,
);
expect(mocks.hookRunner.runResolveExecEnv).toHaveBeenCalledTimes(2);
expect(mocks.hookRunner.runResolveExecEnv).toHaveBeenNthCalledWith(
expect(mocks.hookRunner.runResolveExecEnv!).toHaveBeenCalledTimes(2);
expect(mocks.hookRunner.runResolveExecEnv!).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ host: "gateway" }),
expect.anything(),
);
expect(mocks.hookRunner.runResolveExecEnv).toHaveBeenNthCalledWith(
expect(mocks.hookRunner.runResolveExecEnv!).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ host: "node" }),
expect.anything(),
@@ -386,7 +386,29 @@ describe("exec resolve_exec_env hook wiring", () => {
testExtensionContext,
);
expect(mocks.hookRunner.runResolveExecEnv).not.toHaveBeenCalled();
expect(mocks.hookRunner.runResolveExecEnv!).not.toHaveBeenCalled();
expect(mocks.gatewayParams[0]?.requestedEnv).toEqual({
REQUEST_SAFE: "request",
});
});
it("skips stale hook runners that report resolve_exec_env without the runner method", async () => {
mocks.hookRunner = {
hasHooks: vi.fn((hookName: string) => hookName === "resolve_exec_env"),
};
const tool = createExecTool({
host: "gateway",
security: "full",
ask: "off",
sessionKey: "agent:main:telegram:chat-1",
});
await tool.execute("call-stale-hook-runner", {
command: "echo ok",
env: { REQUEST_SAFE: "request" },
yieldMs: 120_000,
});
expect(mocks.gatewayParams[0]?.requestedEnv).toEqual({
REQUEST_SAFE: "request",
});
@@ -431,7 +453,7 @@ describe("exec resolve_exec_env hook wiring", () => {
expect(mocks.beforeToolCallParams[0]?.env).toEqual({
REQUEST_SAFE: "request",
});
expect(mocks.hookRunner.runResolveExecEnv).toHaveBeenCalledTimes(1);
expect(mocks.hookRunner.runResolveExecEnv!).toHaveBeenCalledTimes(1);
expect(mocks.gatewayParams[0]?.requestedEnv).toEqual({
PLUGIN_SAFE: "yes",
REQUEST_SAFE: "request",

View File

@@ -1411,7 +1411,10 @@ export function createExecTool(
return markResolveExecEnvPrepared(params);
}
const hookRunner = getGlobalHookRunner();
if (!hookRunner?.hasHooks("resolve_exec_env")) {
if (
!hookRunner?.hasHooks("resolve_exec_env") ||
typeof hookRunner.runResolveExecEnv !== "function"
) {
return markResolveExecEnvPrepared(params);
}
let host: ExecHost;

View File

@@ -46,6 +46,7 @@ export async function connectGatewayClient(params: {
deviceIdentity?: DeviceIdentity;
onEvent?: (evt: { event?: string; payload?: unknown }) => void;
connectChallengeTimeoutMs?: number;
requestTimeoutMs?: number;
timeoutMs?: number;
timeoutMessage?: string;
}) {
@@ -90,6 +91,7 @@ export async function connectGatewayClient(params: {
...(params.connectChallengeTimeoutMs !== undefined
? { connectChallengeTimeoutMs: params.connectChallengeTimeoutMs }
: {}),
...(params.requestTimeoutMs !== undefined ? { requestTimeoutMs: params.requestTimeoutMs } : {}),
clientName: params.clientName ?? GATEWAY_CLIENT_NAMES.TEST,
clientDisplayName: params.clientDisplayName ?? "vitest",
clientVersion: params.clientVersion ?? "dev",

View File

@@ -109,6 +109,34 @@ function shellQuote(value: string): string {
return `'${value.replace(/'/gu, `'\\''`)}'`;
}
function runCleanupDefaultPlatform(env: Record<string, string>, hostArch: string): string {
const script = readFileSync(CLEANUP_DOCKER_SMOKE_PATH, "utf8");
const match = script.match(
/(resolve_default_cleanup_platform\(\) \{[\s\S]*?\n\})\n\nPLATFORM=/u,
);
if (!match) {
throw new Error("resolve_default_cleanup_platform was not found");
}
return execFileSync(
"bash",
[
"--noprofile",
"--norc",
"-c",
`${match[1]}\nuname() { if [[ "\${1:-}" == "-m" ]]; then printf "%s" "$FAKE_UNAME_ARCH"; else command uname "$@"; fi; }\nresolve_default_cleanup_platform`,
],
{
encoding: "utf8",
env: {
HOME: "/tmp",
PATH: process.env.PATH ?? "",
FAKE_UNAME_ARCH: hostArch,
...env,
},
},
);
}
describe("docker build helper", () => {
it("forces BuildKit for centralized Docker builds", () => {
const helper = readFileSync(HELPER_PATH, "utf8");
@@ -156,6 +184,15 @@ describe("docker build helper", () => {
expect(installE2eSmoke).not.toContain("docker run --rm \\");
});
it("runs cleanup smoke on the native ARM platform instead of pulling an amd64 tag", () => {
expect(runCleanupDefaultPlatform({ CI: "true" }, "aarch64")).toBe("linux/arm64");
expect(runCleanupDefaultPlatform({ GITHUB_ACTIONS: "true" }, "x86_64")).toBe("linux/amd64");
expect(runCleanupDefaultPlatform({}, "arm64")).toBe("linux/arm64");
expect(
runCleanupDefaultPlatform({ OPENCLAW_CLEANUP_SMOKE_PLATFORM: "linux/s390x" }, "x86_64"),
).toBe("linux/s390x");
});
it("lets Testbox fall back to building when a reused Docker image is missing", () => {
const helper = readFileSync(HELPER_PATH, "utf8");
const e2eImageHelper = readFileSync(DOCKER_E2E_IMAGE_HELPER_PATH, "utf8");

View File

@@ -2,11 +2,12 @@ import { describe, expect, it, vi } from "vitest";
import {
ensurePlaywrightChromium,
resolvePlaywrightInstallRunner,
shouldInstallPlaywrightSystemDependencies,
} from "../../scripts/ensure-playwright-chromium.mjs";
describe("ensurePlaywrightChromium", () => {
it("does nothing when the browser binary exists", () => {
const spawnSync = vi.fn();
it("does nothing when the browser binary exists and runs", () => {
const spawnSync = vi.fn(() => ({ status: 0 }));
expect(
ensurePlaywrightChromium({
@@ -15,11 +16,13 @@ describe("ensurePlaywrightChromium", () => {
spawnSync,
}),
).toBe(0);
expect(spawnSync).not.toHaveBeenCalled();
expect(spawnSync).toHaveBeenCalledWith("/cache/chromium/chrome", ["--version"], {
stdio: "ignore",
});
});
it("uses an explicit Chromium executable override", () => {
const spawnSync = vi.fn();
const spawnSync = vi.fn(() => ({ status: 0 }));
expect(
ensurePlaywrightChromium({
@@ -29,7 +32,9 @@ describe("ensurePlaywrightChromium", () => {
spawnSync,
}),
).toBe(0);
expect(spawnSync).not.toHaveBeenCalled();
expect(spawnSync).toHaveBeenCalledWith("/snap/bin/chromium", ["--version"], {
stdio: "ignore",
});
});
it("fails when the explicit Chromium executable override is missing", () => {
@@ -53,7 +58,7 @@ describe("ensurePlaywrightChromium", () => {
it("uses a system Chromium binary when Playwright Chromium is missing", () => {
const logs: string[] = [];
const spawnSync = vi.fn();
const spawnSync = vi.fn(() => ({ status: 0 }));
expect(
ensurePlaywrightChromium({
@@ -63,10 +68,36 @@ describe("ensurePlaywrightChromium", () => {
spawnSync,
}),
).toBe(0);
expect(spawnSync).not.toHaveBeenCalled();
expect(spawnSync).toHaveBeenCalledWith("/usr/bin/chromium-browser", ["--version"], {
stdio: "ignore",
});
expect(logs.join("\n")).toContain("Using system Chromium at /usr/bin/chromium-browser");
});
it("skips a broken system Chromium binary and uses the first runnable candidate", () => {
const logs: string[] = [];
const spawnSync = vi.fn((path: string) => ({
status: path === "/usr/bin/google-chrome" ? 0 : 127,
}));
expect(
ensurePlaywrightChromium({
executablePath: "/cache/chromium/chrome",
existsSync: (path: string) =>
path === "/snap/bin/chromium" || path === "/usr/bin/google-chrome",
log: (line: string) => logs.push(line),
spawnSync,
}),
).toBe(0);
expect(spawnSync).toHaveBeenCalledWith("/snap/bin/chromium", ["--version"], {
stdio: "ignore",
});
expect(spawnSync).toHaveBeenCalledWith("/usr/bin/google-chrome", ["--version"], {
stdio: "ignore",
});
expect(logs.join("\n")).toContain("Using system Chromium at /usr/bin/google-chrome");
});
it("preserves the intentional missing-browser skip mode", () => {
const logs: string[] = [];
const spawnSync = vi.fn();
@@ -113,6 +144,116 @@ describe("ensurePlaywrightChromium", () => {
);
});
it("installs Linux system dependencies when Chromium still cannot start in a root lane", () => {
const spawnSync = vi
.fn()
.mockReturnValueOnce({ status: 127 })
.mockReturnValueOnce({ status: 0 })
.mockReturnValueOnce({ status: 127 })
.mockReturnValueOnce({ status: 0 })
.mockReturnValueOnce({ status: 0 });
expect(
ensurePlaywrightChromium({
cwd: "/repo",
env: { PATH: "/bin" },
executablePath: "/cache/chromium/chrome",
existsSync: () => true,
getuid: () => 0,
platform: "linux",
spawnSync,
stdio: "pipe",
systemExecutablePath: "",
}),
).toBe(0);
expect(spawnSync).toHaveBeenNthCalledWith(
2,
"pnpm",
["--dir", "ui", "exec", "playwright", "install", "chromium"],
{
cwd: "/repo",
env: { PATH: "/bin" },
shell: false,
stdio: "pipe",
windowsVerbatimArguments: undefined,
},
);
expect(spawnSync).toHaveBeenNthCalledWith(
4,
"pnpm",
["--dir", "ui", "exec", "playwright", "install", "--with-deps", "chromium"],
{
cwd: "/repo",
env: { PATH: "/bin" },
shell: false,
stdio: "pipe",
windowsVerbatimArguments: undefined,
},
);
});
it("does not install Linux system dependencies for an unprivileged local lane", () => {
const spawnSync = vi
.fn()
.mockReturnValueOnce({ status: 127 })
.mockReturnValueOnce({ status: 0 })
.mockReturnValueOnce({ status: 127 });
expect(
ensurePlaywrightChromium({
cwd: "/repo",
env: { PATH: "/bin" },
executablePath: "/cache/chromium/chrome",
existsSync: () => true,
getuid: () => 501,
platform: "linux",
spawnSync,
stdio: "pipe",
systemExecutablePath: "",
}),
).toBe(1);
expect(spawnSync).toHaveBeenCalledTimes(3);
});
it("reinstalls Chromium when the cached executable exists but cannot start", () => {
const spawnSync = vi
.fn()
.mockReturnValueOnce({ status: 127 })
.mockReturnValueOnce({ status: 0 })
.mockReturnValueOnce({ status: 0 });
expect(
ensurePlaywrightChromium({
cwd: "/repo",
env: { PATH: "/bin" },
executablePath: "/cache/chromium/chrome",
existsSync: () => true,
platform: "linux",
spawnSync,
stdio: "pipe",
systemExecutablePath: "",
}),
).toBe(0);
expect(spawnSync).toHaveBeenNthCalledWith(1, "/cache/chromium/chrome", ["--version"], {
stdio: "ignore",
});
expect(spawnSync).toHaveBeenNthCalledWith(
2,
"pnpm",
["--dir", "ui", "exec", "playwright", "install", "chromium"],
{
cwd: "/repo",
env: { PATH: "/bin" },
shell: false,
stdio: "pipe",
windowsVerbatimArguments: undefined,
},
);
expect(spawnSync).toHaveBeenNthCalledWith(3, "/cache/chromium/chrome", ["--version"], {
stdio: "ignore",
});
});
it("returns the installer status when Playwright install fails", () => {
expect(
ensurePlaywrightChromium({
@@ -133,10 +274,46 @@ describe("ensurePlaywrightChromium", () => {
platform: "win32",
}),
).toEqual({
args: ["/d", "/s", "/c", "pnpm.cmd --dir ui exec playwright install chromium"],
args: [
"/d",
"/s",
"/c",
"pnpm.cmd --dir ui exec playwright install chromium",
],
command: "C:\\Windows\\System32\\cmd.exe",
shell: false,
windowsVerbatimArguments: true,
});
});
it("wraps the dependency install command shim on Windows", () => {
expect(
resolvePlaywrightInstallRunner({
comSpec: "C:\\Windows\\System32\\cmd.exe",
env: {},
platform: "win32",
withDeps: true,
}),
).toEqual({
args: [
"/d",
"/s",
"/c",
"pnpm.cmd --dir ui exec playwright install --with-deps chromium",
],
command: "C:\\Windows\\System32\\cmd.exe",
shell: false,
windowsVerbatimArguments: true,
});
});
it("allows dependency installation for Linux CI lanes", () => {
expect(
shouldInstallPlaywrightSystemDependencies({
env: { CI: "true" },
getuid: () => 501,
platform: "linux",
}),
).toBe(true);
});
});

View File

@@ -61,14 +61,45 @@ function runNonrootNodePreflight(version: string, options: { sqlite?: boolean }
}
}
describe("test-install-sh-docker", () => {
it("defaults local Apple Silicon smoke runs to native arm64 while keeping CI on amd64", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
function runDefaultSmokePlatform(env: Record<string, string>, hostArch: string): string {
const script = readFileSync(SCRIPT_PATH, "utf8");
const match = script.match(
/(resolve_default_smoke_platform\(\) \{[\s\S]*?\n\})\n\nprint_pack_audit/u,
);
if (!match) {
throw new Error("resolve_default_smoke_platform was not found");
}
const result = spawnSync(
"bash",
[
"--noprofile",
"--norc",
"-c",
`${match[1]}\nuname() { if [[ "\${1:-}" == "-m" ]]; then printf "%s" "$FAKE_UNAME_ARCH"; else command uname "$@"; fi; }\nresolve_default_smoke_platform`,
],
{
encoding: "utf8",
env: {
HOME: "/tmp",
PATH: process.env.PATH ?? "",
FAKE_UNAME_ARCH: hostArch,
...env,
},
},
);
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
return result.stdout;
}
expect(script).toContain("resolve_default_smoke_platform");
expect(script).toContain('printf "linux/amd64"');
expect(script).toContain('[[ "$host_os" == "Darwin" && "$host_arch" == "arm64" ]]');
expect(script).toContain('printf "linux/arm64"');
describe("test-install-sh-docker", () => {
it("defaults ARM hosts to native arm64 while keeping x64 CI on amd64", () => {
expect(runDefaultSmokePlatform({ CI: "true" }, "aarch64")).toBe("linux/arm64");
expect(runDefaultSmokePlatform({ GITHUB_ACTIONS: "true" }, "x86_64")).toBe("linux/amd64");
expect(runDefaultSmokePlatform({}, "arm64")).toBe("linux/arm64");
expect(
runDefaultSmokePlatform({ OPENCLAW_INSTALL_SMOKE_PLATFORM: "linux/s390x" }, "x86_64"),
).toBe("linux/s390x");
});
it("supports npm update package specs without a separate expected-version env", () => {

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import {
resolvePlaywrightChromiumExecutablePath,
systemChromiumExecutableCandidates,
} from "./control-ui-e2e.ts";
describe("resolvePlaywrightChromiumExecutablePath", () => {
it("uses a runnable system Chromium when the cached Playwright executable cannot start", () => {
const systemExecutable = systemChromiumExecutableCandidates[1];
expect(
resolvePlaywrightChromiumExecutablePath(
"/cache/chromium/chrome",
{},
(candidate) => candidate === systemExecutable,
),
).toBe(systemExecutable);
});
it("keeps explicit Chromium overrides authoritative", () => {
expect(
resolvePlaywrightChromiumExecutablePath(
"/cache/chromium/chrome",
{ PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH: " /custom/chromium " },
() => false,
),
).toBe("/custom/chromium");
});
});

View File

@@ -1,3 +1,4 @@
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { createServer as createNetServer } from "node:net";
@@ -83,7 +84,7 @@ export type MockGatewayControls = {
};
const chromiumExecutableOverrideEnvKey = "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH";
const systemChromiumExecutableCandidates = [
export const systemChromiumExecutableCandidates = [
"/snap/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/chromium",
@@ -99,22 +100,26 @@ function resolveRepoRoot(): string {
export function resolvePlaywrightChromiumExecutablePath(
defaultExecutablePath: string,
env: NodeJS.ProcessEnv = process.env,
canRun: (chromiumExecutablePath: string) => boolean = canRunPlaywrightChromium,
): string {
const executableOverride = env[chromiumExecutableOverrideEnvKey]?.trim();
if (executableOverride) {
return executableOverride;
}
if (existsSync(defaultExecutablePath)) {
if (canRun(defaultExecutablePath)) {
return defaultExecutablePath;
}
return (
systemChromiumExecutableCandidates.find((candidate) => existsSync(candidate)) ??
systemChromiumExecutableCandidates.find((candidate) => canRun(candidate)) ??
defaultExecutablePath
);
}
export function canRunPlaywrightChromium(chromiumExecutablePath: string): boolean {
return existsSync(chromiumExecutablePath);
if (!existsSync(chromiumExecutablePath)) {
return false;
}
return spawnSync(chromiumExecutablePath, ["--version"], { stdio: "ignore" }).status === 0;
}
export async function startControlUiE2eServer(): Promise<ControlUiE2eServer> {

View File

@@ -168,7 +168,7 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
beforeAll(async () => {
if (!chromiumAvailable) {
throw new Error(
`Playwright Chromium is not installed at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install chromium\`, set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH to a compatible browser, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
`Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH to a compatible browser, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
);
}
server = await startControlUiE2eServer();

View File

@@ -90,7 +90,7 @@ describeControlUiE2e("Control UI chat picker mocked Gateway E2E", () => {
beforeAll(async () => {
if (!chromiumAvailable) {
throw new Error(
`Playwright Chromium is not installed at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
`Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
);
}
server = await startControlUiE2eServer();

View File

@@ -115,7 +115,7 @@ describeControlUiE2e("Control UI cron mocked Gateway E2E", () => {
beforeAll(async () => {
if (!chromiumAvailable) {
throw new Error(
`Playwright Chromium is not installed at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
`Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
);
}
server = await startControlUiE2eServer();