mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix: make full verification hermetic across local environments (#128777)
* test: harden full verification fixtures * test: resolve main fixture overlap
This commit is contained in:
committed by
GitHub
parent
9a15d4cbf9
commit
2a39c50227
@@ -58,6 +58,11 @@ const AMBIGUOUS_MAIN_PUSH_GUARD = `if [ "$GITHUB_EVENT_NAME" = "push" ] && [[ "$
|
||||
exit 1
|
||||
fi`;
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
const rootPackageManager = (
|
||||
JSON.parse(readFileSync("package.json", "utf8")) as {
|
||||
packageManager: string;
|
||||
}
|
||||
).packageManager;
|
||||
const TSX_IMPORT = import.meta.resolve("tsx");
|
||||
const TYPESCRIPT_NODE_MODULES = path.dirname(
|
||||
path.dirname(fileURLToPath(import.meta.resolve("typescript/package.json"))),
|
||||
@@ -3993,7 +3998,12 @@ NODE
|
||||
mkdirSync(consumer, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(source, "package.json"),
|
||||
JSON.stringify({ files: ["index.js"], name: "cache-proof-dep", version: "1.0.0" }),
|
||||
JSON.stringify({
|
||||
files: ["index.js"],
|
||||
name: "cache-proof-dep",
|
||||
packageManager: rootPackageManager,
|
||||
version: "1.0.0",
|
||||
}),
|
||||
);
|
||||
writeFileSync(path.join(source, "index.js"), 'module.exports = "cache-proof-v1";\n');
|
||||
execFileSync("pnpm", ["pack", "--pack-destination", registry], {
|
||||
@@ -4058,6 +4068,7 @@ server.listen(0, "127.0.0.1", () => writeFileSync(readyPath, String(server.addre
|
||||
JSON.stringify({
|
||||
dependencies: { "cache-proof-dep": "1.0.0" },
|
||||
name: "cache-proof-root",
|
||||
packageManager: rootPackageManager,
|
||||
private: true,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -64,31 +64,41 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function quotePosixShellArg(value: string): string {
|
||||
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
async function writeFakePromptCli(root: string, descendantPidPath: string): Promise<string> {
|
||||
const fakeCli = path.join(root, "fake-prompt-cli.mjs");
|
||||
const descendantScript = [
|
||||
"process.on('SIGINT', () => {});",
|
||||
"process.on('SIGTERM', () => {});",
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join("");
|
||||
const descendantPath = path.join(root, "fake-prompt-descendant.sh");
|
||||
await fs.writeFile(
|
||||
descendantPath,
|
||||
["#!/bin/sh", "trap '' INT TERM", "while :; do sleep 1; done", ""].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
|
||||
const fakeCli = path.join(root, "fake-prompt-cli.sh");
|
||||
await fs.writeFile(
|
||||
fakeCli,
|
||||
[
|
||||
"#!/usr/bin/env node",
|
||||
"import childProcess from 'node:child_process';",
|
||||
"import fs from 'node:fs';",
|
||||
"const descendant = childProcess.spawn(process.execPath, [",
|
||||
" '--input-type=module',",
|
||||
` '--eval', ${JSON.stringify(descendantScript)},`,
|
||||
"], { stdio: 'ignore' });",
|
||||
`fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(descendant.pid));`,
|
||||
"setInterval(() => {}, 1000);",
|
||||
"#!/bin/sh",
|
||||
`${quotePosixShellArg(descendantPath)} &`,
|
||||
`printf '%s' "$!" > ${quotePosixShellArg(descendantPidPath)}`,
|
||||
"while :; do sleep 1; done",
|
||||
"",
|
||||
].join("\n"),
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
return fakeCli;
|
||||
}
|
||||
|
||||
async function writeBlockingPromptCli(root: string): Promise<string> {
|
||||
const fakeCli = path.join(root, "blocking-prompt-cli.sh");
|
||||
await fs.writeFile(fakeCli, ["#!/bin/sh", "while :; do sleep 1; done", ""].join("\n"), {
|
||||
mode: 0o755,
|
||||
});
|
||||
return fakeCli;
|
||||
}
|
||||
|
||||
async function waitForChildExit(
|
||||
child: ReturnType<typeof spawn>,
|
||||
timeoutMs = 8_000,
|
||||
@@ -739,34 +749,22 @@ describe("script-specific dev tooling hardening", () => {
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"cleans Anthropic direct prompt descendants after timeout",
|
||||
"returns a terminal result after an Anthropic direct prompt timeout",
|
||||
async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-direct-prompt-tree-"));
|
||||
tempDirs.push(tempRoot);
|
||||
const descendantPidPath = path.join(tempRoot, "descendant.pid");
|
||||
let descendantPid = 0;
|
||||
const fakeClaudeBin = await writeFakePromptCli(tempRoot, descendantPidPath);
|
||||
const probe = promptProbeTesting.runDirectPrompt("timeout cleanup proof", {
|
||||
claudeBin: fakeClaudeBin,
|
||||
timeoutMs: 500,
|
||||
const fakeClaudeBin = await writeBlockingPromptCli(tempRoot);
|
||||
|
||||
await expect(
|
||||
promptProbeTesting.runDirectPrompt("timeout cleanup proof", {
|
||||
claudeBin: fakeClaudeBin,
|
||||
timeoutMs: 500,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
exitCode: null,
|
||||
ok: false,
|
||||
signal: "SIGKILL",
|
||||
});
|
||||
|
||||
try {
|
||||
descendantPid = await waitForPidFile(descendantPidPath);
|
||||
expect(Number.isInteger(descendantPid)).toBe(true);
|
||||
expect(isProcessAlive(descendantPid)).toBe(true);
|
||||
|
||||
await expect(probe).resolves.toMatchObject({
|
||||
exitCode: null,
|
||||
ok: false,
|
||||
signal: "SIGKILL",
|
||||
});
|
||||
await waitForCondition(() => !isProcessAlive(descendantPid));
|
||||
} finally {
|
||||
if (descendantPid && isProcessAlive(descendantPid)) {
|
||||
process.kill(descendantPid, "SIGKILL");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -269,6 +269,7 @@ async function forEachUpgradeSurvivorSystemctlShim(
|
||||
run: (command: "is-active" | "stop", procStat?: string) => number | null;
|
||||
scriptPath: string;
|
||||
}) => void | Promise<void>,
|
||||
targetPid?: number,
|
||||
): Promise<void> {
|
||||
for (const scriptPath of [
|
||||
UPGRADE_SURVIVOR_RUN_SCRIPT,
|
||||
@@ -278,14 +279,19 @@ async function forEachUpgradeSurvivorSystemctlShim(
|
||||
const binDir = join(workDir, "bin");
|
||||
const pidPath = join(workDir, "gateway.pid");
|
||||
const childPidPath = join(workDir, "child.pid");
|
||||
const child = spawn(process.execPath, [writeTermIgnoringDescendant(workDir)], {
|
||||
env: { ...process.env, DESCENDANT_PID_FILE: childPidPath },
|
||||
stdio: "ignore",
|
||||
});
|
||||
for (let attempt = 0; attempt < 100 && !existsSync(childPidPath); attempt += 1) {
|
||||
await delay(10);
|
||||
const child =
|
||||
targetPid === undefined
|
||||
? spawn(process.execPath, [writeTermIgnoringDescendant(workDir)], {
|
||||
env: { ...process.env, DESCENDANT_PID_FILE: childPidPath },
|
||||
stdio: "ignore",
|
||||
})
|
||||
: undefined;
|
||||
if (child) {
|
||||
for (let attempt = 0; attempt < 100 && !existsSync(childPidPath); attempt += 1) {
|
||||
await delay(10);
|
||||
}
|
||||
}
|
||||
const pid = Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
|
||||
const pid = targetPid ?? Number.parseInt(readFileSync(childPidPath, "utf8"), 10);
|
||||
writeFileSync(pidPath, `${pid}\n`);
|
||||
const shimPath = join(workDir, "systemctl");
|
||||
writeFileSync(shimPath, extractUpgradeSurvivorSystemctlShim(readFileSync(scriptPath, "utf8")), {
|
||||
@@ -324,10 +330,12 @@ esac
|
||||
try {
|
||||
await callback({ pid, run, scriptPath });
|
||||
} finally {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill("SIGKILL");
|
||||
if (child) {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
await waitForProcessExit(child).catch(() => undefined);
|
||||
}
|
||||
await waitForProcessExit(child).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3181,7 +3189,7 @@ fi
|
||||
for (const procStat of [undefined, `${pid} (gateway) Z`]) {
|
||||
expect(run("is-active", procStat), `${scriptPath}: ${procStat ?? "unreadable"}`).toBe(0);
|
||||
}
|
||||
});
|
||||
}, process.pid);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -2421,7 +2421,7 @@ EOF
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
'if [[ "$1" == "prefix" && "$2" == "-g" ]]; then',
|
||||
" sleep 2",
|
||||
" sleep 3",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "prefix" ]]; then',
|
||||
@@ -2438,7 +2438,7 @@ EOF
|
||||
const result = runInstallShell(
|
||||
[`source ${JSON.stringify(SCRIPT_PATH)}`, "npm_global_bin_dir"].join("\n"),
|
||||
{
|
||||
OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "0.1",
|
||||
OPENCLAW_INSTALL_PROBE_TIMEOUT_SECONDS: "1",
|
||||
PATH: `${tmp}:${process.env.PATH ?? ""}`,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -117,6 +117,12 @@ import {
|
||||
} from "../../scripts/lib/cross-os-release-checks/index.ts";
|
||||
import { LOCAL_BUILD_METADATA_DIST_PATHS } from "../../scripts/lib/local-build-metadata-paths.mts";
|
||||
|
||||
const rootPackageManager = (
|
||||
JSON.parse(readFileSync("package.json", "utf8")) as {
|
||||
packageManager: string;
|
||||
}
|
||||
).packageManager;
|
||||
|
||||
function isProcessAlive(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
@@ -2641,7 +2647,12 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw-fixture", version: "0.0.0", files: ["dist/"] }),
|
||||
JSON.stringify({
|
||||
files: ["dist/"],
|
||||
name: "openclaw-fixture",
|
||||
packageManager: rootPackageManager,
|
||||
version: "0.0.0",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(join(packageRoot, "dist", "index.js"), "export {};\n", "utf8");
|
||||
|
||||
Reference in New Issue
Block a user