test: trim plugin-sdk import-heavy startup

This commit is contained in:
Peter Steinberger
2026-03-22 07:21:47 +00:00
parent 537115bbdc
commit 94ec0d6aeb
5 changed files with 200 additions and 202 deletions

View File

@@ -5,10 +5,8 @@ export const pluginSdkEntrypoints = [...pluginSdkEntryList];
export const pluginSdkSubpaths = pluginSdkEntrypoints.filter((entry) => entry !== "index");
/** Map every SDK entrypoint name to its source file path inside the repo. */
export function buildPluginSdkEntrySources() {
return Object.fromEntries(
pluginSdkEntrypoints.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]),
);
export function buildPluginSdkEntrySources(entries: readonly string[] = pluginSdkEntrypoints) {
return Object.fromEntries(entries.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]));
}
/** List the public package specifiers that should resolve to plugin SDK entrypoints. */

View File

@@ -12,58 +12,55 @@ const bundledRepresentativeEntrypoints = [
"matrix-runtime-heavy",
"windows-spawn",
] as const;
function buildBundledCoverageEntrySources() {
const allEntrySources = buildPluginSdkEntrySources();
return Object.fromEntries(
bundledRepresentativeEntrypoints.map((entry) => [entry, allEntrySources[entry]]),
);
}
const bundledCoverageEntrySources = buildPluginSdkEntrySources(bundledRepresentativeEntrypoints);
describe("plugin-sdk bundled exports", () => {
it("emits importable bundled subpath entries", { timeout: 120_000 }, async () => {
const bundleTempRoot = path.join(process.cwd(), ".tmp");
const bundleTempRoot = path.join(
process.cwd(),
"node_modules",
".cache",
"openclaw-plugin-sdk-build",
);
await fs.mkdir(bundleTempRoot, { recursive: true });
const outDir = await fs.mkdtemp(path.join(bundleTempRoot, "openclaw-plugin-sdk-build-"));
const outDir = path.join(bundleTempRoot, "bundle");
await fs.rm(outDir, { recursive: true, force: true });
await fs.mkdir(outDir, { recursive: true });
try {
const { build } = await import(tsdownModuleUrl);
await build({
clean: false,
config: false,
dts: false,
// Full plugin-sdk coverage belongs to `pnpm build`, package contract
// guardrails, and `subpaths.test.ts`. This file only keeps the expensive
// bundler path honest across representative entrypoint families.
entry: buildBundledCoverageEntrySources(),
env: { NODE_ENV: "production" },
fixedExtension: false,
logLevel: "error",
outDir,
platform: "node",
});
const { build } = await import(tsdownModuleUrl);
await build({
clean: false,
config: false,
dts: false,
// Full plugin-sdk coverage belongs to `pnpm build`, package contract
// guardrails, and `subpaths.test.ts`. This file only keeps the expensive
// bundler path honest across representative entrypoint families.
entry: bundledCoverageEntrySources,
env: { NODE_ENV: "production" },
fixedExtension: false,
logLevel: "error",
outDir,
platform: "node",
});
expect(pluginSdkEntrypoints.length).toBeGreaterThan(bundledRepresentativeEntrypoints.length);
await Promise.all(
bundledRepresentativeEntrypoints.map(async (entry) => {
await expect(fs.stat(path.join(outDir, `${entry}.js`))).resolves.toBeTruthy();
}),
);
expect(pluginSdkEntrypoints.length).toBeGreaterThan(bundledRepresentativeEntrypoints.length);
await Promise.all(
bundledRepresentativeEntrypoints.map(async (entry) => {
await expect(fs.stat(path.join(outDir, `${entry}.js`))).resolves.toBeTruthy();
}),
);
// Export list and package-specifier coverage already live in
// package-contract-guardrails.test.ts and subpaths.test.ts. Keep this file
// focused on the expensive part: can tsdown emit working bundle artifacts?
const importResults = await Promise.all(
bundledRepresentativeEntrypoints.map(async (entry) => [
entry,
typeof (await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href)),
]),
);
expect(Object.fromEntries(importResults)).toEqual(
Object.fromEntries(bundledRepresentativeEntrypoints.map((entry) => [entry, "object"])),
);
} finally {
await fs.rm(outDir, { recursive: true, force: true });
}
// Export list and package-specifier coverage already live in
// package-contract-guardrails.test.ts and subpaths.test.ts. Keep this file
// focused on the expensive part: can tsdown emit working bundle artifacts?
const importResults = await Promise.all(
bundledRepresentativeEntrypoints.map(async (entry) => [
entry,
typeof (await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href)),
]),
);
expect(Object.fromEntries(importResults)).toEqual(
Object.fromEntries(bundledRepresentativeEntrypoints.map((entry) => [entry, "object"])),
);
});
});

View File

@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import type {
BaseProbeResult as ContractBaseProbeResult,
BaseTokenResolution as ContractBaseTokenResolution,
@@ -46,11 +47,11 @@ import { pluginSdkSubpaths } from "./entrypoints.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PLUGIN_SDK_DIR = resolve(ROOT_DIR, "plugin-sdk");
const requireFromHere = createRequire(import.meta.url);
const sourceCache = new Map<string, string>();
const representativeRuntimeSmokeSubpaths = [
"channel-runtime",
"conversation-runtime",
"core",
"discord",
"provider-auth",
"provider-setup",
@@ -58,7 +59,8 @@ const representativeRuntimeSmokeSubpaths = [
"webhook-ingress",
] as const;
const importPluginSdkSubpath = (specifier: string) => import(/* @vite-ignore */ specifier);
const importResolvedPluginSdkSubpath = async (specifier: string) =>
import(pathToFileURL(requireFromHere.resolve(specifier)).href);
function readPluginSdkSource(subpath: string): string {
const file = resolve(PLUGIN_SDK_DIR, `${subpath}.ts`);
@@ -100,18 +102,25 @@ function sourceMentionsIdentifier(source: string, name: string): boolean {
function expectSourceMentions(subpath: string, names: readonly string[]) {
const source = readPluginSdkSource(subpath);
for (const name of names) {
expect(sourceMentionsIdentifier(source, name), `${subpath} should mention ${name}`).toBe(true);
}
const missing = names.filter((name) => !sourceMentionsIdentifier(source, name));
expect(missing, `${subpath} missing exports`).toEqual([]);
}
function expectSourceOmits(subpath: string, names: readonly string[]) {
const source = readPluginSdkSource(subpath);
for (const name of names) {
expect(sourceMentionsIdentifier(source, name), `${subpath} should not mention ${name}`).toBe(
false,
);
}
const present = names.filter((name) => sourceMentionsIdentifier(source, name));
expect(present, `${subpath} leaked exports`).toEqual([]);
}
function expectSourceContract(
subpath: string,
params: { mentions?: readonly string[]; omits?: readonly string[] },
) {
const source = readPluginSdkSource(subpath);
const missing = (params.mentions ?? []).filter((name) => !sourceMentionsIdentifier(source, name));
const present = (params.omits ?? []).filter((name) => sourceMentionsIdentifier(source, name));
expect(missing, `${subpath} missing exports`).toEqual([]);
expect(present, `${subpath} leaked exports`).toEqual([]);
}
describe("plugin-sdk subpath exports", () => {
@@ -154,16 +163,15 @@ describe("plugin-sdk subpath exports", () => {
]);
});
it("re-exports the canonical plugin entry helper from core", async () => {
const [coreSdk, pluginEntrySdk] = await Promise.all([
importPluginSdkSubpath("openclaw/plugin-sdk/core"),
importPluginSdkSubpath("openclaw/plugin-sdk/plugin-entry"),
]);
expect(coreSdk.definePluginEntry).toBe(pluginEntrySdk.definePluginEntry);
});
it("keeps generic helper subpaths aligned", () => {
expectSourceMentions("routing", ["buildAgentSessionKey", "resolveThreadSessionKeys"]);
expectSourceContract("routing", {
mentions: [
"buildAgentSessionKey",
"resolveThreadSessionKeys",
"normalizeMessageChannel",
"resolveGatewayMessageChannel",
],
});
expectSourceMentions("reply-payload", [
"buildMediaPayload",
"deliverTextOrMediaReply",
@@ -183,12 +191,14 @@ describe("plugin-sdk subpath exports", () => {
"clearHistoryEntriesIfEnabled",
"recordPendingHistoryEntryIfEnabled",
]);
expectSourceOmits("reply-runtime", [
"buildPendingHistoryContextFromMap",
"clearHistoryEntriesIfEnabled",
"recordPendingHistoryEntryIfEnabled",
"DEFAULT_GROUP_HISTORY_LIMIT",
]);
expectSourceContract("reply-runtime", {
omits: [
"buildPendingHistoryContextFromMap",
"clearHistoryEntriesIfEnabled",
"recordPendingHistoryEntryIfEnabled",
"DEFAULT_GROUP_HISTORY_LIMIT",
],
});
expectSourceMentions("account-helpers", ["createAccountListHelpers"]);
expectSourceMentions("device-bootstrap", [
"approveDevicePairing",
@@ -199,23 +209,23 @@ describe("plugin-sdk subpath exports", () => {
"buildDmGroupAccountAllowlistAdapter",
"createNestedAllowlistOverrideResolver",
]);
expectSourceMentions("allow-from", [
"addAllowlistUserEntriesFromConfigEntry",
"buildAllowlistResolutionSummary",
"canonicalizeAllowlistWithResolvedIds",
"mapAllowlistResolutionInputs",
"mergeAllowlist",
"patchAllowlistUsersInConfigEntries",
"summarizeMapping",
]);
expectSourceMentions("allow-from", [
"compileAllowlist",
"firstDefined",
"formatAllowlistMatchMeta",
"isSenderIdAllowed",
"mergeDmAllowFromSources",
"resolveAllowlistMatchSimple",
]);
expectSourceContract("allow-from", {
mentions: [
"addAllowlistUserEntriesFromConfigEntry",
"buildAllowlistResolutionSummary",
"canonicalizeAllowlistWithResolvedIds",
"mapAllowlistResolutionInputs",
"mergeAllowlist",
"patchAllowlistUsersInConfigEntries",
"summarizeMapping",
"compileAllowlist",
"firstDefined",
"formatAllowlistMatchMeta",
"isSenderIdAllowed",
"mergeDmAllowFromSources",
"resolveAllowlistMatchSimple",
],
});
expectSourceMentions("runtime", ["createLoggerBackedRuntime"]);
expectSourceMentions("discord", [
"buildDiscordComponentMessage",
@@ -223,7 +233,6 @@ describe("plugin-sdk subpath exports", () => {
"registerBuiltDiscordComponentMessage",
"resolveDiscordAccount",
]);
expectSourceMentions("routing", ["normalizeMessageChannel", "resolveGatewayMessageChannel"]);
expectSourceMentions("conversation-runtime", [
"recordInboundSession",
"recordInboundSessionMetaSafe",
@@ -237,12 +246,6 @@ describe("plugin-sdk subpath exports", () => {
]);
});
it("exports infra runtime helpers from the dedicated subpath", async () => {
const infraRuntimeSdk = await importPluginSdkSubpath("openclaw/plugin-sdk/infra-runtime");
expect(typeof infraRuntimeSdk.createRuntimeOutboundDelegates).toBe("function");
expect(typeof infraRuntimeSdk.resolveOutboundSendDep).toBe("function");
});
it("exports channel runtime helpers from the dedicated subpath", () => {
expectSourceOmits("channel-runtime", [
"applyChannelMatchMeta",
@@ -366,26 +369,43 @@ describe("plugin-sdk subpath exports", () => {
"shouldDebounceTextInbound",
"toLocationContext",
]);
expectSourceOmits("reply-runtime", [
"buildMentionRegexes",
"createInboundDebouncer",
"formatInboundEnvelope",
"formatInboundFromLabel",
"matchesMentionPatterns",
"matchesMentionWithExplicit",
"normalizeMentionText",
"resolveEnvelopeFormatOptions",
"resolveInboundDebounceMs",
]);
expectSourceContract("reply-runtime", {
omits: [
"buildMentionRegexes",
"createInboundDebouncer",
"formatInboundEnvelope",
"formatInboundFromLabel",
"matchesMentionPatterns",
"matchesMentionWithExplicit",
"normalizeMentionText",
"resolveEnvelopeFormatOptions",
"resolveInboundDebounceMs",
"hasControlCommand",
"buildCommandTextFromArgs",
"buildCommandsPaginationKeyboard",
"buildModelsProviderData",
"listNativeCommandSpecsForConfig",
"listSkillCommandsForAgents",
"normalizeCommandBody",
"resolveCommandAuthorization",
"resolveStoredModelOverride",
"shouldComputeCommandAuthorized",
"shouldHandleTextCommands",
],
});
expectSourceMentions("channel-setup", [
"createOptionalChannelSetupSurface",
"createTopLevelChannelDmPolicy",
]);
expectSourceMentions("channel-actions", [
"createUnionActionGate",
"listTokenSourcedAccounts",
"resolveReactionMessageId",
]);
expectSourceContract("channel-actions", {
mentions: [
"createUnionActionGate",
"listTokenSourcedAccounts",
"resolveReactionMessageId",
"createMessageToolButtonsSchema",
"createMessageToolCardSchema",
],
});
expectSourceMentions("channel-targets", [
"applyChannelMatchMeta",
"buildChannelKeyCandidates",
@@ -419,10 +439,6 @@ describe("plugin-sdk subpath exports", () => {
"isRecord",
"resolveEnabledConfiguredAccountId",
]);
expectSourceMentions("channel-actions", [
"createMessageToolButtonsSchema",
"createMessageToolCardSchema",
]);
expectSourceMentions("command-auth", [
"buildCommandTextFromArgs",
"buildCommandsPaginationKeyboard",
@@ -440,19 +456,6 @@ describe("plugin-sdk subpath exports", () => {
"shouldComputeCommandAuthorized",
"shouldHandleTextCommands",
]);
expectSourceOmits("reply-runtime", [
"hasControlCommand",
"buildCommandTextFromArgs",
"buildCommandsPaginationKeyboard",
"buildModelsProviderData",
"listNativeCommandSpecsForConfig",
"listSkillCommandsForAgents",
"normalizeCommandBody",
"resolveCommandAuthorization",
"resolveStoredModelOverride",
"shouldComputeCommandAuthorized",
"shouldHandleTextCommands",
]);
});
it("keeps channel contract types on the dedicated subpath", () => {
@@ -470,39 +473,6 @@ describe("plugin-sdk subpath exports", () => {
expectTypeOf<ContractChannelThreadingToolContext>().toMatchTypeOf<ChannelThreadingToolContext>();
});
it("exports channel lifecycle helpers from the dedicated subpath", async () => {
const channelLifecycleSdk = await importPluginSdkSubpath(
"openclaw/plugin-sdk/channel-lifecycle",
);
expect(typeof channelLifecycleSdk.createDraftStreamLoop).toBe("function");
expect(typeof channelLifecycleSdk.createFinalizableDraftLifecycle).toBe("function");
expect(typeof channelLifecycleSdk.runPassiveAccountLifecycle).toBe("function");
expect(typeof channelLifecycleSdk.createRunStateMachine).toBe("function");
expect(typeof channelLifecycleSdk.createArmableStallWatchdog).toBe("function");
});
it("exports channel pairing helpers from the dedicated subpath", async () => {
const channelPairingSdk = await importPluginSdkSubpath("openclaw/plugin-sdk/channel-pairing");
expectSourceMentions("channel-pairing", [
"createChannelPairingController",
"createChannelPairingChallengeIssuer",
"createLoggedPairingApprovalNotifier",
"createPairingPrefixStripper",
"createTextPairingAdapter",
]);
expect("createScopedPairingAccess" in channelPairingSdk).toBe(false);
});
it("exports channel reply pipeline helpers from the dedicated subpath", async () => {
const channelReplyPipelineSdk = await importPluginSdkSubpath(
"openclaw/plugin-sdk/channel-reply-pipeline",
);
expectSourceMentions("channel-reply-pipeline", ["createChannelReplyPipeline"]);
expect("createTypingCallbacks" in channelReplyPipelineSdk).toBe(false);
expect("createReplyPrefixContext" in channelReplyPipelineSdk).toBe(false);
expect("createReplyPrefixOptions" in channelReplyPipelineSdk).toBe(false);
});
it("keeps source-only helper subpaths aligned", () => {
expectSourceMentions("channel-send-result", [
"attachChannelToResult",
@@ -551,17 +521,15 @@ describe("plugin-sdk subpath exports", () => {
"toFormUrlEncoded",
]);
expectSourceOmits("core", ["buildOauthProviderAuthResult"]);
expectSourceMentions("provider-models", [
"applyOpenAIConfig",
"buildKilocodeModelDefinition",
"discoverHuggingfaceModels",
]);
expectSourceOmits("provider-models", [
"buildMinimaxModelDefinition",
"buildMoonshotProvider",
"QIANFAN_BASE_URL",
"resolveZaiBaseUrl",
]);
expectSourceContract("provider-models", {
mentions: ["applyOpenAIConfig", "buildKilocodeModelDefinition", "discoverHuggingfaceModels"],
omits: [
"buildMinimaxModelDefinition",
"buildMoonshotProvider",
"QIANFAN_BASE_URL",
"resolveZaiBaseUrl",
],
});
expectSourceMentions("setup", [
"DEFAULT_ACCOUNT_ID",
@@ -589,7 +557,6 @@ describe("plugin-sdk subpath exports", () => {
"normalizeResolvedSecretInputString",
"normalizeSecretInputString",
]);
expectSourceMentions("webhook-ingress", [
"registerPluginHttpRoute",
"resolveWebhookPath",
@@ -613,10 +580,55 @@ describe("plugin-sdk subpath exports", () => {
expectTypeOf<CoreChannelMessageActionContext>().toMatchTypeOf<SharedChannelMessageActionContext>();
});
it("resolves representative curated public subpaths", async () => {
it("keeps runtime entry subpaths importable", async () => {
const [
coreSdk,
pluginEntrySdk,
infraRuntimeSdk,
channelLifecycleSdk,
channelPairingSdk,
channelReplyPipelineSdk,
...representativeModules
] = await Promise.all([
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/core"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/plugin-entry"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/infra-runtime"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/channel-lifecycle"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/channel-pairing"),
importResolvedPluginSdkSubpath("openclaw/plugin-sdk/channel-reply-pipeline"),
...representativeRuntimeSmokeSubpaths.map((id) =>
importResolvedPluginSdkSubpath(`openclaw/plugin-sdk/${id}`),
),
]);
expect(coreSdk.definePluginEntry).toBe(pluginEntrySdk.definePluginEntry);
expect(typeof infraRuntimeSdk.createRuntimeOutboundDelegates).toBe("function");
expect(typeof infraRuntimeSdk.resolveOutboundSendDep).toBe("function");
expect(typeof channelLifecycleSdk.createDraftStreamLoop).toBe("function");
expect(typeof channelLifecycleSdk.createFinalizableDraftLifecycle).toBe("function");
expect(typeof channelLifecycleSdk.runPassiveAccountLifecycle).toBe("function");
expect(typeof channelLifecycleSdk.createRunStateMachine).toBe("function");
expect(typeof channelLifecycleSdk.createArmableStallWatchdog).toBe("function");
expectSourceMentions("channel-pairing", [
"createChannelPairingController",
"createChannelPairingChallengeIssuer",
"createLoggedPairingApprovalNotifier",
"createPairingPrefixStripper",
"createTextPairingAdapter",
]);
expect("createScopedPairingAccess" in channelPairingSdk).toBe(false);
expectSourceMentions("channel-reply-pipeline", ["createChannelReplyPipeline"]);
expect("createTypingCallbacks" in channelReplyPipelineSdk).toBe(false);
expect("createReplyPrefixContext" in channelReplyPipelineSdk).toBe(false);
expect("createReplyPrefixOptions" in channelReplyPipelineSdk).toBe(false);
expect(pluginSdkSubpaths.length).toBeGreaterThan(representativeRuntimeSmokeSubpaths.length);
for (const id of representativeRuntimeSmokeSubpaths) {
const mod = await importPluginSdkSubpath(`openclaw/plugin-sdk/${id}`);
for (const [index, id] of representativeRuntimeSmokeSubpaths.entries()) {
const mod = representativeModules[index];
expect(typeof mod).toBe("object");
expect(mod, `subpath ${id} should resolve`).toBeTruthy();
}

View File

@@ -840,25 +840,13 @@ describe("installPluginFromDir", () => {
expectInstalledWithPluginId(res, extensionsDir, "@openclaw/test-plugin");
});
it("rejects bare @ as an invalid scoped id", () => {
expect(() => resolvePluginInstallDir("@")).toThrow(
"invalid plugin name: scoped ids must use @scope/name format",
);
});
it("keeps scoped install-dir validation aligned", () => {
for (const invalidId of ["@", "@/name", "team/name"]) {
expect(() => resolvePluginInstallDir(invalidId)).toThrow(
"invalid plugin name: scoped ids must use @scope/name format",
);
}
it("rejects empty scoped segments like @/name", () => {
expect(() => resolvePluginInstallDir("@/name")).toThrow(
"invalid plugin name: scoped ids must use @scope/name format",
);
});
it("rejects two-segment ids without a scope prefix", () => {
expect(() => resolvePluginInstallDir("team/name")).toThrow(
"invalid plugin name: scoped ids must use @scope/name format",
);
});
it("uses a unique hashed install dir for scoped ids", () => {
const extensionsDir = path.join(makeTempDir(), "extensions");
const scopedTarget = resolvePluginInstallDir("@scope/name", extensionsDir);
const hashedFlatId = safePathSegmentHashed("@scope/name");

View File

@@ -11,6 +11,9 @@ const pluginSdkInternalInventoryPromise =
const relativeOutsidePackageInventoryPromise = collectExtensionPluginSdkBoundaryInventory(
"relative-outside-package",
);
const srcOutsideJsonOutputPromise = getJsonOutput("src-outside-plugin-sdk");
const pluginSdkInternalJsonOutputPromise = getJsonOutput("plugin-sdk-internal");
const relativeOutsidePackageJsonOutputPromise = getJsonOutput("relative-outside-package");
async function getJsonOutput(
mode: Parameters<typeof collectExtensionPluginSdkBoundaryInventory>[0],
@@ -71,7 +74,7 @@ describe("extension src outside plugin-sdk boundary inventory", () => {
});
it("script json output is empty", async () => {
const result = await getJsonOutput("src-outside-plugin-sdk");
const result = await srcOutsideJsonOutputPromise;
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe("");
@@ -87,7 +90,7 @@ describe("extension plugin-sdk-internal boundary inventory", () => {
});
it("script json output is empty", async () => {
const result = await getJsonOutput("plugin-sdk-internal");
const result = await pluginSdkInternalJsonOutputPromise;
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe("");
@@ -103,7 +106,7 @@ describe("extension relative-outside-package boundary inventory", () => {
});
it("script json output is empty", async () => {
const result = await getJsonOutput("relative-outside-package");
const result = await relativeOutsidePackageJsonOutputPromise;
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe("");