mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-28 19:01:47 +03:00
perf(test): replace module resets with direct spies and runtime seams
This commit is contained in:
@@ -1,49 +1,21 @@
|
|||||||
import { EventEmitter } from "node:events";
|
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Readable } from "node:stream";
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { resolveSandboxContext } from "./sandbox.js";
|
||||||
|
|
||||||
type SpawnCall = {
|
vi.mock("./sandbox/docker.js", () => ({
|
||||||
command: string;
|
ensureSandboxContainer: vi.fn(async () => "openclaw-sbx-test"),
|
||||||
args: string[];
|
}));
|
||||||
};
|
|
||||||
|
|
||||||
const spawnCalls: SpawnCall[] = [];
|
vi.mock("./sandbox/browser.js", () => ({
|
||||||
|
ensureSandboxBrowser: vi.fn(async () => null),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("node:child_process", async (importOriginal) => {
|
vi.mock("./sandbox/prune.js", () => ({
|
||||||
const actual = await importOriginal<typeof import("node:child_process")>();
|
maybePruneSandboxes: vi.fn(async () => undefined),
|
||||||
return {
|
}));
|
||||||
...actual,
|
|
||||||
spawn: (command: string, args: string[]) => {
|
|
||||||
spawnCalls.push({ command, args });
|
|
||||||
const child = new EventEmitter() as {
|
|
||||||
stdout?: Readable;
|
|
||||||
stderr?: Readable;
|
|
||||||
on: (event: string, cb: (...args: unknown[]) => void) => void;
|
|
||||||
};
|
|
||||||
child.stdout = new Readable({ read() {} });
|
|
||||||
child.stderr = new Readable({ read() {} });
|
|
||||||
|
|
||||||
const dockerArgs = command === "docker" ? args : [];
|
|
||||||
const shouldFailContainerInspect =
|
|
||||||
dockerArgs[0] === "inspect" &&
|
|
||||||
dockerArgs[1] === "-f" &&
|
|
||||||
dockerArgs[2] === "{{.State.Running}}";
|
|
||||||
const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect";
|
|
||||||
|
|
||||||
const code = shouldFailContainerInspect ? 1 : 0;
|
|
||||||
if (shouldSucceedImageInspect) {
|
|
||||||
queueMicrotask(() => child.emit("close", 0));
|
|
||||||
} else {
|
|
||||||
queueMicrotask(() => child.emit("close", code));
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
async function writeSkill(params: { dir: string; name: string; description: string }) {
|
async function writeSkill(params: { dir: string; name: string; description: string }) {
|
||||||
const { dir, name, description } = params;
|
const { dir, name, description } = params;
|
||||||
@@ -74,25 +46,18 @@ describe("sandbox skill mirroring", () => {
|
|||||||
let envSnapshot: Record<string, string | undefined>;
|
let envSnapshot: Record<string, string | undefined>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spawnCalls.length = 0;
|
|
||||||
envSnapshot = { ...process.env };
|
envSnapshot = { ...process.env };
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
restoreEnv(envSnapshot);
|
restoreEnv(envSnapshot);
|
||||||
vi.resetModules();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const runContext = async (workspaceAccess: "none" | "ro") => {
|
const runContext = async (workspaceAccess: "none" | "ro") => {
|
||||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-state-"));
|
const bundledDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-bundled-skills-"));
|
||||||
const bundledDir = path.join(stateDir, "bundled-skills");
|
|
||||||
await fs.mkdir(bundledDir, { recursive: true });
|
await fs.mkdir(bundledDir, { recursive: true });
|
||||||
|
|
||||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
|
||||||
process.env.OPENCLAW_BUNDLED_SKILLS_DIR = bundledDir;
|
process.env.OPENCLAW_BUNDLED_SKILLS_DIR = bundledDir;
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const { resolveSandboxContext } = await import("./sandbox.js");
|
|
||||||
|
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
|
||||||
await writeSkill({
|
await writeSkill({
|
||||||
@@ -108,7 +73,7 @@ describe("sandbox skill mirroring", () => {
|
|||||||
mode: "all",
|
mode: "all",
|
||||||
scope: "session",
|
scope: "session",
|
||||||
workspaceAccess,
|
workspaceAccess,
|
||||||
workspaceRoot: path.join(stateDir, "sandboxes"),
|
workspaceRoot: path.join(bundledDir, "sandboxes"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
vi.resetModules();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("DEFAULT_AGENT_WORKSPACE_DIR", () => {
|
describe("DEFAULT_AGENT_WORKSPACE_DIR", () => {
|
||||||
it("uses OPENCLAW_HOME at module import time", async () => {
|
it("uses OPENCLAW_HOME when resolving the default workspace dir", () => {
|
||||||
const home = path.join(path.sep, "srv", "openclaw-home");
|
const home = path.join(path.sep, "srv", "openclaw-home");
|
||||||
vi.stubEnv("OPENCLAW_HOME", home);
|
vi.stubEnv("OPENCLAW_HOME", home);
|
||||||
vi.stubEnv("HOME", path.join(path.sep, "home", "other"));
|
vi.stubEnv("HOME", path.join(path.sep, "home", "other"));
|
||||||
vi.resetModules();
|
|
||||||
|
|
||||||
const mod = await import("./workspace.js");
|
expect(resolveDefaultAgentWorkspaceDir()).toBe(
|
||||||
expect(mod.DEFAULT_AGENT_WORKSPACE_DIR).toBe(
|
|
||||||
path.join(path.resolve(home), ".openclaw", "workspace"),
|
path.join(path.resolve(home), ".openclaw", "workspace"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ async function withEnvOverride<T>(
|
|||||||
process.env[key] = overrides[key];
|
process.env[key] = overrides[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vi.resetModules();
|
|
||||||
try {
|
try {
|
||||||
return await fn();
|
return await fn();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -49,7 +48,6 @@ async function withEnvOverride<T>(
|
|||||||
process.env[key] = saved[key];
|
process.env[key] = saved[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vi.resetModules();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,8 +120,6 @@ describe("legacy config detection", () => {
|
|||||||
expect(res.config?.routing).toBeUndefined();
|
expect(res.config?.routing).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("migrates audio.transcription with custom script names", async () => {
|
it("migrates audio.transcription with custom script names", async () => {
|
||||||
vi.resetModules();
|
|
||||||
const { migrateLegacyConfig } = await import("./config.js");
|
|
||||||
const res = migrateLegacyConfig({
|
const res = migrateLegacyConfig({
|
||||||
audio: {
|
audio: {
|
||||||
transcription: {
|
transcription: {
|
||||||
@@ -144,8 +142,6 @@ describe("legacy config detection", () => {
|
|||||||
expect(res.config?.audio).toBeUndefined();
|
expect(res.config?.audio).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("rejects audio.transcription when command contains non-string parts", async () => {
|
it("rejects audio.transcription when command contains non-string parts", async () => {
|
||||||
vi.resetModules();
|
|
||||||
const { migrateLegacyConfig } = await import("./config.js");
|
|
||||||
const res = migrateLegacyConfig({
|
const res = migrateLegacyConfig({
|
||||||
audio: {
|
audio: {
|
||||||
transcription: {
|
transcription: {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
resolveDefaultConfigCandidates,
|
resolveDefaultConfigCandidates,
|
||||||
|
resolveConfigPathCandidate,
|
||||||
resolveConfigPath,
|
resolveConfigPath,
|
||||||
resolveOAuthDir,
|
resolveOAuthDir,
|
||||||
resolveOAuthPath,
|
resolveOAuthPath,
|
||||||
@@ -108,74 +109,16 @@ describe("state + config path candidates", () => {
|
|||||||
|
|
||||||
it("CONFIG_PATH prefers existing config when present", async () => {
|
it("CONFIG_PATH prefers existing config when present", async () => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-"));
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-"));
|
||||||
const previousHome = process.env.HOME;
|
|
||||||
const previousUserProfile = process.env.USERPROFILE;
|
|
||||||
const previousHomeDrive = process.env.HOMEDRIVE;
|
|
||||||
const previousHomePath = process.env.HOMEPATH;
|
|
||||||
const previousOpenClawConfig = process.env.OPENCLAW_CONFIG_PATH;
|
|
||||||
const previousOpenClawState = process.env.OPENCLAW_STATE_DIR;
|
|
||||||
try {
|
try {
|
||||||
const legacyDir = path.join(root, ".openclaw");
|
const legacyDir = path.join(root, ".openclaw");
|
||||||
await fs.mkdir(legacyDir, { recursive: true });
|
await fs.mkdir(legacyDir, { recursive: true });
|
||||||
const legacyPath = path.join(legacyDir, "openclaw.json");
|
const legacyPath = path.join(legacyDir, "openclaw.json");
|
||||||
await fs.writeFile(legacyPath, "{}", "utf-8");
|
await fs.writeFile(legacyPath, "{}", "utf-8");
|
||||||
|
|
||||||
process.env.HOME = root;
|
const resolved = resolveConfigPathCandidate({} as NodeJS.ProcessEnv, () => root);
|
||||||
if (process.platform === "win32") {
|
expect(resolved).toBe(legacyPath);
|
||||||
process.env.USERPROFILE = root;
|
|
||||||
const parsed = path.win32.parse(root);
|
|
||||||
process.env.HOMEDRIVE = parsed.root.replace(/\\$/, "");
|
|
||||||
process.env.HOMEPATH = root.slice(parsed.root.length - 1);
|
|
||||||
}
|
|
||||||
delete process.env.OPENCLAW_CONFIG_PATH;
|
|
||||||
delete process.env.OPENCLAW_STATE_DIR;
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { CONFIG_PATH } = await import("./paths.js");
|
|
||||||
expect(CONFIG_PATH).toBe(legacyPath);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (previousHome === undefined) {
|
|
||||||
delete process.env.HOME;
|
|
||||||
} else {
|
|
||||||
process.env.HOME = previousHome;
|
|
||||||
}
|
|
||||||
if (previousUserProfile === undefined) {
|
|
||||||
delete process.env.USERPROFILE;
|
|
||||||
} else {
|
|
||||||
process.env.USERPROFILE = previousUserProfile;
|
|
||||||
}
|
|
||||||
if (previousHomeDrive === undefined) {
|
|
||||||
delete process.env.HOMEDRIVE;
|
|
||||||
} else {
|
|
||||||
process.env.HOMEDRIVE = previousHomeDrive;
|
|
||||||
}
|
|
||||||
if (previousHomePath === undefined) {
|
|
||||||
delete process.env.HOMEPATH;
|
|
||||||
} else {
|
|
||||||
process.env.HOMEPATH = previousHomePath;
|
|
||||||
}
|
|
||||||
if (previousOpenClawConfig === undefined) {
|
|
||||||
delete process.env.OPENCLAW_CONFIG_PATH;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
|
|
||||||
}
|
|
||||||
if (previousOpenClawConfig === undefined) {
|
|
||||||
delete process.env.OPENCLAW_CONFIG_PATH;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
|
|
||||||
}
|
|
||||||
if (previousOpenClawState === undefined) {
|
|
||||||
delete process.env.OPENCLAW_STATE_DIR;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
|
|
||||||
}
|
|
||||||
if (previousOpenClawState === undefined) {
|
|
||||||
delete process.env.OPENCLAW_STATE_DIR;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
|
|
||||||
}
|
|
||||||
await fs.rm(root, { recursive: true, force: true });
|
await fs.rm(root, { recursive: true, force: true });
|
||||||
vi.resetModules();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { installHooksFromPath } from "./install.js";
|
||||||
|
import {
|
||||||
|
clearInternalHooks,
|
||||||
|
createInternalHookEvent,
|
||||||
|
triggerInternalHook,
|
||||||
|
} from "./internal-hooks.js";
|
||||||
|
import { loadInternalHooks } from "./loader.js";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
@@ -12,36 +19,15 @@ async function makeTempDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("hooks install (e2e)", () => {
|
describe("hooks install (e2e)", () => {
|
||||||
let prevStateDir: string | undefined;
|
|
||||||
let prevBundledDir: string | undefined;
|
|
||||||
let workspaceDir: string;
|
let workspaceDir: string;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const baseDir = await makeTempDir();
|
const baseDir = await makeTempDir();
|
||||||
workspaceDir = path.join(baseDir, "workspace");
|
workspaceDir = path.join(baseDir, "workspace");
|
||||||
await fs.mkdir(workspaceDir, { recursive: true });
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
|
|
||||||
prevStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
||||||
prevBundledDir = process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
|
|
||||||
process.env.OPENCLAW_STATE_DIR = path.join(baseDir, "state");
|
|
||||||
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = path.join(baseDir, "bundled-none");
|
|
||||||
vi.resetModules();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
if (prevStateDir === undefined) {
|
|
||||||
delete process.env.OPENCLAW_STATE_DIR;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_STATE_DIR = prevStateDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevBundledDir === undefined) {
|
|
||||||
delete process.env.OPENCLAW_BUNDLED_HOOKS_DIR;
|
|
||||||
} else {
|
|
||||||
process.env.OPENCLAW_BUNDLED_HOOKS_DIR = prevBundledDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
for (const dir of tempDirs.splice(0)) {
|
for (const dir of tempDirs.splice(0)) {
|
||||||
try {
|
try {
|
||||||
await fs.rm(dir, { recursive: true, force: true });
|
await fs.rm(dir, { recursive: true, force: true });
|
||||||
@@ -92,23 +78,29 @@ describe("hooks install (e2e)", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const { installHooksFromPath } = await import("./install.js");
|
const hooksDir = path.join(baseDir, "managed-hooks");
|
||||||
const installResult = await installHooksFromPath({ path: packDir });
|
const installResult = await installHooksFromPath({ path: packDir, hooksDir });
|
||||||
expect(installResult.ok).toBe(true);
|
expect(installResult.ok).toBe(true);
|
||||||
if (!installResult.ok) {
|
if (!installResult.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { clearInternalHooks, createInternalHookEvent, triggerInternalHook } =
|
|
||||||
await import("./internal-hooks.js");
|
|
||||||
const { loadInternalHooks } = await import("./loader.js");
|
|
||||||
|
|
||||||
clearInternalHooks();
|
clearInternalHooks();
|
||||||
|
const bundledHooksDir = path.join(baseDir, "bundled-none");
|
||||||
|
await fs.mkdir(bundledHooksDir, { recursive: true });
|
||||||
const loaded = await loadInternalHooks(
|
const loaded = await loadInternalHooks(
|
||||||
{ hooks: { internal: { enabled: true } } },
|
{
|
||||||
|
hooks: {
|
||||||
|
internal: {
|
||||||
|
enabled: true,
|
||||||
|
load: { extraDirs: [hooksDir] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
{ managedHooksDir: hooksDir, bundledHooksDir },
|
||||||
);
|
);
|
||||||
expect(loaded).toBe(1);
|
expect(loaded).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
const event = createInternalHookEvent("command", "new", "test-session");
|
const event = createInternalHookEvent("command", "new", "test-session");
|
||||||
await triggerInternalHook(event);
|
await triggerInternalHook(event);
|
||||||
|
|||||||
+9
-1
@@ -36,6 +36,10 @@ import { loadWorkspaceHookEntries } from "./workspace.js";
|
|||||||
export async function loadInternalHooks(
|
export async function loadInternalHooks(
|
||||||
cfg: OpenClawConfig,
|
cfg: OpenClawConfig,
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
|
opts?: {
|
||||||
|
managedHooksDir?: string;
|
||||||
|
bundledHooksDir?: string;
|
||||||
|
},
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
// Check if hooks are enabled
|
// Check if hooks are enabled
|
||||||
if (!cfg.hooks?.internal?.enabled) {
|
if (!cfg.hooks?.internal?.enabled) {
|
||||||
@@ -46,7 +50,11 @@ export async function loadInternalHooks(
|
|||||||
|
|
||||||
// 1. Load hooks from directories (new system)
|
// 1. Load hooks from directories (new system)
|
||||||
try {
|
try {
|
||||||
const hookEntries = loadWorkspaceHookEntries(workspaceDir, { config: cfg });
|
const hookEntries = loadWorkspaceHookEntries(workspaceDir, {
|
||||||
|
config: cfg,
|
||||||
|
managedHooksDir: opts?.managedHooksDir,
|
||||||
|
bundledHooksDir: opts?.bundledHooksDir,
|
||||||
|
});
|
||||||
|
|
||||||
// Filter by eligibility
|
// Filter by eligibility
|
||||||
const eligible = hookEntries.filter((entry) => shouldIncludeHook({ entry, config: cfg }));
|
const eligible = hookEntries.filter((entry) => shouldIncludeHook({ entry, config: cfg }));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import * as skillScanner from "../security/skill-scanner.js";
|
||||||
|
|
||||||
vi.mock("../process/exec.js", () => ({
|
vi.mock("../process/exec.js", () => ({
|
||||||
runCommandWithTimeout: vi.fn(),
|
runCommandWithTimeout: vi.fn(),
|
||||||
@@ -449,18 +450,9 @@ describe("installPluginFromArchive", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("continues install when scanner throws", async () => {
|
it("continues install when scanner throws", async () => {
|
||||||
vi.resetModules();
|
const scanSpy = vi
|
||||||
vi.doMock("../security/skill-scanner.js", async () => {
|
.spyOn(skillScanner, "scanDirectoryWithSummary")
|
||||||
const actual = await vi.importActual<typeof import("../security/skill-scanner.js")>(
|
.mockRejectedValueOnce(new Error("scanner exploded"));
|
||||||
"../security/skill-scanner.js",
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
scanDirectoryWithSummary: async () => {
|
|
||||||
throw new Error("scanner exploded");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const tmpDir = makeTempDir();
|
const tmpDir = makeTempDir();
|
||||||
const pluginDir = path.join(tmpDir, "plugin-src");
|
const pluginDir = path.join(tmpDir, "plugin-src");
|
||||||
@@ -492,9 +484,7 @@ describe("installPluginFromArchive", () => {
|
|||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
expect(warnings.some((w) => w.includes("code safety scan failed"))).toBe(true);
|
expect(warnings.some((w) => w.includes("code safety scan failed"))).toBe(true);
|
||||||
|
scanSpy.mockRestore();
|
||||||
vi.doUnmock("../security/skill-scanner.js");
|
|
||||||
vi.resetModules();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
resolvePackedRootDir,
|
resolvePackedRootDir,
|
||||||
} from "../infra/archive.js";
|
} from "../infra/archive.js";
|
||||||
import { runCommandWithTimeout } from "../process/exec.js";
|
import { runCommandWithTimeout } from "../process/exec.js";
|
||||||
import { scanDirectoryWithSummary } from "../security/skill-scanner.js";
|
import * as skillScanner from "../security/skill-scanner.js";
|
||||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||||
|
|
||||||
type PluginInstallLogger = {
|
type PluginInstallLogger = {
|
||||||
@@ -196,7 +196,7 @@ async function installPluginFromPackageDir(params: {
|
|||||||
|
|
||||||
// Scan plugin source for dangerous code patterns (warn-only; never blocks install)
|
// Scan plugin source for dangerous code patterns (warn-only; never blocks install)
|
||||||
try {
|
try {
|
||||||
const scanSummary = await scanDirectoryWithSummary(params.packageDir, {
|
const scanSummary = await skillScanner.scanDirectoryWithSummary(params.packageDir, {
|
||||||
includeFiles: forcedScanEntries,
|
includeFiles: forcedScanEntries,
|
||||||
});
|
});
|
||||||
if (scanSummary.critical > 0) {
|
if (scanSummary.critical > 0) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import path from "node:path";
|
|||||||
import type { SandboxToolPolicy } from "../agents/sandbox/types.js";
|
import type { SandboxToolPolicy } from "../agents/sandbox/types.js";
|
||||||
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/config.js";
|
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/config.js";
|
||||||
import type { AgentToolsConfig } from "../config/types.tools.js";
|
import type { AgentToolsConfig } from "../config/types.tools.js";
|
||||||
|
import type { SkillScanFinding } from "./skill-scanner.js";
|
||||||
import type { ExecFn } from "./windows-acl.js";
|
import type { ExecFn } from "./windows-acl.js";
|
||||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js";
|
import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js";
|
||||||
@@ -31,7 +32,7 @@ import {
|
|||||||
inspectPathPermissions,
|
inspectPathPermissions,
|
||||||
safeStat,
|
safeStat,
|
||||||
} from "./audit-fs.js";
|
} from "./audit-fs.js";
|
||||||
import { scanDirectoryWithSummary, type SkillScanFinding } from "./skill-scanner.js";
|
import * as skillScanner from "./skill-scanner.js";
|
||||||
|
|
||||||
export type SecurityAuditFinding = {
|
export type SecurityAuditFinding = {
|
||||||
checkId: string;
|
checkId: string;
|
||||||
@@ -812,19 +813,21 @@ export async function collectPluginsCodeSafetyFindings(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = await scanDirectoryWithSummary(pluginPath, {
|
const summary = await skillScanner
|
||||||
includeFiles: forcedScanEntries,
|
.scanDirectoryWithSummary(pluginPath, {
|
||||||
}).catch((err) => {
|
includeFiles: forcedScanEntries,
|
||||||
findings.push({
|
})
|
||||||
checkId: "plugins.code_safety.scan_failed",
|
.catch((err) => {
|
||||||
severity: "warn",
|
findings.push({
|
||||||
title: `Plugin "${pluginName}" code scan failed`,
|
checkId: "plugins.code_safety.scan_failed",
|
||||||
detail: `Static code scan could not complete: ${String(err)}`,
|
severity: "warn",
|
||||||
remediation:
|
title: `Plugin "${pluginName}" code scan failed`,
|
||||||
"Check file permissions and plugin layout, then rerun `openclaw security audit --deep`.",
|
detail: `Static code scan could not complete: ${String(err)}`,
|
||||||
|
remediation:
|
||||||
|
"Check file permissions and plugin layout, then rerun `openclaw security audit --deep`.",
|
||||||
|
});
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
return null;
|
|
||||||
});
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -885,7 +888,7 @@ export async function collectInstalledSkillsCodeSafetyFindings(params: {
|
|||||||
scannedSkillDirs.add(skillDir);
|
scannedSkillDirs.add(skillDir);
|
||||||
|
|
||||||
const skillName = entry.skill.name;
|
const skillName = entry.skill.name;
|
||||||
const summary = await scanDirectoryWithSummary(skillDir).catch((err) => {
|
const summary = await skillScanner.scanDirectoryWithSummary(skillDir).catch((err) => {
|
||||||
findings.push({
|
findings.push({
|
||||||
checkId: "skills.code_safety.scan_failed",
|
checkId: "skills.code_safety.scan_failed",
|
||||||
severity: "warn",
|
severity: "warn",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import type { OpenClawConfig } from "../config/config.js";
|
|||||||
import { discordPlugin } from "../../extensions/discord/src/channel.js";
|
import { discordPlugin } from "../../extensions/discord/src/channel.js";
|
||||||
import { slackPlugin } from "../../extensions/slack/src/channel.js";
|
import { slackPlugin } from "../../extensions/slack/src/channel.js";
|
||||||
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
||||||
|
import { collectPluginsCodeSafetyFindings } from "./audit-extra.js";
|
||||||
import { runSecurityAudit } from "./audit.js";
|
import { runSecurityAudit } from "./audit.js";
|
||||||
|
import * as skillScanner from "./skill-scanner.js";
|
||||||
|
|
||||||
const isWindows = process.platform === "win32";
|
const isWindows = process.platform === "win32";
|
||||||
|
|
||||||
@@ -1492,17 +1494,9 @@ description: test skill
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("reports scan_failed when plugin code scanner throws during deep audit", async () => {
|
it("reports scan_failed when plugin code scanner throws during deep audit", async () => {
|
||||||
vi.resetModules();
|
const scanSpy = vi
|
||||||
vi.doMock("./skill-scanner.js", async () => {
|
.spyOn(skillScanner, "scanDirectoryWithSummary")
|
||||||
const actual =
|
.mockRejectedValueOnce(new Error("boom"));
|
||||||
await vi.importActual<typeof import("./skill-scanner.js")>("./skill-scanner.js");
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
scanDirectoryWithSummary: async () => {
|
|
||||||
throw new Error("boom");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-audit-scanner-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-audit-scanner-"));
|
||||||
try {
|
try {
|
||||||
@@ -1517,12 +1511,10 @@ description: test skill
|
|||||||
);
|
);
|
||||||
await fs.writeFile(path.join(pluginDir, "index.js"), "export {};");
|
await fs.writeFile(path.join(pluginDir, "index.js"), "export {};");
|
||||||
|
|
||||||
const { collectPluginsCodeSafetyFindings } = await import("./audit-extra.js");
|
|
||||||
const findings = await collectPluginsCodeSafetyFindings({ stateDir: tmpDir });
|
const findings = await collectPluginsCodeSafetyFindings({ stateDir: tmpDir });
|
||||||
expect(findings.some((f) => f.checkId === "plugins.code_safety.scan_failed")).toBe(true);
|
expect(findings.some((f) => f.checkId === "plugins.code_safety.scan_failed")).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
vi.doUnmock("./skill-scanner.js");
|
scanSpy.mockRestore();
|
||||||
vi.resetModules();
|
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
|
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user