perf(test): speed up suites and reduce fs churn

This commit is contained in:
Peter Steinberger
2026-02-15 19:18:49 +00:00
parent 8fdde0429e
commit 92f8c0fac3
32 changed files with 1793 additions and 1398 deletions

View File

@@ -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) {

View File

@@ -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);