fix(plugin-sdk): keep test contracts publishable

This commit is contained in:
Peter Steinberger
2026-04-30 01:00:15 +01:00
parent 59982c2aa5
commit 172bc9d043
4 changed files with 92 additions and 1 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.
- CLI/browser: preserve parent flags while lazy-loading browser subcommands, so `openclaw browser --json open` and `openclaw browser --json tabs` keep machine-readable output after reparsing. Fixes #74574. Thanks @devintegeritsm.
- Plugins/runtime-deps: add `openclaw plugins deps` inspection and repair with script-free package-manager defaults shared across plugin installers, so operators can repair missing bundled runtime deps without corrupting JSON output or blocking unrelated conflict-free deps. Thanks @vincentkoc.
- Agents/output: strip internal `[tool calls omitted]` replay placeholders from user-facing replies while preserving visible reply whitespace. Fixes #74573. Thanks @blaspat.

View File

@@ -3,6 +3,7 @@
import { execFileSync, execSync } from "node:child_process";
import {
existsSync,
lstatSync,
mkdtempSync,
mkdirSync,
realpathSync,
@@ -116,6 +117,15 @@ const appcastPath = resolve("appcast.xml");
const laneBuildMin = 1_000_000_000;
const laneFloorAdoptionDateKey = 20260227;
const SAFE_UNIX_SMOKE_PATH = "/usr/bin:/bin";
export const MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES = 2 * 1024 * 1024;
export const CRITICAL_PLUGIN_SDK_SIZE_CHECK_SPECIFIERS = [
"openclaw/plugin-sdk/agent-runtime-test-contracts",
"openclaw/plugin-sdk/plugin-test-contracts",
"openclaw/plugin-sdk/provider-test-contracts",
] as const;
export const CRITICAL_PLUGIN_SDK_IMPORT_SMOKE_SPECIFIERS = [
"openclaw/plugin-sdk/plugin-test-contracts",
] as const;
export const PACKED_CLI_SMOKE_COMMANDS = [
["--help"],
["onboard", "--help"],
@@ -843,6 +853,44 @@ async function checkPluginSdkExports() {
}
}
export function collectCriticalPluginSdkEntrypointSizeErrors(rootDir = process.cwd()): string[] {
const errors: string[] = [];
for (const specifier of CRITICAL_PLUGIN_SDK_SIZE_CHECK_SPECIFIERS) {
const subpath = specifier.slice("openclaw/plugin-sdk/".length);
const relativePath = `dist/plugin-sdk/${subpath}.js`;
const filePath = resolve(rootDir, relativePath);
if (!existsSync(filePath)) {
errors.push(`${relativePath} is missing.`);
continue;
}
const stat = lstatSync(filePath);
if (!stat.isFile()) {
errors.push(`${relativePath} is not a file.`);
continue;
}
if (stat.size > MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES) {
errors.push(
`${relativePath} is ${stat.size} bytes, exceeding ${MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES} bytes. Keep public SDK test-contract entrypoints lazy and avoid bundling compiler/runtime internals.`,
);
}
}
return errors;
}
function runCriticalPluginSdkEntrypointImportSmoke() {
const script = [
`const specifiers = ${JSON.stringify(CRITICAL_PLUGIN_SDK_IMPORT_SMOKE_SPECIFIERS)};`,
`const importModule = new Function("specifier", "return imp" + "ort(specifier)");`,
"for (const specifier of specifiers) {",
" await importModule(specifier);",
"}",
].join("\n");
execFileSync(process.execPath, ["--input-type=module", "--eval", script], {
cwd: process.cwd(),
stdio: "inherit",
});
}
async function main() {
checkAppcastSparkleVersions();
checkCliBootstrapExternalImports({
@@ -851,6 +899,15 @@ async function main() {
},
});
await checkPluginSdkExports();
const criticalPluginSdkEntrypointErrors = collectCriticalPluginSdkEntrypointSizeErrors();
if (criticalPluginSdkEntrypointErrors.length > 0) {
console.error("release-check: critical plugin-sdk entrypoint validation failed:");
for (const error of criticalPluginSdkEntrypointErrors) {
console.error(` - ${error}`);
}
process.exit(1);
}
runCriticalPluginSdkEntrypointImportSmoke();
checkBundledExtensionMetadata();
await writePackageDistInventory(process.cwd());

View File

@@ -1,7 +1,13 @@
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import ts from "typescript";
const nodeRequire = createRequire(import.meta.url);
function loadTypeScript(): typeof import("typescript") {
return nodeRequire("typescript") as typeof import("typescript");
}
const JITI_EXTENSIONS = [
".ts",
@@ -79,6 +85,7 @@ function resolveLocalModulePath(filePath: string, specifier: string): string | n
}
function collectSourceModuleRefs(filePath: string): SourceModuleRef[] {
const ts = loadTypeScript();
const sourceText = readFileSync(filePath, "utf8");
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
const refs: SourceModuleRef[] = [];

View File

@@ -11,6 +11,7 @@ import {
collectAppcastSparkleVersionErrors,
collectBundledExtensionManifestErrors,
collectBundledPluginRootRuntimeMirrorErrors,
collectCriticalPluginSdkEntrypointSizeErrors,
collectDeclaredRootRuntimeDependencyMetadataErrors,
collectForbiddenPackContentPaths,
collectInstalledBundledPluginRuntimeDepErrors,
@@ -21,6 +22,7 @@ import {
collectPackUnpackedSizeErrors,
createPackedCliSmokeEnv,
createPackedBundledPluginPostinstallEnv,
MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES,
PACKED_CLI_SMOKE_COMMANDS,
packageNameFromSpecifier,
resolveMissingPackBuildHint,
@@ -729,6 +731,30 @@ describe("collectPackUnpackedSizeErrors", () => {
});
});
describe("collectCriticalPluginSdkEntrypointSizeErrors", () => {
it("flags oversized plugin SDK test-contract entrypoints before publish", () => {
const root = mkdtempSync(join(tmpdir(), "release-check-critical-sdk-"));
try {
const pluginSdkDir = join(root, "dist", "plugin-sdk");
mkdirSync(pluginSdkDir, { recursive: true });
writeFileSync(join(pluginSdkDir, "agent-runtime-test-contracts.js"), "export {};\n");
writeFileSync(join(pluginSdkDir, "provider-test-contracts.js"), "export {};\n");
writeFileSync(
join(pluginSdkDir, "plugin-test-contracts.js"),
"x".repeat(MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES + 1),
);
expect(collectCriticalPluginSdkEntrypointSizeErrors(root)).toEqual([
`dist/plugin-sdk/plugin-test-contracts.js is ${
MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES + 1
} bytes, exceeding ${MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES} bytes. Keep public SDK test-contract entrypoints lazy and avoid bundling compiler/runtime internals.`,
]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});
describe("createPackedBundledPluginPostinstallEnv", () => {
it("keeps packed postinstall on the lazy bundled dependency path", () => {
expect(createPackedBundledPluginPostinstallEnv({ PATH: "/usr/bin" })).toEqual({