fix(plugins): gate package test api aliases

This commit is contained in:
Vincent Koc
2026-05-03 21:39:30 -07:00
parent 796d4ab43d
commit 19f948af2e
3 changed files with 50 additions and 6 deletions

View File

@@ -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.

View File

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

View File

@@ -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<string, string> = {};
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,