Skip to content

Commit a3d5e5b

Browse files
committed
fix(test): support macOS Bash 3 script suites
1 parent 7cb2571 commit a3d5e5b

7 files changed

Lines changed: 146 additions & 10 deletions

scripts/lib/docker-e2e-container.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,11 @@ docker_e2e_docker_cmd() {
142142
if [ "${1:-}" = "run" ]; then
143143
shift
144144
docker_e2e_docker_run_resource_args "$@"
145-
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
145+
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
146+
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
147+
else
148+
docker_e2e_timeout_cmd "$timeout_value" docker run "$@"
149+
fi
146150
return
147151
fi
148152
docker_e2e_timeout_cmd "$timeout_value" docker "$@"
@@ -153,7 +157,11 @@ docker_e2e_docker_run_cmd() {
153157
if [ "${1:-}" = "run" ]; then
154158
shift
155159
docker_e2e_docker_run_resource_args "$@"
156-
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
160+
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
161+
docker_e2e_timeout_cmd "$timeout_value" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
162+
else
163+
docker_e2e_timeout_cmd "$timeout_value" docker run "$@"
164+
fi
157165
return
158166
fi
159167
docker_e2e_timeout_cmd "$timeout_value" docker "$@"

scripts/lib/docker-e2e-package.sh

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,18 @@ if ! declare -F docker_e2e_docker_run_cmd >/dev/null 2>&1; then
104104
shift
105105
docker_e2e_docker_run_resource_args "$@"
106106
if declare -F docker_e2e_timeout_cmd >/dev/null 2>&1; then
107-
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
107+
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
108+
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
109+
else
110+
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker run "$@"
111+
fi
108112
return
109113
fi
110-
set -- run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
114+
if [ "${#DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" -gt 0 ]; then
115+
set -- run "${DOCKER_E2E_RUN_RESOURCE_ARGS[@]}" "$@"
116+
else
117+
set -- run "$@"
118+
fi
111119
fi
112120
if declare -F docker_e2e_timeout_cmd >/dev/null 2>&1; then
113121
docker_e2e_timeout_cmd "${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_DOCKER_E2E_RUN_TIMEOUT:-3600s}}" docker "$@"
@@ -333,8 +341,7 @@ docker_e2e_run_with_harness() {
333341
rmdir "$cid_dir" 2>/dev/null || true
334342
docker_e2e_cleanup_package_mount_args
335343
if [ -n "$harness_stdin_fd" ]; then
336-
exec {harness_stdin_fd}<&-
337-
harness_stdin_fd=""
344+
eval "exec ${harness_stdin_fd}<&-"
338345
fi
339346
restore_harness_traps
340347
if [ "$exit_after_cleanup" = "1" ]; then
@@ -345,7 +352,19 @@ docker_e2e_run_with_harness() {
345352
trap 'cleanup_harness_run 130 1' INT
346353
trap 'cleanup_harness_run 143 1' TERM
347354
trap 'cleanup_harness_run 129 1' HUP
348-
exec {harness_stdin_fd}<&0
355+
local candidate_fd
356+
for candidate_fd in 19 18 17 16 15 14 13 12 11 10; do
357+
if ! eval "true <&${candidate_fd}" 2>/dev/null; then
358+
harness_stdin_fd="$candidate_fd"
359+
break
360+
fi
361+
done
362+
if [ -z "$harness_stdin_fd" ]; then
363+
echo "no free file descriptor available for Docker harness stdin" >&2
364+
cleanup_harness_run 1
365+
return 1
366+
fi
367+
eval "exec ${harness_stdin_fd}<&0"
349368
docker_e2e_docker_run_cmd run --rm --cidfile "$cidfile" "${DOCKER_E2E_HARNESS_ARGS[@]}" "$@" <&$harness_stdin_fd &
350369
docker_run_pid="$!"
351370
local had_errexit=0

test/scripts/claude-auth-status.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process";
33
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
44
import path from "node:path";
55
import { describe, expect, it } from "vitest";
6-
import { createScriptTestHarness } from "./test-helpers.ts";
6+
import { createScriptTestHarness, writeNodeBackedJq } from "./test-helpers.ts";
77

88
const SCRIPT = "scripts/claude-auth-status.sh";
99

@@ -16,6 +16,7 @@ describe("claude-auth-status.sh", () => {
1616
mkdirSync(bin, { recursive: true });
1717
const openclaw = path.join(bin, "openclaw");
1818
const futureMs = String(Date.now() + 2 * 60 * 60 * 1000);
19+
writeNodeBackedJq(bin);
1920

2021
writeFileSync(
2122
openclaw,

test/scripts/docker-build-helper.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2002,6 +2002,72 @@ grep -Fxq 'printf "heredoc reached docker\\n"' "$TMPDIR/docker-stdin-seen"
20022002
}
20032003
});
20042004

2005+
it("preserves caller-owned file descriptors around harness runs", () => {
2006+
const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-harness-fd-"));
2007+
try {
2008+
const rootDir = process.cwd();
2009+
const script = String.raw`
2010+
set -euo pipefail
2011+
ROOT_DIR=${shellQuote(rootDir)}
2012+
TMPDIR=${shellQuote(workDir)}
2013+
export ROOT_DIR TMPDIR
2014+
2015+
mkdir -p "$TMPDIR/bin"
2016+
cat >"$TMPDIR/bin/timeout" <<'SH'
2017+
#!/usr/bin/env bash
2018+
case "$1" in
2019+
--kill-after=1s)
2020+
exit 0
2021+
;;
2022+
--kill-after=30s)
2023+
shift 2
2024+
;;
2025+
*)
2026+
shift
2027+
;;
2028+
esac
2029+
"$@"
2030+
SH
2031+
chmod +x "$TMPDIR/bin/timeout"
2032+
export PATH="$TMPDIR/bin:$PATH"
2033+
2034+
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
2035+
2036+
docker() {
2037+
local cidfile=""
2038+
local expect_cidfile=0
2039+
local arg
2040+
for arg in "$@"; do
2041+
if [[ "$expect_cidfile" == "1" ]]; then
2042+
cidfile="$arg"
2043+
expect_cidfile=0
2044+
continue
2045+
fi
2046+
if [[ "$arg" == "--cidfile" ]]; then
2047+
expect_cidfile=1
2048+
fi
2049+
done
2050+
test -n "$cidfile"
2051+
printf "container-fd\n" >"$cidfile"
2052+
cat >/dev/null
2053+
}
2054+
export -f docker
2055+
2056+
exec 19>"$TMPDIR/caller-fd"
2057+
docker_e2e_run_with_harness image-name bash -s <<'SH'
2058+
true
2059+
SH
2060+
printf "preserved\n" >&19
2061+
exec 19>&-
2062+
grep -Fxq preserved "$TMPDIR/caller-fd"
2063+
`;
2064+
2065+
execFileSync("bash", ["-lc", script], { encoding: "utf8" });
2066+
} finally {
2067+
rmSync(workDir, { recursive: true, force: true });
2068+
}
2069+
});
2070+
20052071
it("cleans Codex npm plugin live package artifacts on every exit path", () => {
20062072
const runner = readFileSync(CODEX_NPM_PLUGIN_LIVE_DOCKER_E2E_PATH, "utf8");
20072073

test/scripts/github-activity-helper.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
44
import { tmpdir } from "node:os";
55
import path from "node:path";
66
import { afterEach, describe, expect, it } from "vitest";
7+
import { writeNodeBackedJq } from "./test-helpers.ts";
78

89
const repoRoot = path.resolve(import.meta.dirname, "../..");
910
const helperPath = path.join(
@@ -25,6 +26,7 @@ function runHelper(args: string[]) {
2526
const logPath = path.join(dir, "gh.log");
2627
const ghPath = path.join(binDir, "gh");
2728
mkdirSync(binDir);
29+
writeNodeBackedJq(binDir);
2830
writeFileSync(
2931
ghPath,
3032
`#!/usr/bin/env bash

test/scripts/test-helpers.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,44 @@ import os from "node:os";
55
import path from "node:path";
66
import { afterEach } from "vitest";
77

8+
export function writeNodeBackedJq(binDir: string): void {
9+
const jqPath = path.join(binDir, "jq");
10+
fs.writeFileSync(
11+
jqPath,
12+
`#!/usr/bin/env node
13+
const fs = require("node:fs");
14+
const args = process.argv.slice(2);
15+
const query = args.at(-1) ?? "";
16+
const input = JSON.parse(fs.readFileSync(0, "utf8"));
17+
const print = (value) => process.stdout.write(String(value ?? "") + "\\n");
18+
19+
if (query === ".login") print(input.login);
20+
else if (query === ".name // empty") print(input.name ?? "");
21+
else if (query === ".created_at") print(input.created_at);
22+
else if (query === ".type") print(input.type);
23+
else if (query === ".totalCommitContributions") print(input.totalCommitContributions);
24+
else if (query === ".totalIssueContributions") print(input.totalIssueContributions);
25+
else if (query === ".totalPullRequestContributions") print(input.totalPullRequestContributions);
26+
else if (query === ".totalPullRequestReviewContributions") print(input.totalPullRequestReviewContributions);
27+
else if (query.includes("{id: .profileId")) {
28+
const profiles = input.auth?.oauth?.profiles ?? [];
29+
const profile = profiles.filter((item) => item.provider === "anthropic" && item.type === "oauth").sort((a, b) => (b.expiresAt ?? 0) - (a.expiresAt ?? 0))[0];
30+
print(profile?.profileId ?? "none");
31+
} else if (query.includes(".auth.providers[]")) {
32+
const counts = (input.auth?.providers ?? []).filter((item) => item.provider === "anthropic").map((item) => item.profiles?.apiKey ?? 0);
33+
print(Math.max(0, ...counts));
34+
} else if (query.includes(".auth.oauth.profiles[]")) {
35+
const profiles = (input.auth?.oauth?.profiles ?? []).filter((item) => item.provider === "anthropic" && item.type === "oauth");
36+
print(Math.max(0, ...profiles.map((item) => item.expiresAt ?? 0)));
37+
} else {
38+
process.stderr.write("unsupported jq query: " + query + "\\n");
39+
process.exit(2);
40+
}
41+
`,
42+
);
43+
fs.chmodSync(jqPath, 0o755);
44+
}
45+
846
export function createScriptTestHarness() {
947
const tempDirs: string[] = [];
1048

test/scripts/test-report-utils.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
collectVitestAssertionDurations,
88
collectVitestFileDurations,
99
normalizeTrackedRepoPath,
10-
runVitestJsonReport,
1110
tryReadJsonFile,
1211
} from "../../scripts/test-report-utils.mjs";
1312

@@ -112,10 +111,12 @@ describe("scripts/test-report-utils tryReadJsonFile", () => {
112111

113112
describe("scripts/test-report-utils runVitestJsonReport", () => {
114113
beforeEach(() => {
114+
vi.resetModules();
115115
spawnSyncMock.mockReset();
116116
});
117117

118118
it("launches Vitest through pnpm exec", async () => {
119+
const { runVitestJsonReport } = await import("../../scripts/test-report-utils.mjs");
119120
const reportPath = path.join(os.tmpdir(), `openclaw-vitest-json-${Date.now()}.json`);
120121
spawnSyncMock.mockImplementation(() => {
121122
fs.writeFileSync(reportPath, `${JSON.stringify({ testResults: [] })}\n`, "utf8");
@@ -152,7 +153,8 @@ describe("scripts/test-report-utils runVitestJsonReport", () => {
152153
);
153154
});
154155

155-
it("fails when Vitest exits successfully without writing a JSON report", () => {
156+
it("fails when Vitest exits successfully without writing a JSON report", async () => {
157+
const { runVitestJsonReport } = await import("../../scripts/test-report-utils.mjs");
156158
spawnSyncMock.mockReturnValue({ status: 0 });
157159
const reportPath = path.join(os.tmpdir(), `openclaw-vitest-json-missing-${Date.now()}.json`);
158160
const exitSpy = vi.spyOn(process, "exit").mockImplementation((code) => {

0 commit comments

Comments
 (0)