fix(test): support macOS Bash 3 script suites

This commit is contained in:
Vincent Koc
2026-06-10 15:37:11 +09:00
parent 7cb2571a99
commit a3d5e5bc72
7 changed files with 146 additions and 10 deletions

View File

@@ -142,7 +142,11 @@ docker_e2e_docker_cmd() {
if [ "${1:-}" = "run" ]; then
shift
docker_e2e_docker_run_resource_args "$@"
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
else
docker_e2e_timeout_cmd "$timeout_value" docker run "$@"
fi
return
fi
docker_e2e_timeout_cmd "$timeout_value" docker "$@"
@@ -153,7 +157,11 @@ docker_e2e_docker_run_cmd() {
if [ "${1:-}" = "run" ]; then
shift
docker_e2e_docker_run_resource_args "$@"
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
else
docker_e2e_timeout_cmd "$timeout_value" docker run "$@"
fi
return
fi
docker_e2e_timeout_cmd "$timeout_value" docker "$@"

View File

@@ -104,10 +104,18 @@ if ! declare -F docker_e2e_docker_run_cmd >/dev/null 2>&1; then
shift
docker_e2e_docker_run_resource_args "$@"
if declare -F docker_e2e_timeout_cmd >/dev/null 2>&1; then
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
else
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker run "$@"
fi
return
fi
set -- run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
set -- run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
else
set -- run "$@"
fi
fi
if declare -F docker_e2e_timeout_cmd >/dev/null 2>&1; then
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker "$@"
@@ -333,8 +341,7 @@ docker_e2e_run_with_harness() {
rmdir "$cid_dir" 2>/dev/null || true
docker_e2e_cleanup_package_mount_args
if [ -n "$harness_stdin_fd" ]; then
exec {harness_stdin_fd}<&-
harness_stdin_fd=""
eval "exec ${harness_stdin_fd}<&-"
fi
restore_harness_traps
if [ "$exit_after_cleanup" = "1" ]; then
@@ -345,7 +352,19 @@ docker_e2e_run_with_harness() {
trap 'cleanup_harness_run 130 1' INT
trap 'cleanup_harness_run 143 1' TERM
trap 'cleanup_harness_run 129 1' HUP
exec {harness_stdin_fd}<&0
local candidate_fd
for candidate_fd in 19 18 17 16 15 14 13 12 11 10; do
if ! eval "true <&${candidate_fd}" 2>/dev/null; then
harness_stdin_fd="$candidate_fd"
break
fi
done
if [ -z "$harness_stdin_fd" ]; then
echo "no free file descriptor available for Docker harness stdin" >&2
cleanup_harness_run 1
return 1
fi
eval "exec ${harness_stdin_fd}<&0"
docker_e2e_docker_run_cmd run --rm --cidfile "$cidfile" "${DOCKER_E2E_HARNESS_ARGS[@]}" "$@" <&$harness_stdin_fd &
docker_run_pid="$!"
local had_errexit=0

View File

@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createScriptTestHarness } from "./test-helpers.ts";
import { createScriptTestHarness, writeNodeBackedJq } from "./test-helpers.ts";
const SCRIPT = "scripts/claude-auth-status.sh";
@@ -16,6 +16,7 @@ describe("claude-auth-status.sh", () => {
mkdirSync(bin, { recursive: true });
const openclaw = path.join(bin, "openclaw");
const futureMs = String(Date.now() + 2 * 60 * 60 * 1000);
writeNodeBackedJq(bin);
writeFileSync(
openclaw,

View File

@@ -2002,6 +2002,72 @@ grep -Fxq 'printf "heredoc reached docker\\n"' "$TMPDIR/docker-stdin-seen"
}
});
it("preserves caller-owned file descriptors around harness runs", () => {
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-harness-fd-"));
try {
const rootDir = process.cwd();
const script = String.raw`
set -euo pipefail
ROOT_DIR=${shellQuote(rootDir)}
TMPDIR=${shellQuote(workDir)}
export ROOT_DIR TMPDIR
mkdir -p "$TMPDIR/bin"
cat >"$TMPDIR/bin/timeout" <<'SH'
#!/usr/bin/env bash
case "$1" in
--kill-after=1s)
exit 0
;;
--kill-after=30s)
shift 2
;;
*)
shift
;;
esac
"$@"
SH
chmod +x "$TMPDIR/bin/timeout"
export PATH="$TMPDIR/bin:$PATH"
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
docker() {
local cidfile=""
local expect_cidfile=0
local arg
for arg in "$@"; do
if [[ "$expect_cidfile" == "1" ]]; then
cidfile="$arg"
expect_cidfile=0
continue
fi
if [[ "$arg" == "--cidfile" ]]; then
expect_cidfile=1
fi
done
test -n "$cidfile"
printf "container-fd\n" >"$cidfile"
cat >/dev/null
}
export -f docker
exec 19>"$TMPDIR/caller-fd"
docker_e2e_run_with_harness image-name bash -s <<'SH'
true
SH
printf "preserved\n" >&19
exec 19>&-
grep -Fxq preserved "$TMPDIR/caller-fd"
`;
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
});
it("cleans Codex npm plugin live package artifacts on every exit path", () => {
const runner = readFileSync(CODEX_NPM_PLUGIN_LIVE_DOCKER_E2E_PATH, "utf8");

View File

@@ -4,6 +4,7 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { writeNodeBackedJq } from "./test-helpers.ts";
const repoRoot = path.resolve(import.meta.dirname, "../..");
const helperPath = path.join(
@@ -25,6 +26,7 @@ function runHelper(args: string[]) {
const logPath = path.join(dir, "gh.log");
const ghPath = path.join(binDir, "gh");
mkdirSync(binDir);
writeNodeBackedJq(binDir);
writeFileSync(
ghPath,
`#!/usr/bin/env bash

View File

@@ -5,6 +5,44 @@ import os from "node:os";
import path from "node:path";
import { afterEach } from "vitest";
export function writeNodeBackedJq(binDir: string): void {
const jqPath = path.join(binDir, "jq");
fs.writeFileSync(
jqPath,
`#!/usr/bin/env node
const fs = require("node:fs");
const args = process.argv.slice(2);
const query = args.at(-1) ?? "";
const input = JSON.parse(fs.readFileSync(0, "utf8"));
const print = (value) => process.stdout.write(String(value ?? "") + "\\n");
if (query === ".login") print(input.login);
else if (query === ".name // empty") print(input.name ?? "");
else if (query === ".created_at") print(input.created_at);
else if (query === ".type") print(input.type);
else if (query === ".totalCommitContributions") print(input.totalCommitContributions);
else if (query === ".totalIssueContributions") print(input.totalIssueContributions);
else if (query === ".totalPullRequestContributions") print(input.totalPullRequestContributions);
else if (query === ".totalPullRequestReviewContributions") print(input.totalPullRequestReviewContributions);
else if (query.includes("{id: .profileId")) {
const profiles = input.auth?.oauth?.profiles ?? [];
const profile = profiles.filter((item) => item.provider === "anthropic" && item.type === "oauth").sort((a, b) => (b.expiresAt ?? 0) - (a.expiresAt ?? 0))[0];
print(profile?.profileId ?? "none");
} else if (query.includes(".auth.providers[]")) {
const counts = (input.auth?.providers ?? []).filter((item) => item.provider === "anthropic").map((item) => item.profiles?.apiKey ?? 0);
print(Math.max(0, ...counts));
} else if (query.includes(".auth.oauth.profiles[]")) {
const profiles = (input.auth?.oauth?.profiles ?? []).filter((item) => item.provider === "anthropic" && item.type === "oauth");
print(Math.max(0, ...profiles.map((item) => item.expiresAt ?? 0)));
} else {
process.stderr.write("unsupported jq query: " + query + "\\n");
process.exit(2);
}
`,
);
fs.chmodSync(jqPath, 0o755);
}
export function createScriptTestHarness() {
const tempDirs: string[] = [];

View File

@@ -7,7 +7,6 @@ import {
collectVitestAssertionDurations,
collectVitestFileDurations,
normalizeTrackedRepoPath,
runVitestJsonReport,
tryReadJsonFile,
} from "../../scripts/test-report-utils.mjs";
@@ -112,10 +111,12 @@ describe("scripts/test-report-utils tryReadJsonFile", () => {
describe("scripts/test-report-utils runVitestJsonReport", () => {
beforeEach(() => {
vi.resetModules();
spawnSyncMock.mockReset();
});
it("launches Vitest through pnpm exec", async () => {
const { runVitestJsonReport } = await import("../../scripts/test-report-utils.mjs");
const reportPath = path.join(os.tmpdir(), `openclaw-vitest-json-${Date.now()}.json`);
spawnSyncMock.mockImplementation(() => {
fs.writeFileSync(reportPath, `${JSON.stringify({ testResults: [] })}\n`, "utf8");
@@ -152,7 +153,8 @@ describe("scripts/test-report-utils runVitestJsonReport", () => {
);
});
it("fails when Vitest exits successfully without writing a JSON report", () => {
it("fails when Vitest exits successfully without writing a JSON report", async () => {
const { runVitestJsonReport } = await import("../../scripts/test-report-utils.mjs");
spawnSyncMock.mockReturnValue({ status: 0 });
const reportPath = path.join(os.tmpdir(), `openclaw-vitest-json-missing-${Date.now()}.json`);
const exitSpy = vi.spyOn(process, "exit").mockImplementation((code) => {