mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 13:31:52 +00:00
Make the subprocess timeout opt-in so long-running setup steps (brew
install, service restart, sudo writes) keep their unbounded wait, and
apply a 15s SIGKILL-backed deadline only to the fast brew --prefix probe
so a signal-resistant hung shim cannot block dns setup.
Also fix the regression test on Linux CI: stub process.platform via
withMockedPlatform (the action does not consult os.platform()) and point
the mocked brew prefix at a real temp dir so un-mocked fs writes succeed
without sudo. Add focused coverage that non-probe subprocesses stay
unbounded.
(cherry picked from commit 469ae1db40)
Co-authored-by: thomas.szbay <thomas.szbay@example.com>
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
// Regression: the dns setup brew-prefix probe must be bounded by a SIGKILL-backed
|
|
// timeout so a hung binary cannot block `openclaw dns setup`, while long-running
|
|
// setup steps (install/restart/sudo writes) stay unbounded.
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const spawnSyncMock = vi.hoisted(() => vi.fn());
|
|
const testState = vi.hoisted(() => ({ zonePath: "" }));
|
|
|
|
vi.mock("node:child_process", async () => {
|
|
const { mockNodeChildProcessSpawnSync } = await import("openclaw/plugin-sdk/test-node-mocks");
|
|
return mockNodeChildProcessSpawnSync(spawnSyncMock, () =>
|
|
vi.importActual<typeof import("node:child_process")>("node:child_process"),
|
|
);
|
|
});
|
|
|
|
vi.mock("../infra/widearea-dns.js", async () => {
|
|
return {
|
|
getWideAreaZonePath: () => testState.zonePath,
|
|
normalizeWideAreaDomain: (d: string) => d,
|
|
resolveWideAreaDiscoveryDomain: () => "openclaw.internal.",
|
|
};
|
|
});
|
|
|
|
vi.mock("../infra/tailnet.js", async () => {
|
|
return {
|
|
pickPrimaryTailnetIPv4: () => "100.64.0.1",
|
|
pickPrimaryTailnetIPv6: () => undefined,
|
|
};
|
|
});
|
|
|
|
vi.mock("../config/config.js", async () => {
|
|
return { getRuntimeConfig: () => ({}) };
|
|
});
|
|
|
|
import { Command } from "commander";
|
|
import { withMockedPlatform } from "../test-utils/vitest-spies.js";
|
|
import { registerDnsCli } from "./dns-cli.js";
|
|
|
|
function spawnOk(stdout = "") {
|
|
return { stdout, stderr: "", pid: 1, output: [], status: 0, signal: null };
|
|
}
|
|
|
|
function isBrewPrefixProbe(call: unknown[]): boolean {
|
|
const args = call[1] as string[] | undefined;
|
|
return call[0] === "brew" && args?.[0] === "--prefix";
|
|
}
|
|
|
|
describe("dns-cli probe bounds", () => {
|
|
let brewPrefix: string;
|
|
|
|
beforeEach(() => {
|
|
// A real writable temp prefix lets the un-mocked fs writes (Corefile, conf.d,
|
|
// zone bootstrap) succeed on Linux CI without touching sudo paths.
|
|
brewPrefix = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-dns-cli-test-"));
|
|
testState.zonePath = path.join(brewPrefix, "test.zone");
|
|
spawnSyncMock.mockReset();
|
|
spawnSyncMock.mockImplementation((cmd: string, args: string[]) => {
|
|
if (cmd === "brew" && args?.[0] === "--prefix") {
|
|
return spawnOk(`${brewPrefix}\n`);
|
|
}
|
|
return spawnOk();
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(brewPrefix, { recursive: true, force: true });
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
async function runDnsSetupApply(): Promise<void> {
|
|
const program = new Command();
|
|
program.exitOverride();
|
|
registerDnsCli(program);
|
|
// The action gates on process.platform; os.platform() is not consulted.
|
|
await withMockedPlatform("darwin", () =>
|
|
program.parseAsync([
|
|
"node",
|
|
"openclaw",
|
|
"dns",
|
|
"setup",
|
|
"--domain",
|
|
"openclaw.internal",
|
|
"--apply",
|
|
]),
|
|
);
|
|
}
|
|
|
|
it("bounds the brew prefix probe with a SIGKILL-backed timeout", async () => {
|
|
await runDnsSetupApply();
|
|
|
|
const probeCall = spawnSyncMock.mock.calls.find(isBrewPrefixProbe);
|
|
expect(probeCall).toBeDefined();
|
|
expect(probeCall?.[2]).toMatchObject({ timeout: 15_000, killSignal: "SIGKILL" });
|
|
});
|
|
|
|
it("leaves long-running setup subprocesses unbounded", async () => {
|
|
await runDnsSetupApply();
|
|
|
|
const nonProbeCalls = spawnSyncMock.mock.calls.filter((call) => !isBrewPrefixProbe(call));
|
|
expect(nonProbeCalls.length).toBeGreaterThan(0);
|
|
for (const call of nonProbeCalls) {
|
|
expect(call[2]?.timeout).toBeUndefined();
|
|
}
|
|
});
|
|
});
|