fix: make full verification hermetic across local environments (#128777)

* test: harden full verification fixtures

* test: resolve main fixture overlap
This commit is contained in:
Peter Steinberger
2026-08-24 13:26:00 -07:00
committed by GitHub
parent 9a15d4cbf9
commit 2a39c50227
15 changed files with 272 additions and 282 deletions
@@ -1,52 +1,52 @@
// Brave tests cover brave web search provider.merge plugin behavior.
import { describe, expect, it, vi } from "vitest";
import { createBraveWebSearchProvider } from "./brave-web-search-provider.js";
const runtimeMock = vi.hoisted(() => {
const searchConfigs: Array<Record<string, unknown> | undefined> = [];
return {
searchConfigs,
executeBraveSearch: vi.fn(async (_args: unknown, searchConfig?: Record<string, unknown>) => {
searchConfigs.push(searchConfig);
return { results: [] };
}),
};
});
vi.mock("./brave-web-search-provider.runtime.js", () => ({
executeBraveSearch: runtimeMock.executeBraveSearch,
}));
describe("brave web search config merge", () => {
it("keeps plugin webSearch runtime-only after merging it for the tool", async () => {
const provider = createBraveWebSearchProvider();
const tool = provider.createTool({
config: {
plugins: {
entries: {
brave: {
config: {
webSearch: {
apiKey: "brave-test-key",
mode: "llm-context",
vi.resetModules();
const searchConfigs: Array<Record<string, unknown> | undefined> = [];
const executeBraveSearch = vi.fn(
async (_args: unknown, searchConfig?: Record<string, unknown>) => {
searchConfigs.push(searchConfig);
return { results: [] };
},
);
vi.doMock("./brave-web-search-provider.runtime.js", () => ({ executeBraveSearch }));
try {
const { createBraveWebSearchProvider } = await import("./brave-web-search-provider.js");
const provider = createBraveWebSearchProvider();
const tool = provider.createTool({
config: {
plugins: {
entries: {
brave: {
config: {
webSearch: {
apiKey: "brave-test-key",
mode: "llm-context",
},
},
},
},
},
},
},
searchConfig: { provider: "brave" },
});
searchConfig: { provider: "brave" },
});
await tool?.execute({ query: "OpenClaw docs" });
await tool?.execute({ query: "OpenClaw docs" });
const [searchConfig] = runtimeMock.searchConfigs;
expect(searchConfig?.brave).toEqual({
apiKey: "brave-test-key",
mode: "llm-context",
});
expect(searchConfig?.apiKey).toBe("brave-test-key");
expect(Object.keys(searchConfig ?? {})).toEqual(["provider", "apiKey"]);
expect(Object.getOwnPropertyDescriptor(searchConfig ?? {}, "brave")?.enumerable).toBe(false);
const [searchConfig] = searchConfigs;
expect(searchConfig?.brave).toEqual({
apiKey: "brave-test-key",
mode: "llm-context",
});
expect(searchConfig?.apiKey).toBe("brave-test-key");
expect(Object.keys(searchConfig ?? {})).toEqual(["provider", "apiKey"]);
expect(Object.getOwnPropertyDescriptor(searchConfig ?? {}, "brave")?.enumerable).toBe(false);
} finally {
vi.doUnmock("./brave-web-search-provider.runtime.js");
vi.resetModules();
}
});
});
+36 -92
View File
@@ -137,56 +137,6 @@ function createOpenRouterOAuthContext(params: {
return { ctx, progress, note, text, log, openUrl };
}
async function startLocalOpenRouterOAuthLogin() {
let markReady = () => {};
const ready = new Promise<void>((resolve) => {
markReady = resolve;
});
const controller = new AbortController();
const fetchImpl = vi.fn<typeof fetch>(async () => jsonResponse({ key: "sk-or-v1-test" }));
const { ctx } = createOpenRouterOAuthContext({
isRemote: false,
onProgress: (message) => {
if (message.startsWith("Waiting for OpenRouter OAuth callback")) {
markReady();
}
},
signal: controller.signal,
});
const login = loginOpenRouterOAuth(ctx, {
createPkce: () => ({ verifier: "verifier-1", challenge: "challenge-1" }),
createState: () => "state-1",
fetchImpl,
});
login.catch(() => undefined);
await Promise.race([
ready,
login.then(
() => {
throw new Error("OpenRouter OAuth completed before callback server started");
},
(error: unknown) => {
throw error;
},
),
]);
return {
abort: () => controller.abort(),
fetchImpl,
login,
request: async (pathOrQuery: string, init?: RequestInit) => {
const url = pathOrQuery.startsWith("http")
? pathOrQuery
: `${OPENROUTER_OAUTH_REDIRECT_URI}?${pathOrQuery}`;
const headers = new Headers(init?.headers);
headers.set("Connection", "close");
const response = await fetch(url, { ...init, headers });
return { response, body: await response.text() };
},
};
}
function runRemoteOpenRouterOAuthRedirect(redirectInput: string) {
const { ctx } = createOpenRouterOAuthContext({ isRemote: true, redirectInput });
return loginOpenRouterOAuth(ctx, {
@@ -415,7 +365,7 @@ describe("OpenRouter OAuth", () => {
expect(progress.stop).toHaveBeenCalledWith("OpenRouter OAuth complete");
});
it("binds the local callback before opening the browser", async () => {
it("binds the local callback before opening the browser and exchanges its accepted code", async () => {
const fetchImpl = vi.fn<typeof fetch>(async () => jsonResponse({ key: "sk-or-v1-test" }));
const waitForCallback = vi.fn(async () => ({
type: "authorization_code" as const,
@@ -446,6 +396,41 @@ describe("OpenRouter OAuth", () => {
expect(close).toHaveBeenCalledTimes(1);
expect(openUrl).toHaveBeenCalledWith(expect.stringContaining("https://openrouter.ai/auth?"));
expect(text).not.toHaveBeenCalled();
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(requestJsonBody(fetchImpl.mock.calls[0]?.[1])).toEqual({
code: "AUTHCODE",
code_verifier: "verifier-1",
code_challenge_method: "S256",
});
});
it("closes a state-bound provider denial without exchanging a code", async () => {
const fetchImpl = vi.fn<typeof fetch>(async () => jsonResponse({ key: "sk-or-v1-test" }));
const waitForCallback = vi.fn(async () => ({
type: "oauth_error" as const,
error: "access_denied",
errorDescription: "Denied",
}));
const close = vi.fn(async () => undefined);
const startCallback = vi.fn(async () => ({ waitForCallback, close }));
const { ctx, text } = createOpenRouterOAuthContext({ isRemote: false });
await expect(
loginOpenRouterOAuth(ctx, {
createPkce: () => ({ verifier: "verifier-1", challenge: "challenge-1" }),
createState: () => "state-1",
fetchImpl,
startCallback,
}),
).rejects.toThrow("OpenRouter OAuth error: access_denied: Denied");
expect(startCallback).toHaveBeenCalledWith(
expect.objectContaining({ expectedState: "state-1" }),
);
expect(waitForCallback).toHaveBeenCalledTimes(1);
expect(close).toHaveBeenCalledTimes(1);
expect(fetchImpl).not.toHaveBeenCalled();
expect(text).not.toHaveBeenCalled();
});
it("falls back to a pasted redirect when the local listener cannot start", async () => {
@@ -472,47 +457,6 @@ describe("OpenRouter OAuth", () => {
expect(fetchImpl).toHaveBeenCalledTimes(1);
});
it("keeps waiting after rejected callback candidates and exchanges one valid code", async () => {
const local = await startLocalOpenRouterOAuthLogin();
try {
expect((await local.request("http://localhost:3000/wrong")).response.status).toBe(404);
expect((await local.request("", { method: "POST" })).response.status).toBe(405);
expect((await local.request("code=missing-state")).response.status).toBe(400);
expect((await local.request("state=wrong&code=wrong-state")).response.status).toBe(400);
expect((await local.request("state=state-1")).response.status).toBe(400);
const validCallbacks = await Promise.allSettled([
local.request("state=state-1&code=AUTHCODE"),
local.request("state=state-1&code=REPLAY"),
]);
const statuses = validCallbacks.flatMap((result) =>
result.status === "fulfilled" ? [result.value.response.status] : [],
);
expect(statuses.filter((status) => status === 200)).toHaveLength(1);
await expect(local.login).resolves.toMatchObject({ defaultModel: "openrouter/auto" });
expect(local.fetchImpl).toHaveBeenCalledTimes(1);
} finally {
local.abort();
await local.login.catch(() => undefined);
}
});
it("terminates a state-bound provider denial without exchanging a code", async () => {
const local = await startLocalOpenRouterOAuthLogin();
try {
const denied = await local.request(
"state=state-1&error=access_denied&error_description=Denied",
);
expect(denied.response.status).toBe(400);
expect(denied.body).toBe("Authorization was not completed.");
await expect(local.login).rejects.toThrow("OpenRouter OAuth error: access_denied: Denied");
expect(local.fetchImpl).not.toHaveBeenCalled();
} finally {
local.abort();
await local.login.catch(() => undefined);
}
});
it("exposes stable auth choice metadata", () => {
expect(createOpenRouterOAuthAuthMethod().wizard?.choiceId).toBe("openrouter-oauth");
});
@@ -1004,6 +1004,10 @@ describe("createTelegramBot channel_post media", () => {
it("drops the media group when a non-recoverable media error occurs", async () => {
replySpy.mockReset();
setOpenChannelPostConfig();
saveRemoteMedia.mockResolvedValueOnce({
path: "/tmp/fatal-album-first.jpg",
contentType: "image/jpeg",
});
const runtimeError = vi.fn();
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
@@ -1031,6 +1035,10 @@ describe("createTelegramBot channel_post media", () => {
expect.stringContaining("media group handler failed"),
),
);
expect(runtimeError).toHaveBeenCalledWith(
expect.stringContaining("Telegram getFile returned no file_path"),
);
expect(saveRemoteMedia).toHaveBeenCalledTimes(1);
expect(replySpy).not.toHaveBeenCalled();
} finally {
setTimeoutSpy.mockRestore();
+20 -12
View File
@@ -3,7 +3,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { installedPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { hashConfigIncludeRaw } from "../config/includes.js";
import { recordPluginManifestInstallOwner } from "../plugins/manifest-install-owner.js";
@@ -12,6 +12,7 @@ import {
resolveOfficialExternalPluginId,
resolveOfficialExternalPluginInstall,
} from "../plugins/official-external-plugin-catalog.js";
import { withTempDir } from "../test-utils/temp-dir.js";
import {
applyExclusiveSlotSelectionMock,
buildPluginSnapshotReportMock,
@@ -1944,20 +1945,27 @@ describe("plugins cli install", () => {
it.each(OFFICIAL_EXTERNAL_NPM_INSTALLS_WITHOUT_INTEGRITY)(
"keeps official external npm installs trusted without integrity for $pluginId",
async ({ pluginId, npmSpec }) => {
primeSuccessfulPluginPersistence(pluginId);
findBundledPluginSourceMock.mockReturnValue(undefined);
installPluginFromNpmSpecMock.mockResolvedValue(createNpmPluginInstallResult(pluginId));
await withTempDir("openclaw-official-plugin-install-", async (cwd) => {
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(cwd);
try {
primeSuccessfulPluginPersistence(pluginId);
findBundledPluginSourceMock.mockReturnValue(undefined);
installPluginFromNpmSpecMock.mockResolvedValue(createNpmPluginInstallResult(pluginId));
await runPluginsCommand(["plugins", "install", pluginId]);
await runPluginsCommand(["plugins", "install", pluginId]);
expect(findBundledPluginSourceMock).toHaveBeenCalledWith({
lookup: { kind: "pluginId", value: pluginId },
expect(findBundledPluginSourceMock).toHaveBeenCalledWith({
lookup: { kind: "pluginId", value: pluginId },
});
expect(installPluginFromClawHubMock).not.toHaveBeenCalled();
expect(npmInstallCall().spec).toBe(npmSpec);
expect(npmInstallCall().expectedPluginId).toBe(pluginId);
expect(npmInstallCall().trustedSourceLinkedOfficialInstall).toBe(true);
expect(npmInstallCall().expectedIntegrity).toBeUndefined();
} finally {
cwdSpy.mockRestore();
}
});
expect(installPluginFromClawHubMock).not.toHaveBeenCalled();
expect(npmInstallCall().spec).toBe(npmSpec);
expect(npmInstallCall().expectedPluginId).toBe(pluginId);
expect(npmInstallCall().trustedSourceLinkedOfficialInstall).toBe(true);
expect(npmInstallCall().expectedIntegrity).toBeUndefined();
},
);
+31 -69
View File
@@ -13,7 +13,6 @@ import { pathExists } from "../utils.js";
import { resolveStableNodePath } from "./stable-node-path.js";
import type { UpdateChannel } from "./update-channels.js";
import type { DevUpdateTarget } from "./update-dev-target.js";
import { buildUpdateCommandRunner } from "./update-runner-command.js";
import {
resolveUpdateDoctorExecutionPolicy,
resolveUpdateInstallSurface,
@@ -53,28 +52,6 @@ function createRunner(responses: Record<string, CommandResponse>) {
return { runner, calls };
}
function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
async function waitForProcessExit(pid: number, timeoutMs = 2_000): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!isProcessAlive(pid)) {
return true;
}
await new Promise<void>((resolve) => {
setTimeout(resolve, 5);
});
}
return !isProcessAlive(pid);
}
describe("resolveUpdateDoctorExecutionPolicy", () => {
it("keeps fix mode when service repair is authorized", () => {
expect(
@@ -197,53 +174,38 @@ describe("runGatewayUpdate", () => {
await fs.writeFile(path.join(tempDir, "package.json"), JSON.stringify(pkg), "utf-8");
}
it.runIf(process.platform !== "win32")(
"kills nested updater subprocesses when a default command times out",
{ timeout: 10_000 },
async () => {
await setupGitCheckout();
const fakeBinDir = path.join(tempDir, "fake-bin");
const fakeCommandName = "openclaw-update-timeout-fixture.cjs";
const fakeCommandPath = path.join(fakeBinDir, fakeCommandName);
const childPidPath = path.join(tempDir, "nested-child.pid");
await fs.mkdir(fakeBinDir, { recursive: true });
await fs.writeFile(
fakeCommandPath,
"#!/usr/bin/env node\n" +
`const { spawn } = require("node:child_process");\n` +
`const fs = require("node:fs");\n` +
`const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" });\n` +
`fs.writeFileSync(process.env.OPENCLAW_UPDATE_TEST_CHILD_PID_PATH, String(child.pid));\n` +
`setInterval(() => {}, 1000);\n`,
"utf-8",
);
await fs.chmod(fakeCommandPath, 0o755);
let childPid: number | null = null;
let childExited = false;
try {
const { runCommand } = await buildUpdateCommandRunner();
const result = await runCommand([process.execPath, fakeCommandPath], {
timeoutMs: 500,
env: {
...process.env,
NODE_OPTIONS: "",
OPENCLAW_UPDATE_TEST_CHILD_PID_PATH: childPidPath,
},
});
expect(result.termination).toBe("timeout");
expect(await pathExists(childPidPath)).toBe(true);
childPid = Number.parseInt(await fs.readFile(childPidPath, "utf-8"), 10);
expect(Number.isInteger(childPid) && childPid > 0).toBe(true);
childExited = await waitForProcessExit(childPid);
} finally {
if (childPid && isProcessAlive(childPid)) {
process.kill(childPid, "SIGKILL");
}
}
it("owns default updater subprocess trees", async () => {
const runCommandWithTimeoutMock = vi.fn(async () => ({
stdout: "",
stderr: "",
code: 0,
killed: false,
signal: null,
}));
vi.resetModules();
vi.doMock("../process/exec.js", () => ({ runCommandWithTimeout: runCommandWithTimeoutMock }));
vi.doMock("./update-global.js", () => ({
createGlobalInstallEnv: async () => ({ OPENCLAW_UPDATE_TEST_ENV: "1" }),
}));
expect(childExited).toBe(true);
},
);
try {
const { buildUpdateCommandRunner } = await import("./update-runner-command.js");
const { runCommand } = await buildUpdateCommandRunner();
await runCommand(["pnpm", "install"], { cwd: tempDir, timeoutMs: 500 });
expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(["pnpm", "install"], {
cwd: tempDir,
env: { OPENCLAW_UPDATE_TEST_ENV: "1" },
killProcessTree: true,
timeoutMs: 500,
});
} finally {
vi.doUnmock("../process/exec.js");
vi.doUnmock("./update-global.js");
vi.resetModules();
}
});
it.runIf(process.platform !== "win32")(
"classifies a prepared pnpm 11 project by its canonical package root",
@@ -60,8 +60,8 @@ const OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDING_COUNTS = new Map<strin
["@openclaw/acpx:dangerous-exec:dist/mcp-proxy.mjs", 1],
["@openclaw/acpx:dangerous-exec:dist/service-<hash>.js", 1],
["@openclaw/codex:dangerous-exec:dist/api.js", 1],
["@openclaw/codex:dangerous-exec:dist/dynamic-tools-<hash>.js", 2],
["@openclaw/codex:dangerous-exec:dist/session-catalog-<hash>.js", 1],
["@openclaw/codex:dangerous-exec:dist/dynamic-tools-<hash>.js", 1],
["@openclaw/codex:dangerous-exec:dist/shared-client-<hash>.js", 1],
["@openclaw/codex:dangerous-exec:dist/transport-stdio-<hash>.js", 1],
["@openclaw/llama-cpp-provider:dangerous-exec:dist/index.js", 1],
["@openclaw/slack:dynamic-code-execution:dist/outbound-payload.test-harness-<hash>.js", 1],
@@ -496,7 +496,7 @@ describe("publishable plugin npm package install security scan", () => {
"@openclaw/codex",
"dist/dynamic-tools-current.js",
),
).toEqual([dynamicToolsKey, dynamicToolsKey]);
).toEqual([dynamicToolsKey]);
expect(
expectedOptionalReviewedFindingsForPackedPath(
"@openclaw/codex",
@@ -508,13 +508,13 @@ describe("publishable plugin npm package install security scan", () => {
"@openclaw/codex",
"dist/session-catalog-current.js",
),
).toEqual(["@openclaw/codex:dangerous-exec:dist/session-catalog-<hash>.js"]);
).toEqual([]);
expect(
expectedOptionalReviewedFindingsForPackedPath(
"@openclaw/codex",
"dist/shared-client-current.js",
),
).toEqual([]);
).toEqual(["@openclaw/codex:dangerous-exec:dist/shared-client-<hash>.js"]);
expect(
expectedOptionalReviewedFindingsForPackedPath("@openclaw/codex", "dist/client-retired.js"),
).toEqual([]);
+22 -8
View File
@@ -7,7 +7,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
killPidIfAlive,
readPidFile,
waitForPidFile,
waitForPidToExit,
writeForkingNoOutputScript,
} from "../test-utils/process-tree.js";
@@ -438,6 +438,17 @@ describe("secret ref resolver", () => {
const pidPath = path.join(root, "forked.pid");
let childPid: number | undefined;
let resultPromise: Promise<string> | undefined;
const nativeSetTimeout = globalThis.setTimeout;
const noOutputTimeouts: Array<() => void> = [];
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback, delay, ...args) => {
if (delay === 1_000) {
noOutputTimeouts.push(() => callback(...args));
return nativeSetTimeout(() => undefined, 60_000);
}
return nativeSetTimeout(callback, delay, ...args);
});
try {
resultPromise = resolveExecSecret(scriptPath, {
@@ -446,14 +457,16 @@ describe("secret ref resolver", () => {
timeoutMs: 10_000,
});
const resultErrorPromise = resultPromise.catch((error: unknown) => error);
await vi.waitFor(async () => {
childPid = await readPidFile(pidPath);
expect(childPid).toBeGreaterThan(0);
});
if (childPid === undefined) {
throw new Error("forked exec provider pid was not recorded");
}
childPid = await waitForPidFile(pidPath);
await vi.waitFor(
() => {
expect(noOutputTimeouts.length).toBeGreaterThanOrEqual(2);
},
{ timeout: 5_000 },
);
noOutputTimeouts.at(-1)?.();
const error = await resultErrorPromise;
expect(isProviderScopedSecretResolutionError(error)).toBe(true);
if (!isProviderScopedSecretResolutionError(error)) {
throw new Error("expected a provider-scoped no-output error");
@@ -466,6 +479,7 @@ describe("secret ref resolver", () => {
});
expect(await waitForPidToExit(childPid, 5_000)).toBe(true);
} finally {
setTimeoutSpy.mockRestore();
killPidIfAlive(childPid);
await resultPromise?.catch(() => {});
}
+3 -4
View File
@@ -6,7 +6,7 @@ import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
killPidIfAlive,
readPidFile,
waitForPidFile,
waitForPidToExit,
writeForkingNoOutputScript,
} from "../test-utils/process-tree.js";
@@ -232,8 +232,6 @@ describe("runInstallPolicy", () => {
command: forkScriptPath,
env: { NODE_BINARY: process.execPath, PID_FILE: pidPath },
trustedDirs: [path.dirname(forkScriptPath)],
// Preserve production-like startup headroom; the test fires
// the re-armed timer only after the readiness byte arrives.
noOutputTimeoutMs: 1_000,
timeoutMs: 10_000,
},
@@ -242,13 +240,14 @@ describe("runInstallPolicy", () => {
},
request: baseRequest(sourceDir),
});
void resultPromise.catch(() => undefined);
childPid = await waitForPidFile(pidPath);
await vi.waitFor(
() => {
expect(noOutputTimeouts.length).toBeGreaterThanOrEqual(2);
},
{ timeout: 5_000 },
);
childPid = await readPidFile(pidPath);
noOutputTimeouts.at(-1)?.();
const result = await resultPromise;
+20
View File
@@ -40,6 +40,26 @@ export async function readPidFile(pidPath: string): Promise<number> {
return Number((await fs.readFile(pidPath, "utf8")).trim());
}
export async function waitForPidFile(pidPath: string, timeoutMs = 5_000): Promise<number> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const pid = await readPidFile(pidPath);
if (Number.isInteger(pid) && pid > 0) {
return pid;
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
throw error;
}
}
await new Promise<void>((resolve) => {
setTimeout(resolve, 25);
});
}
throw new Error(`Timed out waiting for pid file: ${pidPath}`);
}
export function killPidIfAlive(pid: number | undefined): void {
if (pid === undefined || !isPidAlive(pid)) {
return;
+7
View File
@@ -23,6 +23,11 @@ import {
import { useAutoCleanupTempDirTracker } from "./helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const rootPackageManager = (
JSON.parse(readFileSync("package.json", "utf8")) as {
packageManager: string;
}
).packageManager;
const standaloneBundledChannelSmokeFiles = [
"scripts/test-built-bundled-channel-entry-smoke.mts",
@@ -108,6 +113,7 @@ describe("collectSourcePackWorkspaceDependencyErrors", () => {
const rootPackageJson = {
dependencies: { "@openclaw/ai": "workspace:*" },
name: "openclaw-source-pack-regression",
packageManager: rootPackageManager,
version,
};
mkdirSync(aiDir, { recursive: true });
@@ -213,6 +219,7 @@ describe("collectSourcePackWorkspaceDependencyErrors", () => {
const originalPackageJson = `${JSON.stringify(
{
name: "openclaw-direct-pack-manifest",
packageManager: rootPackageManager,
version: "2099.1.2-test.0",
scripts: {
prepack: "node scripts/package-manifest.mjs prepare",
+12 -1
View File
@@ -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,
}),
);
+37 -39
View File
@@ -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");
}
}
},
);
+19 -11
View File
@@ -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);
},
);
+2 -2
View File
@@ -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");