mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -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
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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