From 42a8679c4dfee6e855d77e7d367f153ff5088f7f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 9 Jul 2026 09:39:08 +0100 Subject: [PATCH] fix: restore release validation on main (#102516) * test(qqbot): assert approval fence exactly * fix(release): wait for ClawHub publish logging * fix(ci): repair Bun global install smoke * test(slack): fix app identity fixture --- .../qqbot/src/engine/approval/index.test.ts | 4 +- .../slack/src/monitor.tool-result.test.ts | 2 +- scripts/e2e/bun-global-install-smoke.sh | 147 +++++++++---- .../e2e/lib/bun-global-install/assertions.mjs | 29 ++- scripts/plugin-clawhub-publish.sh | 2 +- test/scripts/test-install-sh-docker.test.ts | 200 ++++++++++++------ 6 files changed, 278 insertions(+), 106 deletions(-) diff --git a/extensions/qqbot/src/engine/approval/index.test.ts b/extensions/qqbot/src/engine/approval/index.test.ts index 1ae00afc8fc..949b6f43e58 100644 --- a/extensions/qqbot/src/engine/approval/index.test.ts +++ b/extensions/qqbot/src/engine/approval/index.test.ts @@ -32,7 +32,7 @@ describe("buildExecApprovalText", () => { commandPreview: `${safePrefix}🎉 trailing text`, }, }); - const codeFence = text.split("```\n")[1]?.split("\n```")[0] ?? ""; - expect(codeFence).toBe(safePrefix); + const expectedCommandBlock = ["```", safePrefix, "```"].join("\n"); + expect(text).toContain(expectedCommandBlock); }); }); diff --git a/extensions/slack/src/monitor.tool-result.test.ts b/extensions/slack/src/monitor.tool-result.test.ts index e83a9f4080f..edcd2d78e6e 100644 --- a/extensions/slack/src/monitor.tool-result.test.ts +++ b/extensions/slack/src/monitor.tool-result.test.ts @@ -351,7 +351,7 @@ describe("monitorSlackProvider tool results", () => { (client.auth as { test: ReturnType }).test.mockResolvedValue({ user_id: "bot-user", team_id: "T1", - api_app_id: "A1", + app_id: "A1", }); await runSlackMessageOnce( diff --git a/scripts/e2e/bun-global-install-smoke.sh b/scripts/e2e/bun-global-install-smoke.sh index b628f6d21a6..39123024742 100755 --- a/scripts/e2e/bun-global-install-smoke.sh +++ b/scripts/e2e/bun-global-install-smoke.sh @@ -24,6 +24,7 @@ DIST_IMAGE="${OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE:-}" PACKAGE_TGZ="${OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ:-}" COMMAND_TIMEOUT_MS="$(read_positive_int_env OPENCLAW_BUN_GLOBAL_SMOKE_TIMEOUT_MS 180000)" DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_BUN_GLOBAL_SMOKE_DOCKER_COMMAND_TIMEOUT:-600s}}" +AI_PACKAGE_TGZ="" SMOKE_DIR="" PACK_DIR="" @@ -36,6 +37,39 @@ cleanup() { fi } +prepare_ai_candidate() { + local ai_manifest + local ai_package_dir + local ai_tarballs + local root_manifest + + if [ -z "$PACK_DIR" ]; then + PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-pack.XXXXXX")" + fi + echo "==> Extract bundled candidate @openclaw/ai package" + ai_package_dir="$PACK_DIR/ai-candidate" + mkdir -p "$ai_package_dir" + tar -xzf "$PACKAGE_TGZ" \ + -C "$ai_package_dir" \ + --strip-components=4 \ + package/node_modules/@openclaw/ai + root_manifest="$PACK_DIR/openclaw-package.json" + ai_manifest="$ai_package_dir/package.json" + tar -xOf "$PACKAGE_TGZ" package/package.json >"$root_manifest" + node scripts/e2e/lib/bun-global-install/assertions.mjs \ + assert-release-versions \ + "$root_manifest" \ + "$ai_manifest" \ + >/dev/null + npm pack --ignore-scripts --silent --pack-destination "$PACK_DIR" "$ai_package_dir" >/dev/null + ai_tarballs=("$PACK_DIR"/openclaw-ai-*.tgz) + if [ "${#ai_tarballs[@]}" -ne 1 ] || [ ! -f "${ai_tarballs[0]}" ]; then + echo "expected one packed @openclaw/ai candidate in $PACK_DIR" >&2 + exit 1 + fi + AI_PACKAGE_TGZ="${ai_tarballs[0]}" +} + trap cleanup EXIT run_with_timeout() { @@ -44,52 +78,50 @@ run_with_timeout() { node scripts/e2e/lib/bun-global-install/assertions.mjs run-with-timeout "$timeout_ms" "$@" } -resolve_pack_tarball_path() { - local pack_json_file="$1" - local pack_dir="$2" - node -e ' -const fs = require("node:fs"); -const path = require("node:path"); -const raw = fs.readFileSync(process.argv[1], "utf8") || "[]"; -const parsed = JSON.parse(raw); -const last = Array.isArray(parsed) ? parsed.at(-1) : null; -const filename = typeof last?.filename === "string" ? last.filename.trim() : ""; -if ( - !filename.endsWith(".tgz") || - filename.includes("\0") || - filename !== path.basename(filename) || - filename !== path.win32.basename(filename) -) { - console.error(`ERROR: npm pack reported unsafe tarball filename ${JSON.stringify(filename)}`); - process.exit(1); -} -process.stdout.write(path.resolve(process.argv[2], filename)); -' "$pack_json_file" "$pack_dir" -} - restore_dist_from_image() { local image="$1" + local ai_backup_dir="" + local ai_dist_installed=0 local backup_dir="" local container_id="" - local swapped=0 + local dist_installed=0 + local restore_complete=0 local temp_dir="" cleanup_restore_dist() { if [ -n "$container_id" ]; then docker_e2e_docker_cmd rm -f "$container_id" >/dev/null 2>&1 || true fi - if [ "$swapped" != "1" ] && [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then - rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true - if [ ! -e "$ROOT_DIR/dist" ] && mv "$backup_dir" "$ROOT_DIR/dist" >/dev/null 2>&1; then - backup_dir="" + # Both build trees come from one image. A partial swap must restore both or + # the following package step could mix artifacts from different builds. + if [ "$restore_complete" != "1" ]; then + if [ "$dist_installed" = "1" ]; then + rm -rf "$ROOT_DIR/dist" >/dev/null 2>&1 || true + fi + if [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then + if [ ! -e "$ROOT_DIR/dist" ] && mv "$backup_dir" "$ROOT_DIR/dist" >/dev/null 2>&1; then + backup_dir="" + fi + fi + if [ "$ai_dist_installed" = "1" ]; then + rm -rf "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1 || true + fi + if [ -n "$ai_backup_dir" ] && [ -d "$ai_backup_dir" ]; then + if [ ! -e "$ROOT_DIR/packages/ai/dist" ] && \ + mv "$ai_backup_dir" "$ROOT_DIR/packages/ai/dist" >/dev/null 2>&1; then + ai_backup_dir="" + fi fi fi if [ -n "$temp_dir" ]; then rm -rf "$temp_dir" fi - if [ "$swapped" = "1" ] && [ -n "$backup_dir" ]; then + if [ "$restore_complete" = "1" ] && [ -n "$backup_dir" ]; then rm -rf "$backup_dir" fi + if [ "$restore_complete" = "1" ] && [ -n "$ai_backup_dir" ]; then + rm -rf "$ai_backup_dir" + fi } echo "==> Reuse dist/ from Docker image: $image" @@ -105,6 +137,12 @@ restore_dist_from_image() { cleanup_restore_dist return 1 fi + if ! docker_e2e_docker_cmd cp \ + "${container_id}:/app/node_modules/@openclaw/ai/dist" \ + "$temp_dir/ai-dist"; then + cleanup_restore_dist + return 1 + fi if [ -e "$ROOT_DIR/dist" ]; then if ! backup_dir="$(mktemp -d "$ROOT_DIR/.dist-backup.XXXXXX")"; then cleanup_restore_dist @@ -123,7 +161,27 @@ restore_dist_from_image() { cleanup_restore_dist return 1 fi - swapped=1 + dist_installed=1 + if [ -e "$ROOT_DIR/packages/ai/dist" ]; then + if ! ai_backup_dir="$(mktemp -d "$ROOT_DIR/packages/ai/.dist-backup.XXXXXX")"; then + cleanup_restore_dist + return 1 + fi + if ! rmdir "$ai_backup_dir"; then + cleanup_restore_dist + return 1 + fi + if ! mv "$ROOT_DIR/packages/ai/dist" "$ai_backup_dir"; then + cleanup_restore_dist + return 1 + fi + fi + if ! mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"; then + cleanup_restore_dist + return 1 + fi + ai_dist_installed=1 + restore_complete=1 cleanup_restore_dist } @@ -151,16 +209,15 @@ resolve_package_tgz() { exit 1 fi - echo "==> Write package inventory" - node --import tsx scripts/write-package-dist-inventory.ts - - local pack_json_file PACK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-pack.XXXXXX")" - pack_json_file="$PACK_DIR/pack.json" echo "==> Pack OpenClaw tarball" - npm pack --ignore-scripts --json --pack-destination "$PACK_DIR" >"$pack_json_file" - PACKAGE_TGZ="$(resolve_pack_tarball_path "$pack_json_file" "$PACK_DIR")" + PACKAGE_TGZ="$( + node scripts/package-openclaw-for-docker.mjs \ + --skip-build \ + --output-dir "$PACK_DIR" \ + --output-name openclaw-current.tgz + )" if [ -z "$PACKAGE_TGZ" ] || [ ! -f "$PACKAGE_TGZ" ]; then echo "missing packed OpenClaw tarball" >&2 exit 1 @@ -176,6 +233,7 @@ main() { fi resolve_package_tgz + prepare_ai_candidate local bun_path local openclaw_bin @@ -188,8 +246,21 @@ main() { export OPENCLAW_NO_ONBOARD=1 export OPENCLAW_DISABLE_UPDATE_CHECK=1 export NO_COLOR=1 - mkdir -p "$HOME" "$BUN_INSTALL/bin" "$XDG_CACHE_HOME" + mkdir -p "$HOME" "$BUN_INSTALL/bin" "$BUN_INSTALL/install/global" "$XDG_CACHE_HOME" export PATH="$BUN_INSTALL/bin:$(dirname "$(command -v node)"):$PATH" + # Release publishes @openclaw/ai first. Bun 1.3.14 ignores bundled deps in + # local tarballs, so resolve that one package from the exact candidate bytes. + node --input-type=module - \ + "$BUN_INSTALL/install/global/package.json" \ + "$AI_PACKAGE_TGZ" <<'NODE' +import fs from "node:fs"; + +const [, , packageJsonPath, aiPackageTarball] = process.argv; +fs.writeFileSync( + packageJsonPath, + `${JSON.stringify({ private: true, overrides: { "@openclaw/ai": `file:${aiPackageTarball}` } })}\n`, +); +NODE echo "==> Bun version" "$bun_path" --version diff --git a/scripts/e2e/lib/bun-global-install/assertions.mjs b/scripts/e2e/lib/bun-global-install/assertions.mjs index 80f45aa6ebb..9656106f5fd 100644 --- a/scripts/e2e/lib/bun-global-install/assertions.mjs +++ b/scripts/e2e/lib/bun-global-install/assertions.mjs @@ -1,11 +1,14 @@ // Assertions for Bun global install E2E validation. import { spawn } from "node:child_process"; +import fs from "node:fs"; const DEFAULT_TIMEOUT_KILL_GRACE_MS = 30_000; const PARENT_TERMINATION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"]; const usage = () => { - console.error("Usage: assertions.mjs [...]"); + console.error( + "Usage: assertions.mjs [...]", + ); process.exit(2); }; @@ -204,4 +207,28 @@ if (mode === "assert-image-providers") { process.exit(0); } +if (mode === "assert-release-versions") { + const [rootManifestPath, aiManifestPath] = args; + if (!rootManifestPath || !aiManifestPath) { + usage(); + } + const rootManifest = JSON.parse(fs.readFileSync(rootManifestPath, "utf8")); + const aiManifest = JSON.parse(fs.readFileSync(aiManifestPath, "utf8")); + const rootVersion = rootManifest.version; + const aiVersion = aiManifest.version; + const rootAiVersion = rootManifest.dependencies?.["@openclaw/ai"]; + if ( + typeof rootVersion !== "string" || + typeof aiVersion !== "string" || + rootVersion !== aiVersion || + rootAiVersion !== aiVersion + ) { + throw new Error( + `candidate version mismatch: openclaw=${String(rootVersion)}, dependency=${String(rootAiVersion)}, @openclaw/ai=${String(aiVersion)}`, + ); + } + process.stdout.write(aiVersion); + process.exit(0); +} + usage(); diff --git a/scripts/plugin-clawhub-publish.sh b/scripts/plugin-clawhub-publish.sh index 758006f9c9b..bf65d7be621 100644 --- a/scripts/plugin-clawhub-publish.sh +++ b/scripts/plugin-clawhub-publish.sh @@ -200,7 +200,7 @@ fi publish_log="${pack_dir}/publish.log" for attempt in $(seq 1 "${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8}"); do - if CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}" > >(tee "${publish_log}") 2>&1; then + if CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}" 2>&1 | tee "${publish_log}"; then exit 0 fi if ! grep -Eqi "rate limit|too many requests|\\b429\\b" "${publish_log}"; then diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 93f3ce90f46..99ff1ae7e09 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -1,6 +1,14 @@ // Test Install Sh Docker tests cover test install sh docker script behavior. import { spawn, spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import path, { join } from "node:path"; import { runInNewContext } from "node:vm"; @@ -216,40 +224,6 @@ read_pack_tarball_filename "$pack_json_file"`, ); } -function extractResolvePackTarballPath(): string { - const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8"); - const match = script.match(/(resolve_pack_tarball_path\(\) \{[\s\S]*?\n\})\n\nrestore_dist/u); - if (!match) { - throw new Error("resolve_pack_tarball_path helper was not found"); - } - return match[1]; -} - -function runResolvePackTarballPath(filename: string) { - return spawnSync( - "bash", - [ - "--noprofile", - "--norc", - "-c", - `${extractResolvePackTarballPath()} -pack_dir="$(mktemp -d)" -pack_json_file="$pack_dir/pack.json" -trap 'rm -rf "$pack_dir"' EXIT -printf '%s' "$PACK_JSON" >"$pack_json_file" -resolve_pack_tarball_path "$pack_json_file" "$pack_dir"`, - ], - { - encoding: "utf8", - env: { - HOME: "/tmp", - PACK_JSON: JSON.stringify([{ filename }]), - PATH: process.env.PATH ?? "", - }, - }, - ); -} - 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"); @@ -902,7 +876,10 @@ describe("bun global install smoke", () => { const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8"); const assertions = readFileSync(BUN_GLOBAL_ASSERTIONS_PATH, "utf8"); - expect(script).toContain("npm pack --ignore-scripts --json --pack-destination"); + expect(script).toContain("node scripts/package-openclaw-for-docker.mjs"); + expect(script).toContain("--skip-build"); + expect(script).toContain("--output-name openclaw-current.tgz"); + expect(script).not.toContain("npm pack --ignore-scripts --json --pack-destination"); expect(script).toContain('"$bun_path" install -g "$PACKAGE_TGZ" --no-progress'); expect(script).toContain("infer image providers --json"); expect(script).toContain("assert-image-providers"); @@ -919,6 +896,9 @@ describe("bun global install smoke", () => { expect(script).toContain( 'docker_e2e_docker_cmd cp "${container_id}:/app/dist" "$temp_dir/dist"', ); + expect(script).toContain('"${container_id}:/app/node_modules/@openclaw/ai/dist"'); + expect(script).toContain('"$temp_dir/ai-dist"'); + expect(script).toContain('mv "$temp_dir/ai-dist" "$ROOT_DIR/packages/ai/dist"'); expect(script).toContain("cleanup_restore_dist() {"); expect(script).toContain('mv "$ROOT_DIR/dist" "$backup_dir"'); expect(script).toContain('mv "$temp_dir/dist" "$ROOT_DIR/dist"'); @@ -947,40 +927,134 @@ describe("bun global install smoke", () => { expect(result.stderr).not.toContain("Bun is required"); }); - it("keeps npm pack tarball paths inside the Bun smoke pack directory", () => { + it("uses the canonical package builder for bundled workspace dependencies", () => { const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8"); - expect(script).toContain("resolve_pack_tarball_path()"); - expect(script).toContain( - 'PACKAGE_TGZ="$(resolve_pack_tarball_path "$pack_json_file" "$PACK_DIR")"', + expect(script).toContain('PACK_DIR="$(mktemp -d'); + expect(script).toContain("node scripts/package-openclaw-for-docker.mjs"); + expect(script).toContain('--output-dir "$PACK_DIR"'); + expect(script).toContain("--output-name openclaw-current.tgz"); + }); + + it("resolves the matching candidate AI package without changing the public registry", () => { + const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8"); + + expect(script).toContain("assert-release-versions"); + expect(script).toContain('"$BUN_INSTALL/install/global/package.json"'); + expect(script).toContain("package/node_modules/@openclaw/ai"); + expect(script).toContain("--strip-components=4"); + expect(script).toContain('npm pack --ignore-scripts --silent --pack-destination "$PACK_DIR"'); + expect(script).toContain('overrides: { "@openclaw/ai": `file:${aiPackageTarball}` }'); + expect(script).not.toContain("--registry"); + expect(script).not.toContain("@openclaw:registry"); + }); + + it("requires root and AI candidate versions to match", () => { + const tempDir = tempDirs.make("openclaw-bun-candidate-versions-"); + const rootManifestPath = join(tempDir, "openclaw.json"); + const aiManifestPath = join(tempDir, "ai.json"); + writeFileSync( + rootManifestPath, + JSON.stringify({ + name: "openclaw", + version: "2026.6.17", + dependencies: { "@openclaw/ai": "2026.6.17" }, + }), + ); + writeFileSync(aiManifestPath, JSON.stringify({ name: "@openclaw/ai", version: "2026.6.17" })); + + const matching = spawnSync( + process.execPath, + [BUN_GLOBAL_ASSERTIONS_PATH, "assert-release-versions", rootManifestPath, aiManifestPath], + { encoding: "utf8" }, + ); + expect(matching).toMatchObject({ status: 0, stdout: "2026.6.17" }); + + writeFileSync(aiManifestPath, JSON.stringify({ name: "@openclaw/ai", version: "2026.6.18" })); + const mismatched = spawnSync( + process.execPath, + [BUN_GLOBAL_ASSERTIONS_PATH, "assert-release-versions", rootManifestPath, aiManifestPath], + { encoding: "utf8" }, + ); + expect(mismatched.status).not.toBe(0); + expect(mismatched.stderr).toContain( + "candidate version mismatch: openclaw=2026.6.17, dependency=2026.6.17, @openclaw/ai=2026.6.18", ); - expect(script).toContain("filename !== path.basename(filename)"); - expect(script).toContain("filename !== path.win32.basename(filename)"); - expect(script).toContain("npm pack reported unsafe tarball filename"); }); - it("rejects path-like npm pack tarball filenames in Bun smoke metadata", () => { - const safeResult = runResolvePackTarballPath("openclaw-2026.6.17.tgz"); + it.runIf(process.platform !== "win32")( + "uses bundled AI bytes when a prebuilt tarball is provided", + () => { + const tempDir = tempDirs.make("openclaw-bun-prebuilt-"); + const packageDir = join(tempDir, "fixture", "package"); + const aiDir = join(packageDir, "node_modules", "@openclaw", "ai"); + const packageTgz = join(tempDir, "openclaw-prebuilt.tgz"); + const bunPath = join(tempDir, "bun"); + mkdirSync(aiDir, { recursive: true }); + writeFileSync( + join(packageDir, "package.json"), + JSON.stringify({ + name: "openclaw", + version: "2026.6.17", + dependencies: { "@openclaw/ai": "2026.6.17" }, + bundleDependencies: ["@openclaw/ai"], + }), + ); + writeFileSync( + join(aiDir, "package.json"), + JSON.stringify({ name: "@openclaw/ai", version: "2026.6.17" }), + ); + const packed = spawnSync( + "tar", + ["-czf", packageTgz, "-C", join(tempDir, "fixture"), "package"], + { + encoding: "utf8", + }, + ); + expect(packed.status, packed.stderr).toBe(0); + writeFileSync( + bunPath, + `#!/usr/bin/env bash +set -euo pipefail +if [ "\${1:-}" = "--version" ]; then + echo "1.3.14" + exit 0 +fi +override="$(node -e 'const p=require(process.argv[1]);process.stdout.write(p.overrides["@openclaw/ai"])' "$BUN_INSTALL/install/global/package.json")" +case "\${override#file:}" in + *.tgz) ;; + *) exit 1 ;; +esac +test -f "\${override#file:}" +mkdir -p "$BUN_INSTALL/bin" +cat >"$BUN_INSTALL/bin/openclaw" <<'OPENCLAW' +#!/usr/bin/env bash +if [ "\${1:-}" = "--version" ]; then + echo "OpenClaw 2026.6.17" +else + printf '[{"id":"google"},{"id":"openai"},{"id":"xai"}]\n' +fi +OPENCLAW +chmod +x "$BUN_INSTALL/bin/openclaw" +`, + ); + chmodSync(bunPath, 0o755); - expect(safeResult.status).toBe(0); - expect(safeResult.stdout).toMatch(/\/openclaw-2026\.6\.17\.tgz$/u); + const result = spawnSync("bash", [BUN_GLOBAL_SMOKE_PATH], { + encoding: "utf8", + env: { + ...process.env, + BUN_BIN: bunPath, + OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD: "0", + OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ: packageTgz, + OPENCLAW_BUN_GLOBAL_SMOKE_TIMEOUT_MS: "10000", + }, + }); - const unsafeFilenames = [ - "../openclaw.tgz", - "nested/openclaw.tgz", - "nested\\openclaw.tgz", - "/tmp/openclaw.tgz", - "C:\\temp\\openclaw.tgz", - "openclaw.tar.gz", - ]; - - for (const filename of unsafeFilenames) { - const result = runResolvePackTarballPath(filename); - - expect(result.status, filename).not.toBe(0); - expect(result.stderr, filename).toContain("npm pack reported unsafe tarball filename"); - } - }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("bun-global-install-smoke: image providers OK (3 providers)"); + }, + ); it.runIf(process.platform !== "win32" && existsSync("/usr/bin/time"))( "preserves Bun global timeout kill grace after the leader exits",