



























1-import { execFileSync } from "node:child_process";
1+import { execFileSync, spawn } from "node:child_process";
22import {
33chmodSync,
4+existsSync,
45mkdtempSync,
56mkdirSync,
67readdirSync,
@@ -10,6 +11,7 @@ import {
1011} from "node:fs";
1112import { tmpdir } from "node:os";
1213import { join } from "node:path";
14+import { setTimeout as delay } from "node:timers/promises";
1315import { describe, expect, it } from "vitest";
14161517const HELPER_PATH = "scripts/lib/docker-build.sh";
@@ -293,6 +295,105 @@ output="$(docker_build_run e2e-build -t demo-image .)"
293295}
294296});
295297298+it("stops the tracked build command without retrying when interrupted", async () => {
299+const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-build-signal-"));
300+301+try {
302+const binDir = join(workDir, "bin");
303+mkdirSync(binDir);
304+writeFileSync(
305+join(binDir, "docker"),
306+`#!/bin/bash
307+set -euo pipefail
308+count=0
309+if [ -f "$TMPDIR/docker-count" ]; then
310+ count="$(<"$TMPDIR/docker-count")"
311+fi
312+count="$((count + 1))"
313+printf '%s\\n' "$count" >"$TMPDIR/docker-count"
314+printf '%s\\n' "$$" >"$TMPDIR/docker.pid"
315+printf 'rpc error: code = Unavailable\\n'
316+trap 'printf "term\\n" >"$TMPDIR/docker.term"; exit 0' TERM
317+while true; do
318+ /bin/sleep 1
319+done
320+`,
321+);
322+chmodSync(join(binDir, "docker"), 0o755);
323+const rootDir = process.cwd();
324+writeFileSync(
325+join(workDir, "runner.sh"),
326+`#!/bin/bash
327+set -euo pipefail
328+ROOT_DIR=${shellQuote(rootDir)}
329+TMPDIR=${shellQuote(workDir)}
330+export ROOT_DIR TMPDIR
331+export PATH="$TMPDIR/bin:$PATH"
332+export OPENCLAW_DOCKER_BUILD_RETRIES=3
333+source "$ROOT_DIR/scripts/lib/docker-build.sh"
334+docker_build_run e2e-build -t demo-image .
335+`,
336+);
337+chmodSync(join(workDir, "runner.sh"), 0o755);
338+339+const waitForFile = async (filePath: string) => {
340+for (let attempt = 0; attempt < 50; attempt += 1) {
341+if (existsSync(filePath)) {
342+return;
343+}
344+await delay(100);
345+}
346+throw new Error(`file was not written: ${filePath}`);
347+};
348+const waitForExit = async (child: ReturnType<typeof spawn>) =>
349+await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => {
350+child.once("exit", (code, signal) => resolve({ code, signal }));
351+});
352+const waitForDead = async (pid: number) => {
353+for (let attempt = 0; attempt < 50; attempt += 1) {
354+try {
355+process.kill(pid, 0);
356+} catch {
357+return;
358+}
359+await delay(100);
360+}
361+throw new Error(`process stayed alive: ${pid}`);
362+};
363+const runInterruptedBuild = async (signal: NodeJS.Signals, expectedCode: number) => {
364+rmSync(join(workDir, "docker.pid"), { force: true });
365+rmSync(join(workDir, "docker.term"), { force: true });
366+rmSync(join(workDir, "docker-count"), { force: true });
367+const runner = spawn(join(workDir, "runner.sh"), {
368+env: { ...process.env, TMPDIR: workDir },
369+stdio: "ignore",
370+});
371+try {
372+const pidPath = join(workDir, "docker.pid");
373+await waitForFile(pidPath);
374+const buildPid = Number.parseInt(readFileSync(pidPath, "utf8"), 10);
375+376+runner.kill(signal);
377+const exit = await waitForExit(runner);
378+379+expect(exit).toEqual({ code: expectedCode, signal: null });
380+await waitForFile(join(workDir, "docker.term"));
381+expect(readFileSync(join(workDir, "docker-count"), "utf8").trim()).toBe("1");
382+await waitForDead(buildPid);
383+} finally {
384+if (runner.exitCode === null && runner.signalCode === null) {
385+runner.kill("SIGKILL");
386+}
387+}
388+};
389+390+await runInterruptedBuild("SIGTERM", 143);
391+await runInterruptedBuild("SIGINT", 130);
392+} finally {
393+rmSync(workDir, { recursive: true, force: true });
394+}
395+});
396+296397it("does not delay fast successful centralized Docker builds until the next heartbeat", () => {
297398const workDir = mkdtempSync(join(tmpdir(), "openclaw-docker-build-fast-heartbeat-"));
298399此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。