mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 07:40:23 +00:00
perf(test): speed up suites and reduce fs churn
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const copyToClipboard = vi.fn();
|
||||
const runtime = {
|
||||
@@ -10,6 +8,91 @@ const runtime = {
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" };
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
entries: new Map<string, FakeFsEntry>(),
|
||||
counter: 0,
|
||||
}));
|
||||
|
||||
const abs = (p: string) => path.resolve(p);
|
||||
|
||||
function setFile(p: string, content = "") {
|
||||
const resolved = abs(p);
|
||||
state.entries.set(resolved, { kind: "file", content });
|
||||
setDir(path.dirname(resolved));
|
||||
}
|
||||
|
||||
function setDir(p: string) {
|
||||
const resolved = abs(p);
|
||||
if (!state.entries.has(resolved)) {
|
||||
state.entries.set(resolved, { kind: "dir" });
|
||||
}
|
||||
}
|
||||
|
||||
function copyTree(src: string, dest: string) {
|
||||
const srcAbs = abs(src);
|
||||
const destAbs = abs(dest);
|
||||
const srcPrefix = `${srcAbs}${path.sep}`;
|
||||
for (const [key, entry] of state.entries.entries()) {
|
||||
if (key === srcAbs || key.startsWith(srcPrefix)) {
|
||||
const rel = key === srcAbs ? "" : key.slice(srcPrefix.length);
|
||||
const next = rel ? path.join(destAbs, rel) : destAbs;
|
||||
state.entries.set(next, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs")>();
|
||||
const pathMod = await import("node:path");
|
||||
const absInMock = (p: string) => pathMod.resolve(p);
|
||||
|
||||
const wrapped = {
|
||||
...actual,
|
||||
existsSync: (p: string) => state.entries.has(absInMock(p)),
|
||||
mkdirSync: (p: string, _opts?: unknown) => {
|
||||
setDir(p);
|
||||
},
|
||||
writeFileSync: (p: string, content: string) => {
|
||||
setFile(p, content);
|
||||
},
|
||||
renameSync: (from: string, to: string) => {
|
||||
const fromAbs = absInMock(from);
|
||||
const toAbs = absInMock(to);
|
||||
const entry = state.entries.get(fromAbs);
|
||||
if (!entry) {
|
||||
throw new Error(`ENOENT: no such file or directory, rename '${from}' -> '${to}'`);
|
||||
}
|
||||
state.entries.delete(fromAbs);
|
||||
state.entries.set(toAbs, entry);
|
||||
},
|
||||
rmSync: (p: string) => {
|
||||
const root = absInMock(p);
|
||||
const prefix = `${root}${pathMod.sep}`;
|
||||
const keys = Array.from(state.entries.keys());
|
||||
for (const key of keys) {
|
||||
if (key === root || key.startsWith(prefix)) {
|
||||
state.entries.delete(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
mkdtempSync: (prefix: string) => {
|
||||
const dir = `${prefix}${state.counter++}`;
|
||||
setDir(dir);
|
||||
return dir;
|
||||
},
|
||||
promises: {
|
||||
...actual.promises,
|
||||
cp: async (src: string, dest: string, _opts?: unknown) => {
|
||||
copyTree(src, dest);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { ...wrapped, default: wrapped };
|
||||
});
|
||||
|
||||
vi.mock("../infra/clipboard.js", () => ({
|
||||
copyToClipboard,
|
||||
}));
|
||||
@@ -18,86 +101,83 @@ vi.mock("../runtime.js", () => ({
|
||||
defaultRuntime: runtime,
|
||||
}));
|
||||
|
||||
let resolveBundledExtensionRootDir: typeof import("./browser-cli-extension.js").resolveBundledExtensionRootDir;
|
||||
let installChromeExtension: typeof import("./browser-cli-extension.js").installChromeExtension;
|
||||
let registerBrowserExtensionCommands: typeof import("./browser-cli-extension.js").registerBrowserExtensionCommands;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ resolveBundledExtensionRootDir, installChromeExtension, registerBrowserExtensionCommands } =
|
||||
await import("./browser-cli-extension.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
state.entries.clear();
|
||||
state.counter = 0;
|
||||
copyToClipboard.mockReset();
|
||||
runtime.log.mockReset();
|
||||
runtime.error.mockReset();
|
||||
runtime.exit.mockReset();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function writeManifest(dir: string) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "manifest.json"), JSON.stringify({ manifest_version: 3 }));
|
||||
setDir(dir);
|
||||
setFile(path.join(dir, "manifest.json"), JSON.stringify({ manifest_version: 3 }));
|
||||
}
|
||||
|
||||
describe("bundled extension resolver", () => {
|
||||
it("walks up to find the assets directory", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ext-root-"));
|
||||
describe("bundled extension resolver (fs-mocked)", () => {
|
||||
it("walks up to find the assets directory", () => {
|
||||
const root = abs("/tmp/openclaw-ext-root");
|
||||
const here = path.join(root, "dist", "cli");
|
||||
const assets = path.join(root, "assets", "chrome-extension");
|
||||
|
||||
try {
|
||||
writeManifest(assets);
|
||||
fs.mkdirSync(here, { recursive: true });
|
||||
writeManifest(assets);
|
||||
setDir(here);
|
||||
|
||||
const { resolveBundledExtensionRootDir } = await import("./browser-cli-extension.js");
|
||||
expect(resolveBundledExtensionRootDir(here)).toBe(assets);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
expect(resolveBundledExtensionRootDir(here)).toBe(assets);
|
||||
});
|
||||
|
||||
it("prefers the nearest assets directory", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ext-root-"));
|
||||
it("prefers the nearest assets directory", () => {
|
||||
const root = abs("/tmp/openclaw-ext-root-nearest");
|
||||
const here = path.join(root, "dist", "cli");
|
||||
const distAssets = path.join(root, "dist", "assets", "chrome-extension");
|
||||
const rootAssets = path.join(root, "assets", "chrome-extension");
|
||||
|
||||
try {
|
||||
writeManifest(distAssets);
|
||||
writeManifest(rootAssets);
|
||||
fs.mkdirSync(here, { recursive: true });
|
||||
writeManifest(distAssets);
|
||||
writeManifest(rootAssets);
|
||||
setDir(here);
|
||||
|
||||
const { resolveBundledExtensionRootDir } = await import("./browser-cli-extension.js");
|
||||
expect(resolveBundledExtensionRootDir(here)).toBe(distAssets);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
expect(resolveBundledExtensionRootDir(here)).toBe(distAssets);
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser extension install", () => {
|
||||
describe("browser extension install (fs-mocked)", () => {
|
||||
it("installs into the state dir (never node_modules)", async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ext-"));
|
||||
const tmp = abs("/tmp/openclaw-ext-install");
|
||||
const sourceDir = path.join(tmp, "source-ext");
|
||||
writeManifest(sourceDir);
|
||||
setFile(path.join(sourceDir, "test.txt"), "ok");
|
||||
|
||||
try {
|
||||
const { installChromeExtension } = await import("./browser-cli-extension.js");
|
||||
// Keep this test hermetic + fast: use a tiny fixture instead of copying the
|
||||
// full repo assets tree.
|
||||
const sourceDir = path.join(tmp, "source-ext");
|
||||
writeManifest(sourceDir);
|
||||
fs.writeFileSync(path.join(sourceDir, "test.txt"), "ok");
|
||||
const result = await installChromeExtension({ stateDir: tmp, sourceDir });
|
||||
const result = await installChromeExtension({ stateDir: tmp, sourceDir });
|
||||
|
||||
expect(result.path).toBe(path.join(tmp, "browser", "chrome-extension"));
|
||||
expect(fs.existsSync(path.join(result.path, "manifest.json"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(result.path, "test.txt"))).toBe(true);
|
||||
expect(result.path.includes("node_modules")).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
expect(result.path).toBe(path.join(tmp, "browser", "chrome-extension"));
|
||||
expect(state.entries.has(abs(path.join(result.path, "manifest.json")))).toBe(true);
|
||||
expect(state.entries.has(abs(path.join(result.path, "test.txt")))).toBe(true);
|
||||
expect(result.path.includes("node_modules")).toBe(false);
|
||||
});
|
||||
|
||||
it("copies extension path to clipboard", async () => {
|
||||
const prev = process.env.OPENCLAW_STATE_DIR;
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ext-path-"));
|
||||
const tmp = abs("/tmp/openclaw-ext-path");
|
||||
process.env.OPENCLAW_STATE_DIR = tmp;
|
||||
|
||||
try {
|
||||
copyToClipboard.mockReset();
|
||||
copyToClipboard.mockResolvedValue(true);
|
||||
runtime.log.mockReset();
|
||||
runtime.error.mockReset();
|
||||
runtime.exit.mockReset();
|
||||
|
||||
const dir = path.join(tmp, "browser", "chrome-extension");
|
||||
writeManifest(dir);
|
||||
|
||||
const { Command } = await import("commander");
|
||||
const { registerBrowserExtensionCommands } = await import("./browser-cli-extension.js");
|
||||
|
||||
const program = new Command();
|
||||
const browser = program.command("browser").option("--json", false);
|
||||
@@ -107,7 +187,6 @@ describe("browser extension install", () => {
|
||||
);
|
||||
|
||||
await program.parseAsync(["browser", "extension", "path"], { from: "user" });
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(dir);
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
|
||||
@@ -64,11 +64,27 @@ describe("runGatewayLoop", () => {
|
||||
|
||||
const closeFirst = vi.fn(async () => {});
|
||||
const closeSecond = vi.fn(async () => {});
|
||||
const start = vi
|
||||
.fn<StartServer>()
|
||||
.mockResolvedValueOnce({ close: closeFirst })
|
||||
.mockResolvedValueOnce({ close: closeSecond })
|
||||
.mockRejectedValueOnce(new Error("stop-loop"));
|
||||
|
||||
const start = vi.fn<StartServer>();
|
||||
let resolveFirst: (() => void) | null = null;
|
||||
const startedFirst = new Promise<void>((resolve) => {
|
||||
resolveFirst = resolve;
|
||||
});
|
||||
start.mockImplementationOnce(async () => {
|
||||
resolveFirst?.();
|
||||
return { close: closeFirst };
|
||||
});
|
||||
|
||||
let resolveSecond: (() => void) | null = null;
|
||||
const startedSecond = new Promise<void>((resolve) => {
|
||||
resolveSecond = resolve;
|
||||
});
|
||||
start.mockImplementationOnce(async () => {
|
||||
resolveSecond?.();
|
||||
return { close: closeSecond };
|
||||
});
|
||||
|
||||
start.mockRejectedValueOnce(new Error("stop-loop"));
|
||||
|
||||
const beforeSigterm = new Set(
|
||||
process.listeners("SIGTERM") as Array<(...args: unknown[]) => void>,
|
||||
@@ -80,25 +96,24 @@ describe("runGatewayLoop", () => {
|
||||
process.listeners("SIGUSR1") as Array<(...args: unknown[]) => void>,
|
||||
);
|
||||
|
||||
const loopPromise = import("./run-loop.js").then(({ runGatewayLoop }) =>
|
||||
runGatewayLoop({
|
||||
start,
|
||||
runtime: {
|
||||
exit: vi.fn(),
|
||||
} as { exit: (code: number) => never },
|
||||
}),
|
||||
);
|
||||
const { runGatewayLoop } = await import("./run-loop.js");
|
||||
const loopPromise = runGatewayLoop({
|
||||
start,
|
||||
runtime: {
|
||||
exit: vi.fn(),
|
||||
} as { exit: (code: number) => never },
|
||||
});
|
||||
|
||||
try {
|
||||
await vi.waitFor(() => {
|
||||
expect(start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
await startedFirst;
|
||||
expect(start).toHaveBeenCalledTimes(1);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
process.emit("SIGUSR1");
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(start).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
await startedSecond;
|
||||
expect(start).toHaveBeenCalledTimes(2);
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(waitForActiveTasks).toHaveBeenCalledWith(30_000);
|
||||
expect(gatewayLog.warn).toHaveBeenCalledWith(DRAIN_TIMEOUT_LOG);
|
||||
|
||||
Reference in New Issue
Block a user