From 8a33a8d60770e8b0799b917177f934568200b44d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 16:48:32 +0100 Subject: [PATCH] perf(test): trim runtime lookups and add changed bench --- docs/help/testing.md | 2 + docs/reference/test.md | 2 + package.json | 1 + scripts/bench-test-changed.mjs | 187 ++++++++++++++++++ src/cli/command-secret-targets.import.test.ts | 3 + src/cli/command-secret-targets.test.ts | 14 ++ src/cli/command-secret-targets.ts | 90 +++++---- src/plugin-sdk/facade-runtime.ts | 168 +++++++++++++--- test/scripts/test-projects.test.ts | 6 +- test/vitest-light-paths.test.ts | 4 + vitest.commands-light-paths.mjs | 16 ++ 11 files changed, 430 insertions(+), 63 deletions(-) create mode 100644 scripts/bench-test-changed.mjs diff --git a/docs/help/testing.md b/docs/help/testing.md index f7dda0facb..80d86cf1ee 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -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 ` 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. diff --git a/docs/reference/test.md b/docs/reference/test.md index 7633a04cf1..9dc03790b6 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -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 ` 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`. diff --git a/package.json b/package.json index ae15e8a76d..8734224bb0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/bench-test-changed.mjs b/scripts/bench-test-changed.mjs new file mode 100644 index 0000000000..e02fffa3ed --- /dev/null +++ b/scripts/bench-test-changed.mjs @@ -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" + }`, +); diff --git a/src/cli/command-secret-targets.import.test.ts b/src/cli/command-secret-targets.import.test.ts index d9e1e0c1b3..a1e02f5d21 100644 --- a/src/cli/command-secret-targets.import.test.ts +++ b/src/cli/command-secret-targets.import.test.ts @@ -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); diff --git a/src/cli/command-secret-targets.test.ts b/src/cli/command-secret-targets.test.ts index 4d8694607d..9f286e4fed 100644 --- a/src/cli/command-secret-targets.test.ts +++ b/src/cli/command-secret-targets.test.ts @@ -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); diff --git a/src/cli/command-secret-targets.ts b/src/cli/command-secret-targets.ts index 4d382a67ef..9792b2565f 100644 --- a/src/cli/command-secret-targets.ts +++ b/src/cli/command-secret-targets.ts @@ -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 { - return toTargetIdSet(getCommandSecretTargets().qrRemote); + return toTargetIdSet(STATIC_QR_REMOTE_TARGET_IDS); } export function getChannelsCommandSecretTargetIds(): Set { @@ -135,7 +159,7 @@ export function getChannelsCommandSecretTargetIds(): Set { } export function getModelsCommandSecretTargetIds(): Set { - return toTargetIdSet(getCommandSecretTargets().models); + return toTargetIdSet(STATIC_MODEL_TARGET_IDS); } export function getAgentRuntimeCommandSecretTargetIds(): Set { diff --git a/src/plugin-sdk/facade-runtime.ts b/src/plugin-sdk/facade-runtime.ts index 8cab98e8f6..a156ae56a9 100644 --- a/src/plugin-sdk/facade-runtime.ts +++ b/src/plugin-sdk/facade-runtime.ts @@ -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>(); const loadedFacadeModules = new Map(); const loadedFacadePluginIds = new Set(); +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; } | undefined; +let cachedManifestRegistry: readonly PluginManifestRecord[] | undefined; +const cachedFacadeModuleLocationsByKey = new Map< + string, + { + modulePath: string; + boundaryRoot: string; + } | null +>(); +const cachedFacadeManifestRecordsByKey = new Map(); +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) : ""}`; +} + +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(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(); } diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index c0fe5489a2..54872bccbc 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -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, }, ]); diff --git a/test/vitest-light-paths.test.ts b/test/vitest-light-paths.test.ts index ed01eb64e6..c28570d69c 100644 --- a/test/vitest-light-paths.test.ts +++ b/test/vitest-light-paths.test.ts @@ -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", () => { diff --git a/vitest.commands-light-paths.mjs b/vitest.commands-light-paths.mjs index 6a1b64a233..5d387f5531 100644 --- a/vitest.commands-light-paths.mjs +++ b/vitest.commands-light-paths.mjs @@ -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",