From 19f948af2efde664c8e93e194cb2751a5843ab1b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 21:39:30 -0700 Subject: [PATCH] fix(plugins): gate package test api aliases --- CHANGELOG.md | 1 + src/plugins/sdk-alias.test.ts | 23 +++++++++++++++++++++++ src/plugins/sdk-alias.ts | 32 ++++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34261c6238b..ef3e9b872b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/loader: keep bundled plugin package `test-api.js` aliases behind private QA mode, so source transforms do not expose test-only public surfaces during normal plugin loading. Thanks @vincentkoc. - Gateway/startup: start cron and record the post-ready memory trace even when deferred maintenance timers fail after readiness, so a non-fatal timer setup issue does not silently leave scheduled jobs idle. Thanks @vincentkoc. - Agents/session status: keep semantic `session_status({ sessionKey: "current" })` on the live run session even before that run has a persisted session-store entry, instead of falling back to the sandbox policy key. Thanks @vincentkoc. - QA/Slack: resolve bundled official plugin public-surface package aliases during source-mode QA runs, so release Slack live validation can load `@openclaw/slack/api.js` without workspace symlinks. Thanks @vincentkoc. diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index f828ceaaf32..910f5111f2b 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -211,12 +211,16 @@ function createBundledPluginPackagePublicSurfaceAliasFixture() { ); const sourceApiPath = path.join(extensionRoot, "api.ts"); const sourceRuntimeApiPath = path.join(extensionRoot, "runtime-api.ts"); + const sourceTestApiPath = path.join(extensionRoot, "test-api.ts"); const distApiPath = path.join(distExtensionRoot, "api.js"); const distRuntimeApiPath = path.join(distExtensionRoot, "runtime-api.js"); + const distTestApiPath = path.join(distExtensionRoot, "test-api.js"); fs.writeFileSync(sourceApiPath, "export const slackApi = 'source';\n", "utf-8"); fs.writeFileSync(sourceRuntimeApiPath, "export const slackRuntimeApi = 'source';\n", "utf-8"); + fs.writeFileSync(sourceTestApiPath, "export const slackTestApi = 'source';\n", "utf-8"); fs.writeFileSync(distApiPath, "export const slackApi = 'dist';\n", "utf-8"); fs.writeFileSync(distRuntimeApiPath, "export const slackRuntimeApi = 'dist';\n", "utf-8"); + fs.writeFileSync(distTestApiPath, "export const slackTestApi = 'dist';\n", "utf-8"); fs.writeFileSync( path.join(extensionRoot, "internal.ts"), "export const internal = true;\n", @@ -226,8 +230,10 @@ function createBundledPluginPackagePublicSurfaceAliasFixture() { ...fixture, distApiPath, distRuntimeApiPath, + distTestApiPath, sourceApiPath, sourceRuntimeApiPath, + sourceTestApiPath, }; } @@ -828,9 +834,26 @@ describe("plugin sdk alias helpers", () => { expect(fs.realpathSync(aliases["@openclaw/slack/runtime-api.js"] ?? "")).toBe( fs.realpathSync(sourceRuntimeApiPath), ); + expect(aliases["@openclaw/slack/test-api.js"]).toBeUndefined(); expect(aliases["@openclaw/slack/internal.js"]).toBeUndefined(); }); + it("aliases bundled plugin package test surfaces only in private QA mode", () => { + const { fixture, sourceTestApiPath } = createBundledPluginPackagePublicSurfaceAliasFixture(); + const sourcePluginEntry = writePluginEntry( + fixture.root, + bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"), + ); + + const aliases = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1", NODE_ENV: undefined }, () => + buildPluginLoaderAliasMap(sourcePluginEntry), + ); + + expect(fs.realpathSync(aliases["@openclaw/slack/test-api.js"] ?? "")).toBe( + fs.realpathSync(sourceTestApiPath), + ); + }); + it("aliases bundled plugin package public surfaces to dist when dist resolution is requested", () => { const { fixture, distApiPath, distRuntimeApiPath } = createBundledPluginPackagePublicSurfaceAliasFixture(); diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index b2410a44882..94e11c47140 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -331,10 +331,23 @@ function readBundledPluginPackageName(packageJsonPath: string): string | null { } } -function listBundledPluginPublicSurfaceSourceBasenames(extensionSourceRoot: string): string[] { +function isBundledPluginPublicSurfaceSourceBasename(params: { + basename: string; + includePrivateQa: boolean; +}): boolean { + if (params.basename === "test-api") { + return params.includePrivateQa; + } + return BUNDLED_PLUGIN_PUBLIC_SURFACE_SOURCE_PATTERN.test(params.basename); +} + +function listBundledPluginPublicSurfaceSourceBasenames(params: { + extensionSourceRoot: string; + includePrivateQa: boolean; +}): string[] { try { return fs - .readdirSync(extensionSourceRoot, { withFileTypes: true }) + .readdirSync(params.extensionSourceRoot, { withFileTypes: true }) .filter((entry) => entry.isFile()) .map((entry) => entry.name) .flatMap((fileName) => { @@ -345,7 +358,12 @@ function listBundledPluginPublicSurfaceSourceBasenames(extensionSourceRoot: stri return []; } const basename = fileName.slice(0, -ext.length); - return BUNDLED_PLUGIN_PUBLIC_SURFACE_SOURCE_PATTERN.test(basename) ? [basename] : []; + return isBundledPluginPublicSurfaceSourceBasename({ + basename, + includePrivateQa: params.includePrivateQa, + }) + ? [basename] + : []; }) .toSorted(); } catch { @@ -410,6 +428,7 @@ function resolveBundledPluginPackagePublicSurfaceAliasMap(params: { isProduction: process.env.NODE_ENV === "production", pluginSdkResolution: params.pluginSdkResolution, }); + const includePrivateQa = shouldIncludePrivateLocalOnlyPluginSdkSubpaths(); const aliasMap: Record = {}; for (const entry of extensionDirs) { if (!entry.isDirectory()) { @@ -422,9 +441,10 @@ function resolveBundledPluginPackagePublicSurfaceAliasMap(params: { if (!packageName) { continue; } - for (const basename of listBundledPluginPublicSurfaceSourceBasenames( - path.join(extensionsRoot, dirName), - )) { + for (const basename of listBundledPluginPublicSurfaceSourceBasenames({ + extensionSourceRoot: path.join(extensionsRoot, dirName), + includePrivateQa, + })) { const target = resolveBundledPluginPublicSurfaceAliasTarget({ packageRoot, dirName,