mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(plugin-sdk): add export verification tests and release guard (#27569)
This commit is contained in:
committed by
Peter Steinberger
parent
2438fde6d9
commit
61d14e8a8a
86
scripts/check-plugin-sdk-exports.mjs
Executable file
86
scripts/check-plugin-sdk-exports.mjs
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Verifies that critical plugin-sdk exports are present in the compiled dist output.
|
||||
* Regression guard for #27569 where isDangerousNameMatchingEnabled was missing
|
||||
* from the compiled output, breaking channel extension plugins at runtime.
|
||||
*
|
||||
* Run after `pnpm build` to catch missing exports before release.
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const distFile = resolve(__dirname, "..", "dist", "plugin-sdk", "index.js");
|
||||
|
||||
if (!existsSync(distFile)) {
|
||||
console.error("ERROR: dist/plugin-sdk/index.js not found. Run `pnpm build` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const content = readFileSync(distFile, "utf-8");
|
||||
|
||||
// Extract the final export statement from the compiled output.
|
||||
// tsdown/rolldown emits a single `export { ... }` at the end of the file.
|
||||
const exportMatch = content.match(/export\s*\{([^}]+)\}\s*;?\s*$/);
|
||||
if (!exportMatch) {
|
||||
console.error("ERROR: Could not find export statement in dist/plugin-sdk/index.js");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const exportedNames = exportMatch[1]
|
||||
.split(",")
|
||||
.map((s) => {
|
||||
// Handle `foo as bar` aliases — the exported name is the `bar` part
|
||||
const parts = s.trim().split(/\s+as\s+/);
|
||||
return (parts[parts.length - 1] || "").trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const exportSet = new Set(exportedNames);
|
||||
|
||||
// Critical functions that channel extension plugins import from openclaw/plugin-sdk.
|
||||
// If any of these are missing, plugins will fail at runtime with:
|
||||
// TypeError: (0 , _pluginSdk.<name>) is not a function
|
||||
const requiredExports = [
|
||||
"isDangerousNameMatchingEnabled",
|
||||
"createAccountListHelpers",
|
||||
"buildAgentMediaPayload",
|
||||
"createReplyPrefixOptions",
|
||||
"createTypingCallbacks",
|
||||
"logInboundDrop",
|
||||
"logTypingFailure",
|
||||
"buildPendingHistoryContextFromMap",
|
||||
"clearHistoryEntriesIfEnabled",
|
||||
"recordPendingHistoryEntryIfEnabled",
|
||||
"resolveControlCommandGate",
|
||||
"resolveDmGroupAccessWithLists",
|
||||
"resolveAllowlistProviderRuntimeGroupPolicy",
|
||||
"resolveDefaultGroupPolicy",
|
||||
"resolveChannelMediaMaxBytes",
|
||||
"warnMissingProviderGroupPolicyFallbackOnce",
|
||||
"emptyPluginConfigSchema",
|
||||
"normalizePluginHttpPath",
|
||||
"registerPluginHttpRoute",
|
||||
"DEFAULT_ACCOUNT_ID",
|
||||
"DEFAULT_GROUP_HISTORY_LIMIT",
|
||||
];
|
||||
|
||||
let missing = 0;
|
||||
for (const name of requiredExports) {
|
||||
if (!exportSet.has(name)) {
|
||||
console.error(`MISSING EXPORT: ${name}`);
|
||||
missing += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (missing > 0) {
|
||||
console.error(`\nERROR: ${missing} required export(s) missing from dist/plugin-sdk/index.js.`);
|
||||
console.error("This will break channel extension plugins at runtime.");
|
||||
console.error("Check src/plugin-sdk/index.ts and rebuild.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`OK: All ${requiredExports.length} required plugin-sdk exports verified.`);
|
||||
@@ -164,6 +164,59 @@ function checkAppcastSparkleVersions() {
|
||||
console.error("release-check: appcast sparkle version validation failed:");
|
||||
for (const error of errors) {
|
||||
console.error(` - ${error}`);
|
||||
|
||||
// Critical functions that channel extension plugins import from openclaw/plugin-sdk.
|
||||
// If any are missing from the compiled output, plugins crash at runtime (#27569).
|
||||
const requiredPluginSdkExports = [
|
||||
"isDangerousNameMatchingEnabled",
|
||||
"createAccountListHelpers",
|
||||
"buildAgentMediaPayload",
|
||||
"createReplyPrefixOptions",
|
||||
"createTypingCallbacks",
|
||||
"logInboundDrop",
|
||||
"logTypingFailure",
|
||||
"resolveControlCommandGate",
|
||||
"resolveDmGroupAccessWithLists",
|
||||
"resolveAllowlistProviderRuntimeGroupPolicy",
|
||||
"resolveDefaultGroupPolicy",
|
||||
"resolveChannelMediaMaxBytes",
|
||||
"emptyPluginConfigSchema",
|
||||
"normalizePluginHttpPath",
|
||||
"registerPluginHttpRoute",
|
||||
"DEFAULT_ACCOUNT_ID",
|
||||
"DEFAULT_GROUP_HISTORY_LIMIT",
|
||||
];
|
||||
|
||||
function checkPluginSdkExports() {
|
||||
const distPath = resolve("dist", "plugin-sdk", "index.js");
|
||||
let content: string;
|
||||
try {
|
||||
content = readFileSync(distPath, "utf8");
|
||||
} catch {
|
||||
console.error("release-check: dist/plugin-sdk/index.js not found (build missing?).");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const exportMatch = content.match(/export\s*\{([^}]+)\}\s*;?\s*$/);
|
||||
if (!exportMatch) {
|
||||
console.error("release-check: could not find export statement in dist/plugin-sdk/index.js.");
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const exportedNames = new Set(
|
||||
exportMatch[1].split(",").map((s) => {
|
||||
const parts = s.trim().split(/\s+as\s+/);
|
||||
return (parts[parts.length - 1] || "").trim();
|
||||
}),
|
||||
);
|
||||
|
||||
const missingExports = requiredPluginSdkExports.filter((name) => !exportedNames.has(name));
|
||||
if (missingExports.length > 0) {
|
||||
console.error("release-check: missing critical plugin-sdk exports (#27569):");
|
||||
for (const name of missingExports) {
|
||||
console.error(` - ${name}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -172,6 +225,7 @@ function checkAppcastSparkleVersions() {
|
||||
function main() {
|
||||
checkPluginVersions();
|
||||
checkAppcastSparkleVersions();
|
||||
checkPluginSdkExports();
|
||||
|
||||
const results = runPackDry();
|
||||
const files = results.flatMap((entry) => entry.files ?? []);
|
||||
|
||||
@@ -46,4 +46,62 @@ describe("plugin-sdk exports", () => {
|
||||
expect(Object.prototype.hasOwnProperty.call(sdk, key)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify critical functions that extensions depend on are exported and callable.
|
||||
// Regression guard for #27569 where isDangerousNameMatchingEnabled was missing
|
||||
// from the compiled output, breaking mattermost/googlechat/msteams/irc plugins.
|
||||
it("exports critical functions used by channel extensions", () => {
|
||||
const requiredFunctions = [
|
||||
"isDangerousNameMatchingEnabled",
|
||||
"createAccountListHelpers",
|
||||
"buildAgentMediaPayload",
|
||||
"createReplyPrefixOptions",
|
||||
"createTypingCallbacks",
|
||||
"logInboundDrop",
|
||||
"logTypingFailure",
|
||||
"buildPendingHistoryContextFromMap",
|
||||
"clearHistoryEntriesIfEnabled",
|
||||
"recordPendingHistoryEntryIfEnabled",
|
||||
"resolveControlCommandGate",
|
||||
"resolveDmGroupAccessWithLists",
|
||||
"resolveAllowlistProviderRuntimeGroupPolicy",
|
||||
"resolveDefaultGroupPolicy",
|
||||
"resolveChannelMediaMaxBytes",
|
||||
"warnMissingProviderGroupPolicyFallbackOnce",
|
||||
"createDedupeCache",
|
||||
"formatInboundFromLabel",
|
||||
"resolveRuntimeGroupPolicy",
|
||||
"emptyPluginConfigSchema",
|
||||
"normalizePluginHttpPath",
|
||||
"registerPluginHttpRoute",
|
||||
"buildBaseAccountStatusSnapshot",
|
||||
"buildBaseChannelStatusSummary",
|
||||
"buildTokenChannelStatusSummary",
|
||||
"collectStatusIssuesFromLastError",
|
||||
"createDefaultChannelRuntimeState",
|
||||
"resolveChannelEntryMatch",
|
||||
"resolveChannelEntryMatchWithFallback",
|
||||
"normalizeChannelSlug",
|
||||
"buildChannelKeyCandidates",
|
||||
];
|
||||
|
||||
for (const key of requiredFunctions) {
|
||||
expect(sdk).toHaveProperty(key);
|
||||
expect(typeof (sdk as Record<string, unknown>)[key]).toBe("function");
|
||||
}
|
||||
});
|
||||
|
||||
// Verify critical constants that extensions depend on are exported.
|
||||
it("exports critical constants used by channel extensions", () => {
|
||||
const requiredConstants = [
|
||||
"DEFAULT_GROUP_HISTORY_LIMIT",
|
||||
"DEFAULT_ACCOUNT_ID",
|
||||
"SILENT_REPLY_TOKEN",
|
||||
"PAIRING_APPROVED_MESSAGE",
|
||||
];
|
||||
|
||||
for (const key of requiredConstants) {
|
||||
expect(sdk).toHaveProperty(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user