mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 13:11:40 +00:00
perf(test): trim runtime lookups and add changed bench
This commit is contained in:
@@ -88,6 +88,8 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
||||
- Perf-debug note:
|
||||
- `pnpm test:perf:imports` enables Vitest import-duration reporting plus import-breakdown output.
|
||||
- `pnpm test:perf:imports:changed` scopes the same profiling view to files changed since `origin/main`.
|
||||
- `pnpm test:perf:changed:bench -- --ref <git-ref>` compares routed `test:changed` against the native root-project path for that committed diff and prints wall time plus macOS max RSS.
|
||||
- `pnpm test:perf:changed:bench -- --worktree` benchmarks the current dirty tree by routing the changed file list through `scripts/test-projects.mjs` and the root Vitest config.
|
||||
- `pnpm test:perf:profile:main` writes a main-thread CPU profile for Vitest/Vite startup and transform overhead.
|
||||
- `pnpm test:perf:profile:runner` writes runner CPU+heap profiles for the unit suite with file parallelism disabled.
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ title: "Tests"
|
||||
- `pnpm test:extensions`: runs extension/plugin suites.
|
||||
- `pnpm test:perf:imports`: enables Vitest import-duration + import-breakdown reporting, while still using scoped lane routing for explicit file/directory targets.
|
||||
- `pnpm test:perf:imports:changed`: same import profiling, but only for files changed since `origin/main`.
|
||||
- `pnpm test:perf:changed:bench -- --ref <git-ref>` benchmarks the routed changed-mode path against the native root-project run for the same committed git diff.
|
||||
- `pnpm test:perf:changed:bench -- --worktree` benchmarks the current worktree change set without committing first.
|
||||
- `pnpm test:perf:profile:main`: writes a CPU profile for the Vitest main thread (`.artifacts/vitest-main-profile`).
|
||||
- `pnpm test:perf:profile:runner`: writes CPU + heap profiles for the unit runner (`.artifacts/vitest-runner-profile`).
|
||||
- Gateway integration: opt-in via `OPENCLAW_TEST_INCLUDE_GATEWAY=1 pnpm test` or `pnpm test:gateway`.
|
||||
|
||||
@@ -1176,6 +1176,7 @@
|
||||
"test:parallels:npm-update": "bash scripts/e2e/parallels-npm-update-smoke.sh",
|
||||
"test:parallels:windows": "bash scripts/e2e/parallels-windows-smoke.sh",
|
||||
"test:perf:budget": "node scripts/test-perf-budget.mjs",
|
||||
"test:perf:changed:bench": "node scripts/bench-test-changed.mjs",
|
||||
"test:perf:hotspots": "node scripts/test-hotspots.mjs",
|
||||
"test:perf:imports": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/test-projects.mjs",
|
||||
"test:perf:imports:changed": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/test-projects.mjs --changed origin/main",
|
||||
|
||||
187
scripts/bench-test-changed.mjs
Normal file
187
scripts/bench-test-changed.mjs
Normal file
@@ -0,0 +1,187 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { floatFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
|
||||
import { formatMs } from "./lib/vitest-report-cli-utils.mjs";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = parseFlagArgs(
|
||||
argv,
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
ref: "origin/main",
|
||||
rss: process.platform === "darwin",
|
||||
mode: "ref",
|
||||
},
|
||||
[
|
||||
stringFlag("--cwd", "cwd"),
|
||||
stringFlag("--ref", "ref"),
|
||||
floatFlag("--max-workers", "maxWorkers", { min: 1 }),
|
||||
],
|
||||
{
|
||||
allowUnknownOptions: true,
|
||||
onUnhandledArg(arg, target) {
|
||||
if (arg === "--no-rss") {
|
||||
target.rss = false;
|
||||
return "handled";
|
||||
}
|
||||
if (arg === "--worktree") {
|
||||
target.mode = "worktree";
|
||||
return "handled";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
);
|
||||
return {
|
||||
cwd: path.resolve(args.cwd),
|
||||
mode: args.mode,
|
||||
ref: args.ref,
|
||||
rss: args.rss,
|
||||
...(typeof args.maxWorkers === "number" ? { maxWorkers: Math.trunc(args.maxWorkers) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function quoteArg(arg) {
|
||||
return /[^A-Za-z0-9_./:-]/.test(arg) ? JSON.stringify(arg) : arg;
|
||||
}
|
||||
|
||||
function runGitList(args, cwd) {
|
||||
const result = spawnSync("git", args, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || result.stdout || `git ${args.join(" ")} failed`);
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
}
|
||||
|
||||
function listChangedPaths(opts) {
|
||||
if (opts.mode === "worktree") {
|
||||
return [
|
||||
...new Set([
|
||||
...runGitList(["diff", "--name-only", "--relative", "HEAD", "--"], opts.cwd),
|
||||
...runGitList(["ls-files", "--others", "--exclude-standard"], opts.cwd),
|
||||
]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
return runGitList(["diff", "--name-only", `${opts.ref}...HEAD`], opts.cwd);
|
||||
}
|
||||
|
||||
function parseMaxRssKb(output) {
|
||||
const match = output.match(/(\d+)\s+maximum resident set size/u);
|
||||
return match ? Number.parseInt(match[1], 10) : null;
|
||||
}
|
||||
|
||||
function formatRss(valueKb) {
|
||||
if (valueKb === null) {
|
||||
return "n/a";
|
||||
}
|
||||
return `${(valueKb / 1024).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
function runBenchCommand(params) {
|
||||
const env = { ...process.env };
|
||||
if (typeof params.maxWorkers === "number") {
|
||||
env.OPENCLAW_VITEST_MAX_WORKERS = String(params.maxWorkers);
|
||||
}
|
||||
const startedAt = process.hrtime.bigint();
|
||||
const commandArgs = params.rss ? ["-l", ...params.command] : params.command;
|
||||
const result = spawnSync(
|
||||
params.rss ? "/usr/bin/time" : commandArgs[0],
|
||||
params.rss ? commandArgs : commandArgs.slice(1),
|
||||
{
|
||||
cwd: params.cwd,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 1024 * 1024 * 32,
|
||||
},
|
||||
);
|
||||
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
||||
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
||||
return {
|
||||
elapsedMs,
|
||||
maxRssKb: params.rss ? parseMaxRssKb(output) : null,
|
||||
status: result.status ?? 1,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
function printRunSummary(label, result) {
|
||||
console.log(
|
||||
`${label.padEnd(8, " ")} wall=${formatMs(result.elapsedMs).padStart(9, " ")} rss=${formatRss(
|
||||
result.maxRssKb,
|
||||
).padStart(9, " ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const opts = parseArgs(process.argv.slice(2));
|
||||
const changedPaths = listChangedPaths(opts);
|
||||
if (changedPaths.length === 0) {
|
||||
console.log(
|
||||
opts.mode === "worktree"
|
||||
? "[bench-test-changed] no changed paths in worktree"
|
||||
: `[bench-test-changed] no changed paths for ${opts.ref}...HEAD`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
opts.mode === "worktree"
|
||||
? "[bench-test-changed] mode=worktree"
|
||||
: `[bench-test-changed] ref=${opts.ref}`,
|
||||
);
|
||||
console.log("[bench-test-changed] changed paths:");
|
||||
for (const changedPath of changedPaths) {
|
||||
console.log(`- ${changedPath}`);
|
||||
}
|
||||
|
||||
const routedCommand =
|
||||
opts.mode === "worktree"
|
||||
? [process.execPath, "scripts/test-projects.mjs", ...changedPaths]
|
||||
: [process.execPath, "scripts/test-projects.mjs", "--changed", opts.ref];
|
||||
const rootCommand = [
|
||||
process.execPath,
|
||||
"scripts/run-vitest.mjs",
|
||||
"run",
|
||||
"--config",
|
||||
"vitest.config.ts",
|
||||
...changedPaths,
|
||||
];
|
||||
|
||||
console.log(`[bench-test-changed] routed: ${routedCommand.map(quoteArg).join(" ")}`);
|
||||
const routed = runBenchCommand({
|
||||
command: routedCommand,
|
||||
cwd: opts.cwd,
|
||||
rss: opts.rss,
|
||||
...(typeof opts.maxWorkers === "number" ? { maxWorkers: opts.maxWorkers } : {}),
|
||||
});
|
||||
if (routed.status !== 0) {
|
||||
process.stderr.write(routed.output);
|
||||
process.exit(routed.status);
|
||||
}
|
||||
|
||||
console.log(`[bench-test-changed] root: ${rootCommand.map(quoteArg).join(" ")}`);
|
||||
const root = runBenchCommand({
|
||||
command: rootCommand,
|
||||
cwd: opts.cwd,
|
||||
rss: opts.rss,
|
||||
...(typeof opts.maxWorkers === "number" ? { maxWorkers: opts.maxWorkers } : {}),
|
||||
});
|
||||
if (root.status !== 0) {
|
||||
process.stderr.write(root.output);
|
||||
process.exit(root.status);
|
||||
}
|
||||
|
||||
printRunSummary("routed", routed);
|
||||
printRunSummary("root", root);
|
||||
console.log(
|
||||
`[bench-test-changed] delta wall=${formatMs(root.elapsedMs - routed.elapsedMs)} rss=${
|
||||
routed.maxRssKb !== null && root.maxRssKb !== null
|
||||
? formatRss(root.maxRssKb - routed.maxRssKb)
|
||||
: "n/a"
|
||||
}`,
|
||||
);
|
||||
@@ -17,6 +17,9 @@ describe("command secret targets module import", () => {
|
||||
|
||||
const mod = await import("./command-secret-targets.js");
|
||||
|
||||
expect(listSecretTargetRegistryEntries).not.toHaveBeenCalled();
|
||||
expect(mod.getModelsCommandSecretTargetIds().has("models.providers.*.apiKey")).toBe(true);
|
||||
expect(mod.getQrRemoteCommandSecretTargetIds().has("gateway.remote.token")).toBe(true);
|
||||
expect(listSecretTargetRegistryEntries).not.toHaveBeenCalled();
|
||||
expect(() => mod.getChannelsCommandSecretTargetIds()).toThrow("registry touched too early");
|
||||
expect(listSecretTargetRegistryEntries).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -53,11 +53,25 @@ vi.mock("../secrets/target-registry.js", () => ({
|
||||
|
||||
import {
|
||||
getAgentRuntimeCommandSecretTargetIds,
|
||||
getModelsCommandSecretTargetIds,
|
||||
getQrRemoteCommandSecretTargetIds,
|
||||
getScopedChannelsCommandSecretTargets,
|
||||
getSecurityAuditCommandSecretTargetIds,
|
||||
} from "./command-secret-targets.js";
|
||||
|
||||
describe("command secret target ids", () => {
|
||||
it("keeps static qr remote targets out of the registry path", () => {
|
||||
const ids = getQrRemoteCommandSecretTargetIds();
|
||||
expect(ids).toEqual(new Set(["gateway.remote.token", "gateway.remote.password"]));
|
||||
});
|
||||
|
||||
it("keeps static model targets out of the registry path", () => {
|
||||
const ids = getModelsCommandSecretTargetIds();
|
||||
expect(ids.has("models.providers.*.apiKey")).toBe(true);
|
||||
expect(ids.has("models.providers.*.request.tls.key")).toBe(true);
|
||||
expect(ids.has("channels.discord.token")).toBe(false);
|
||||
});
|
||||
|
||||
it("includes memorySearch remote targets for agent runtime commands", () => {
|
||||
const ids = getAgentRuntimeCommandSecretTargetIds();
|
||||
expect(ids.has("agents.defaults.memorySearch.remote.apiKey")).toBe(true);
|
||||
|
||||
@@ -5,6 +5,50 @@ import {
|
||||
listSecretTargetRegistryEntries,
|
||||
} from "../secrets/target-registry.js";
|
||||
|
||||
const STATIC_QR_REMOTE_TARGET_IDS = ["gateway.remote.token", "gateway.remote.password"] as const;
|
||||
const STATIC_MODEL_TARGET_IDS = [
|
||||
"models.providers.*.apiKey",
|
||||
"models.providers.*.headers.*",
|
||||
"models.providers.*.request.headers.*",
|
||||
"models.providers.*.request.auth.token",
|
||||
"models.providers.*.request.auth.value",
|
||||
"models.providers.*.request.proxy.tls.ca",
|
||||
"models.providers.*.request.proxy.tls.cert",
|
||||
"models.providers.*.request.proxy.tls.key",
|
||||
"models.providers.*.request.proxy.tls.passphrase",
|
||||
"models.providers.*.request.tls.ca",
|
||||
"models.providers.*.request.tls.cert",
|
||||
"models.providers.*.request.tls.key",
|
||||
"models.providers.*.request.tls.passphrase",
|
||||
] as const;
|
||||
const STATIC_AGENT_RUNTIME_TARGET_IDS = [
|
||||
...STATIC_MODEL_TARGET_IDS,
|
||||
"agents.defaults.memorySearch.remote.apiKey",
|
||||
"agents.list[].memorySearch.remote.apiKey",
|
||||
"messages.tts.providers.*.apiKey",
|
||||
"skills.entries.*.apiKey",
|
||||
"tools.web.search.apiKey",
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
"plugins.entries.google.config.webSearch.apiKey",
|
||||
"plugins.entries.xai.config.webSearch.apiKey",
|
||||
"plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
"plugins.entries.firecrawl.config.webFetch.apiKey",
|
||||
"plugins.entries.tavily.config.webSearch.apiKey",
|
||||
"plugins.entries.minimax.config.webSearch.apiKey",
|
||||
] as const;
|
||||
const STATIC_STATUS_TARGET_IDS = [
|
||||
"agents.defaults.memorySearch.remote.apiKey",
|
||||
"agents.list[].memorySearch.remote.apiKey",
|
||||
] as const;
|
||||
const STATIC_SECURITY_AUDIT_TARGET_IDS = [
|
||||
"gateway.auth.token",
|
||||
"gateway.auth.password",
|
||||
"gateway.remote.token",
|
||||
"gateway.remote.password",
|
||||
] as const;
|
||||
|
||||
function idsByPrefix(prefixes: readonly string[]): string[] {
|
||||
return listSecretTargetRegistryEntries()
|
||||
.map((entry) => entry.id)
|
||||
@@ -12,48 +56,28 @@ function idsByPrefix(prefixes: readonly string[]): string[] {
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
function idsByPredicate(predicate: (id: string) => boolean): string[] {
|
||||
return listSecretTargetRegistryEntries()
|
||||
.map((entry) => entry.id)
|
||||
.filter(predicate)
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
type CommandSecretTargets = {
|
||||
qrRemote: string[];
|
||||
channels: string[];
|
||||
models: string[];
|
||||
agentRuntime: string[];
|
||||
status: string[];
|
||||
securityAudit: string[];
|
||||
};
|
||||
|
||||
let cachedCommandSecretTargets: CommandSecretTargets | undefined;
|
||||
let cachedChannelSecretTargetIds: string[] | undefined;
|
||||
|
||||
function getChannelSecretTargetIds(): string[] {
|
||||
cachedChannelSecretTargetIds ??= idsByPrefix(["channels."]);
|
||||
return cachedChannelSecretTargetIds;
|
||||
}
|
||||
|
||||
function buildCommandSecretTargets(): CommandSecretTargets {
|
||||
const webPluginSecretTargets = idsByPredicate((id) =>
|
||||
/^plugins\.entries\.[^.]+\.config\.(webSearch|webFetch)\.apiKey$/.test(id),
|
||||
);
|
||||
|
||||
const channelTargetIds = getChannelSecretTargetIds();
|
||||
return {
|
||||
qrRemote: ["gateway.remote.token", "gateway.remote.password"],
|
||||
channels: idsByPrefix(["channels."]),
|
||||
models: idsByPrefix(["models.providers."]),
|
||||
agentRuntime: idsByPrefix([
|
||||
"channels.",
|
||||
"models.providers.",
|
||||
"agents.defaults.memorySearch.remote.",
|
||||
"agents.list[].memorySearch.remote.",
|
||||
"skills.entries.",
|
||||
"messages.tts.",
|
||||
"tools.web.search",
|
||||
]).concat(webPluginSecretTargets),
|
||||
status: idsByPrefix([
|
||||
"channels.",
|
||||
"agents.defaults.memorySearch.remote.",
|
||||
"agents.list[].memorySearch.remote.",
|
||||
]),
|
||||
securityAudit: idsByPrefix(["channels.", "gateway.auth.", "gateway.remote."]),
|
||||
channels: channelTargetIds,
|
||||
agentRuntime: [...STATIC_AGENT_RUNTIME_TARGET_IDS, ...channelTargetIds],
|
||||
status: [...STATIC_STATUS_TARGET_IDS, ...channelTargetIds],
|
||||
securityAudit: [...STATIC_SECURITY_AUDIT_TARGET_IDS, ...channelTargetIds],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,7 +151,7 @@ export function getScopedChannelsCommandSecretTargets(params: {
|
||||
}
|
||||
|
||||
export function getQrRemoteCommandSecretTargetIds(): Set<string> {
|
||||
return toTargetIdSet(getCommandSecretTargets().qrRemote);
|
||||
return toTargetIdSet(STATIC_QR_REMOTE_TARGET_IDS);
|
||||
}
|
||||
|
||||
export function getChannelsCommandSecretTargetIds(): Set<string> {
|
||||
@@ -135,7 +159,7 @@ export function getChannelsCommandSecretTargetIds(): Set<string> {
|
||||
}
|
||||
|
||||
export function getModelsCommandSecretTargetIds(): Set<string> {
|
||||
return toTargetIdSet(getCommandSecretTargets().models);
|
||||
return toTargetIdSet(STATIC_MODEL_TARGET_IDS);
|
||||
}
|
||||
|
||||
export function getAgentRuntimeCommandSecretTargetIds(): Set<string> {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js";
|
||||
import {
|
||||
createPluginActivationSource,
|
||||
normalizePluginsConfig,
|
||||
@@ -42,6 +43,7 @@ const EMPTY_FACADE_BOUNDARY_CONFIG: OpenClawConfig = {};
|
||||
const jitiLoaders = new Map<string, ReturnType<typeof createJiti>>();
|
||||
const loadedFacadeModules = new Map<string, unknown>();
|
||||
const loadedFacadePluginIds = new Set<string>();
|
||||
const OPENCLAW_SOURCE_EXTENSIONS_ROOT = path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions");
|
||||
let cachedBoundaryRawConfig: OpenClawConfig | undefined;
|
||||
let cachedBoundaryResolvedConfig:
|
||||
| {
|
||||
@@ -52,6 +54,40 @@ let cachedBoundaryResolvedConfig:
|
||||
autoEnabledReasons: Record<string, string[]>;
|
||||
}
|
||||
| undefined;
|
||||
let cachedManifestRegistry: readonly PluginManifestRecord[] | undefined;
|
||||
const cachedFacadeModuleLocationsByKey = new Map<
|
||||
string,
|
||||
{
|
||||
modulePath: string;
|
||||
boundaryRoot: string;
|
||||
} | null
|
||||
>();
|
||||
const cachedFacadeManifestRecordsByKey = new Map<string, FacadePluginManifestLike | null>();
|
||||
const cachedFacadePublicSurfaceAccessByKey = new Map<
|
||||
string,
|
||||
{ allowed: boolean; pluginId?: string; reason?: string }
|
||||
>();
|
||||
|
||||
type FacadePluginManifestLike = Pick<
|
||||
PluginManifestRecord,
|
||||
"id" | "origin" | "enabledByDefault" | "rootDir" | "channels"
|
||||
>;
|
||||
|
||||
function createFacadeResolutionKey(params: { dirName: string; artifactBasename: string }): string {
|
||||
const bundledPluginsDir = resolveBundledPluginsDir();
|
||||
return `${params.dirName}::${params.artifactBasename}::${bundledPluginsDir ? path.resolve(bundledPluginsDir) : "<default>"}`;
|
||||
}
|
||||
|
||||
function getFacadeManifestRegistry(): readonly PluginManifestRecord[] {
|
||||
if (cachedManifestRegistry) {
|
||||
return cachedManifestRegistry;
|
||||
}
|
||||
cachedManifestRegistry = loadPluginManifestRegistry({
|
||||
config: getFacadeBoundaryResolvedConfig().config,
|
||||
cache: true,
|
||||
}).plugins;
|
||||
return cachedManifestRegistry;
|
||||
}
|
||||
|
||||
function resolveSourceFirstPublicSurfacePath(params: {
|
||||
bundledPluginsDir?: string;
|
||||
@@ -73,8 +109,7 @@ function resolveRegistryPluginModuleLocation(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): { modulePath: string; boundaryRoot: string } | null {
|
||||
const { config } = getFacadeBoundaryResolvedConfig();
|
||||
const registry = loadPluginManifestRegistry({ config, cache: true }).plugins;
|
||||
const registry = getFacadeManifestRegistry();
|
||||
const tiers: Array<(plugin: (typeof registry)[number]) => boolean> = [
|
||||
(plugin) => plugin.id === params.dirName,
|
||||
(plugin) => path.basename(plugin.rootDir) === params.dirName,
|
||||
@@ -100,7 +135,7 @@ function resolveRegistryPluginModuleLocation(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveFacadeModuleLocation(params: {
|
||||
function resolveFacadeModuleLocationUncached(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): { modulePath: string; boundaryRoot: string } | null {
|
||||
@@ -148,6 +183,19 @@ function resolveFacadeModuleLocation(params: {
|
||||
return resolveRegistryPluginModuleLocation(params);
|
||||
}
|
||||
|
||||
function resolveFacadeModuleLocation(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): { modulePath: string; boundaryRoot: string } | null {
|
||||
const key = createFacadeResolutionKey(params);
|
||||
if (cachedFacadeModuleLocationsByKey.has(key)) {
|
||||
return cachedFacadeModuleLocationsByKey.get(key) ?? null;
|
||||
}
|
||||
const resolved = resolveFacadeModuleLocationUncached(params);
|
||||
cachedFacadeModuleLocationsByKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function getJiti(modulePath: string) {
|
||||
const tryNative =
|
||||
shouldPreferNativeJiti(modulePath) || modulePath.includes(`${path.sep}dist${path.sep}`);
|
||||
@@ -211,36 +259,80 @@ function getFacadeBoundaryResolvedConfig() {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveBundledMetadataManifestRecord(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): FacadePluginManifestLike | null {
|
||||
if (resolveBundledPluginsDir()) {
|
||||
return null;
|
||||
}
|
||||
const location = resolveFacadeModuleLocation(params);
|
||||
if (!location) {
|
||||
return null;
|
||||
}
|
||||
if (!location.modulePath.startsWith(`${OPENCLAW_SOURCE_EXTENSIONS_ROOT}${path.sep}`)) {
|
||||
return null;
|
||||
}
|
||||
const relativeToExtensions = path.relative(OPENCLAW_SOURCE_EXTENSIONS_ROOT, location.modulePath);
|
||||
const resolvedDirName = relativeToExtensions.split(path.sep)[0];
|
||||
if (!resolvedDirName) {
|
||||
return null;
|
||||
}
|
||||
const metadata = listBundledPluginMetadata({
|
||||
includeChannelConfigs: false,
|
||||
includeSyntheticChannelConfigs: false,
|
||||
}).find(
|
||||
(entry) =>
|
||||
entry.dirName === resolvedDirName ||
|
||||
entry.manifest.id === params.dirName ||
|
||||
entry.manifest.channels?.includes(params.dirName),
|
||||
);
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: metadata.manifest.id,
|
||||
origin: "bundled",
|
||||
enabledByDefault: metadata.manifest.enabledByDefault,
|
||||
rootDir: path.resolve(OPENCLAW_SOURCE_EXTENSIONS_ROOT, metadata.dirName),
|
||||
channels: [...(metadata.manifest.channels ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBundledPluginManifestRecord(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): PluginManifestRecord | null {
|
||||
const { config } = getFacadeBoundaryResolvedConfig();
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config,
|
||||
cache: true,
|
||||
}).plugins;
|
||||
const location = resolveFacadeModuleLocation(params);
|
||||
if (location) {
|
||||
const normalizedModulePath = path.resolve(location.modulePath);
|
||||
const matchedRecord = registry.find((plugin) => {
|
||||
const normalizedRootDir = path.resolve(plugin.rootDir);
|
||||
return (
|
||||
normalizedModulePath === normalizedRootDir ||
|
||||
normalizedModulePath.startsWith(`${normalizedRootDir}${path.sep}`)
|
||||
);
|
||||
});
|
||||
if (matchedRecord) {
|
||||
return matchedRecord;
|
||||
}
|
||||
}): FacadePluginManifestLike | null {
|
||||
const key = createFacadeResolutionKey(params);
|
||||
if (cachedFacadeManifestRecordsByKey.has(key)) {
|
||||
return cachedFacadeManifestRecordsByKey.get(key) ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
const metadataRecord = resolveBundledMetadataManifestRecord(params);
|
||||
if (metadataRecord) {
|
||||
cachedFacadeManifestRecordsByKey.set(key, metadataRecord);
|
||||
return metadataRecord;
|
||||
}
|
||||
|
||||
const registry = getFacadeManifestRegistry();
|
||||
const location = resolveFacadeModuleLocation(params);
|
||||
const resolved =
|
||||
(location
|
||||
? registry.find((plugin) => {
|
||||
const normalizedRootDir = path.resolve(plugin.rootDir);
|
||||
const normalizedModulePath = path.resolve(location.modulePath);
|
||||
return (
|
||||
normalizedModulePath === normalizedRootDir ||
|
||||
normalizedModulePath.startsWith(`${normalizedRootDir}${path.sep}`)
|
||||
);
|
||||
})
|
||||
: null) ??
|
||||
registry.find((plugin) => plugin.id === params.dirName) ??
|
||||
registry.find((plugin) => path.basename(plugin.rootDir) === params.dirName) ??
|
||||
registry.find((plugin) => plugin.channels.includes(params.dirName)) ??
|
||||
null
|
||||
);
|
||||
null;
|
||||
cachedFacadeManifestRecordsByKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveTrackedFacadePluginId(params: {
|
||||
@@ -254,22 +346,32 @@ function resolveBundledPluginPublicSurfaceAccess(params: {
|
||||
dirName: string;
|
||||
artifactBasename: string;
|
||||
}): { allowed: boolean; pluginId?: string; reason?: string } {
|
||||
const key = createFacadeResolutionKey(params);
|
||||
const cached = cachedFacadePublicSurfaceAccessByKey.get(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (
|
||||
params.artifactBasename === "runtime-api.js" &&
|
||||
ALWAYS_ALLOWED_RUNTIME_DIR_NAMES.has(params.dirName)
|
||||
) {
|
||||
return {
|
||||
const resolved = {
|
||||
allowed: true,
|
||||
pluginId: params.dirName,
|
||||
};
|
||||
cachedFacadePublicSurfaceAccessByKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const manifestRecord = resolveBundledPluginManifestRecord(params);
|
||||
if (!manifestRecord) {
|
||||
return {
|
||||
const resolved = {
|
||||
allowed: false,
|
||||
reason: `no bundled plugin manifest found for ${params.dirName}`,
|
||||
};
|
||||
cachedFacadePublicSurfaceAccessByKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
const { config, normalizedPluginsConfig, activationSource, autoEnabledReasons } =
|
||||
getFacadeBoundaryResolvedConfig();
|
||||
@@ -283,17 +385,21 @@ function resolveBundledPluginPublicSurfaceAccess(params: {
|
||||
autoEnabledReason: autoEnabledReasons[manifestRecord.id]?.[0],
|
||||
});
|
||||
if (activationState.enabled) {
|
||||
return {
|
||||
const resolved = {
|
||||
allowed: true,
|
||||
pluginId: manifestRecord.id,
|
||||
};
|
||||
cachedFacadePublicSurfaceAccessByKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return {
|
||||
const resolved = {
|
||||
allowed: false,
|
||||
pluginId: manifestRecord.id,
|
||||
reason: activationState.reason ?? "plugin runtime is not activated",
|
||||
};
|
||||
cachedFacadePublicSurfaceAccessByKey.set(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function createLazyFacadeValueLoader<T>(load: () => T): () => T {
|
||||
@@ -464,4 +570,8 @@ export function resetFacadeRuntimeStateForTest(): void {
|
||||
jitiLoaders.clear();
|
||||
cachedBoundaryRawConfig = undefined;
|
||||
cachedBoundaryResolvedConfig = undefined;
|
||||
cachedManifestRegistry = undefined;
|
||||
cachedFacadeModuleLocationsByKey.clear();
|
||||
cachedFacadeManifestRecordsByKey.clear();
|
||||
cachedFacadePublicSurfaceAccessByKey.clear();
|
||||
}
|
||||
|
||||
@@ -112,13 +112,17 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
it("routes changed commands source allowlist files to sibling light tests", () => {
|
||||
const plans = buildVitestRunPlans(["--changed", "origin/main"], process.cwd(), () => [
|
||||
"src/commands/status-overview-values.ts",
|
||||
"src/commands/gateway-status/helpers.ts",
|
||||
]);
|
||||
|
||||
expect(plans).toEqual([
|
||||
{
|
||||
config: "vitest.commands-light.config.ts",
|
||||
forwardedArgs: [],
|
||||
includePatterns: ["src/commands/status-overview-values.test.ts"],
|
||||
includePatterns: [
|
||||
"src/commands/status-overview-values.test.ts",
|
||||
"src/commands/gateway-status/helpers.test.ts",
|
||||
],
|
||||
watchMode: false,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -34,6 +34,10 @@ describe("light vitest path routing", () => {
|
||||
expect(resolveCommandsLightIncludePattern("src/commands/text-format.test.ts")).toBe(
|
||||
"src/commands/text-format.test.ts",
|
||||
);
|
||||
expect(isCommandsLightTarget("src/commands/gateway-status/helpers.ts")).toBe(true);
|
||||
expect(resolveCommandsLightIncludePattern("src/commands/gateway-status/helpers.ts")).toBe(
|
||||
"src/commands/gateway-status/helpers.test.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps non-allowlisted commands files off the light lane", () => {
|
||||
|
||||
@@ -11,10 +11,26 @@ const commandsLightEntries = [
|
||||
source: "src/commands/doctor-gateway-auth-token.ts",
|
||||
test: "src/commands/doctor-gateway-auth-token.test.ts",
|
||||
},
|
||||
{
|
||||
source: "src/commands/gateway-status/helpers.ts",
|
||||
test: "src/commands/gateway-status/helpers.test.ts",
|
||||
},
|
||||
{
|
||||
source: "src/commands/sandbox-formatters.ts",
|
||||
test: "src/commands/sandbox-formatters.test.ts",
|
||||
},
|
||||
{
|
||||
source: "src/commands/status-json-command.ts",
|
||||
test: "src/commands/status-json-command.test.ts",
|
||||
},
|
||||
{
|
||||
source: "src/commands/status-json-payload.ts",
|
||||
test: "src/commands/status-json-payload.test.ts",
|
||||
},
|
||||
{
|
||||
source: "src/commands/status-json-runtime.ts",
|
||||
test: "src/commands/status-json-runtime.test.ts",
|
||||
},
|
||||
{
|
||||
source: "src/commands/status-overview-rows.ts",
|
||||
test: "src/commands/status-overview-rows.test.ts",
|
||||
|
||||
Reference in New Issue
Block a user