Files
openclaw/src/plugins/plugin-scan-existence-cache.test.ts
ml12580 1585ec54f1 perf(plugins): cache existence probes within bundle manifest scan [AI-assisted] (#93919)
* perf(plugins): cache existence probes within bundle manifest scan

Bundle plugin discovery re-probes the same marker paths (skills/, commands/,
agents/, .mcp.json, .lsp.json, settings.json, hooks/hooks.json) once in
detectBundleManifestFormat and again in loadBundleManifest's capability
builders. Across the bundled plugin tree this is thousands of redundant
synchronous fs.existsSync calls; #76209 reports 25.4s of self-time on a
Windows cold start.

Add a scan-scoped existence cache (plugin-scan-existence-cache.ts) entered
only around discoverBundleInRoot. pluginScanExistsSync memoizes inside the
active scan and falls back to plain fs.existsSync outside it, so install,
hooks, and doctor flows stay uncached. The cache is push/pop per
discoverBundleInRoot call (try/finally), so a later install/repair pass
re-reads the filesystem — no process-global staleness.

Measured on Windows over a 25-plugin fixture: 550 -> 325 fs.existsSync
calls (41% fewer), 294.75ms -> 208.49ms. Discovery results unchanged.

Closes #76209

* fix(plugins): drop unused test reset helper and satisfy oxlint

Remove __resetPluginScanExistenceCacheForTest: the scan cache is push/pop
balanced by try/finally in withPluginScanExistenceCache, so the stack never
leaks between tests and the helper was dead code. It also tripped oxlint
no-underscore-dangle. Refactor the integration test to count existsSync calls
via a const-returning helper so there is no useless assignment.
2026-06-22 18:27:36 +00:00

161 lines
5.9 KiB
TypeScript

// Verifies the scan-scoped plugin existence cache: within a scan pass repeated
// probes hit the filesystem once, across scans the filesystem is re-read (no
// process-global staleness), and outside a scan it is a plain passthrough.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadBundleManifest, detectBundleManifestFormat } from "./bundle-manifest.js";
import {
pluginScanExistsSync,
withPluginScanExistenceCache,
} from "./plugin-scan-existence-cache.js";
const tempDirs: string[] = [];
function makeTempDir(prefix: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
afterEach(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs.length = 0;
});
describe("pluginScanExistsSync", () => {
it("is a plain passthrough outside a scan (no caching)", () => {
const dir = makeTempDir("exists-passthrough-");
const target = path.join(dir, "marker.json");
fs.writeFileSync(target, "{}");
const spy = vi.spyOn(fs, "existsSync");
try {
expect(pluginScanExistsSync(target)).toBe(true);
expect(pluginScanExistsSync(target)).toBe(true);
expect(pluginScanExistsSync(target)).toBe(true);
// No active scan cache: every probe reaches the filesystem.
expect(spy).toHaveBeenCalledTimes(3);
} finally {
spy.mockRestore();
}
});
it("memoizes repeated probes within a single scan pass", () => {
const dir = makeTempDir("exists-memoize-");
const target = path.join(dir, "marker.json");
fs.writeFileSync(target, "{}");
const spy = vi.spyOn(fs, "existsSync");
try {
const result = withPluginScanExistenceCache(() => {
let hit = false;
for (let i = 0; i < 5; i += 1) {
if (pluginScanExistsSync(target)) {
hit = true;
}
}
return hit;
});
expect(result).toBe(true);
// Five probes of the same path, one filesystem call inside the scan.
expect(spy).toHaveBeenCalledTimes(1);
} finally {
spy.mockRestore();
}
});
it("does not conflate different paths inside a scan", () => {
const dir = makeTempDir("exists-distinct-");
const present = path.join(dir, "present.json");
const absent = path.join(dir, "absent.json");
fs.writeFileSync(present, "{}");
const spy = vi.spyOn(fs, "existsSync");
try {
withPluginScanExistenceCache(() => {
expect(pluginScanExistsSync(present)).toBe(true);
expect(pluginScanExistsSync(absent)).toBe(false);
// Re-probe both: served from cache, no new filesystem calls.
expect(pluginScanExistsSync(present)).toBe(true);
expect(pluginScanExistsSync(absent)).toBe(false);
});
expect(spy).toHaveBeenCalledTimes(2);
} finally {
spy.mockRestore();
}
});
it("re-reads the filesystem across separate scans (no process-global staleness)", () => {
const dir = makeTempDir("exists-freshness-");
const target = path.join(dir, "marker.json");
// First scan sees the path absent.
withPluginScanExistenceCache(() => {
expect(pluginScanExistsSync(target)).toBe(false);
});
// Marker appears between scans (e.g. an install/repair pass).
fs.writeFileSync(target, "{}");
// A later scan must observe the new state, not a stale cached false.
withPluginScanExistenceCache(() => {
expect(pluginScanExistsSync(target)).toBe(true);
});
});
});
describe("bundle manifest scan uses the existence cache", () => {
function buildClaudeBundlePlugin(root: string): void {
fs.mkdirSync(path.join(root, ".claude-plugin"), { recursive: true });
fs.writeFileSync(
path.join(root, ".claude-plugin", "plugin.json"),
JSON.stringify({ name: "demo-bundle" }),
);
// Capability marker dirs/files that both detectBundleManifestFormat and
// loadBundleManifest probe for a claude-format bundle.
fs.mkdirSync(path.join(root, "skills"), { recursive: true });
fs.mkdirSync(path.join(root, "commands"), { recursive: true });
fs.mkdirSync(path.join(root, "agents"), { recursive: true });
fs.writeFileSync(path.join(root, ".mcp.json"), "{}");
fs.writeFileSync(path.join(root, ".lsp.json"), "{}");
fs.writeFileSync(path.join(root, "settings.json"), "{}");
}
it("reduces fs.existsSync calls when detect + load run under one scan cache", () => {
const root = makeTempDir("claude-bundle-");
buildClaudeBundlePlugin(root);
// Run a detect + load pair and count fs.existsSync calls under a spy.
const detectAndLoad = (): void => {
const format = detectBundleManifestFormat(root);
if (format) {
loadBundleManifest({ rootDir: root, bundleFormat: format });
}
};
const countExistsSyncCalls = (run: () => void): number => {
const spy = vi.spyOn(fs, "existsSync");
try {
run();
return spy.mock.calls.length;
} finally {
spy.mockRestore();
}
};
// Baseline: no scan cache → detect and load re-probe the same marker paths.
const uncachedCalls = countExistsSyncCalls(detectAndLoad);
// Same workload, but the detect + load pair shares one scan cache.
const cachedCalls = countExistsSyncCalls(() => withPluginScanExistenceCache(detectAndLoad));
// Both paths must still produce a valid claude bundle manifest.
const manifest = withPluginScanExistenceCache(() => {
const format = detectBundleManifestFormat(root);
return format ? loadBundleManifest({ rootDir: root, bundleFormat: format }) : null;
});
expect(manifest?.ok).toBe(true);
// The cache must eliminate the redundant probes; assert a real reduction
// rather than an exact count so the test is robust to marker-set changes.
expect(cachedCalls).toBeLessThan(uncachedCalls);
expect(cachedCalls).toBeGreaterThan(0);
});
});