test(plugin-sdk): ratchet surface budget checks

This commit is contained in:
Vincent Koc
2026-06-16 02:33:28 +02:00
parent 9eed9c5758
commit d5c9e7ea99
6 changed files with 186 additions and 5 deletions

View File

@@ -251,3 +251,6 @@ jobs:
- name: Check plugin SDK API baseline drift
run: pnpm plugin-sdk:api:check
- name: Check plugin SDK surface budget
run: pnpm plugin-sdk:surface:check

View File

@@ -37,21 +37,145 @@ function readBudgetEnv(name, fallback) {
return parsed;
}
function readEntrypointBudgetEnv(name, fallback) {
const raw = process.env[name];
if (raw === undefined) {
return fallback;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw new Error(`${name} must be a JSON object of entrypoint integer budgets`);
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${name} must be a JSON object of entrypoint integer budgets`);
}
const overrides = {};
for (const [entrypoint, value] of Object.entries(parsed)) {
if (!Number.isSafeInteger(value) || value < 0) {
throw new Error(`${name}.${entrypoint} must be a safe non-negative integer`);
}
overrides[entrypoint] = value;
}
return Object.freeze({ ...fallback, ...overrides });
}
const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
core: 2,
lmstudio: 1,
"provider-setup": 1,
"self-hosted-provider-setup": 14,
routing: 1,
runtime: 3,
"runtime-logger": 3,
"runtime-secret-resolution": 5,
"setup-adapter-runtime": 1,
"channel-streaming": 47,
"approval-reply-runtime": 1,
"config-runtime": 123,
"config-contracts": 1,
"config-types": 415,
"config-schema": 3,
"reply-dedupe": 1,
"inbound-reply-dispatch": 33,
"channel-reply-pipeline": 12,
"channel-reply-options-runtime": 2,
"channel-runtime": 144,
"interactive-runtime": 13,
"outbound-send-deps": 4,
"outbound-runtime": 16,
"file-access-runtime": 2,
"infra-runtime": 570,
"ssrf-policy": 1,
"ssrf-runtime": 1,
"media-runtime": 2,
"text-runtime": 188,
"agent-runtime": 7,
"plugin-runtime": 13,
"channel-secret-runtime": 23,
"secret-file-runtime": 1,
"security-runtime": 7,
"agent-harness": 7,
"agent-harness-runtime": 7,
types: 6,
"agent-config-primitives": 2,
"command-auth": 81,
compat: 152,
"direct-dm": 9,
"direct-dm-access": 5,
discord: 48,
mattermost: 7,
matrix: 1,
"channel-config-schema-legacy": 22,
"channel-actions": 2,
"channel-envelope": 3,
"channel-inbound": 21,
"channel-inbound-roots": 1,
"channel-logging": 4,
"channel-location": 4,
"channel-mention-gating": 7,
"channel-lifecycle": 23,
"channel-ingress": 8,
"channel-message": 228,
"channel-message-runtime": 225,
"channel-pairing-paths": 1,
"channel-policy": 8,
"channel-route": 5,
"session-store-runtime": 1,
"group-access": 13,
"media-generation-runtime-shared": 3,
"music-generation-core": 20,
"reply-history": 8,
"messaging-targets": 12,
"memory-core": 45,
"memory-core-engine-runtime": 15,
"memory-core-host-multimodal": 3,
"memory-core-host-query": 2,
"memory-core-host-events": 11,
"memory-core-host-status": 1,
"memory-core-host-runtime-core": 1,
"memory-host-core": 1,
"memory-host-files": 7,
"memory-host-status": 72,
"provider-auth": 20,
"provider-oauth-runtime": 2,
"provider-auth-login": 3,
"provider-model-shared": 29,
"provider-stream-family": 40,
"provider-stream-shared": 28,
"provider-stream": 40,
"provider-web-search": 1,
"provider-zai-endpoint": 3,
"telegram-account": 3,
"telegram-command-config": 7,
"webhook-ingress": 2,
"webhook-path": 2,
zalouser: 5,
zod: 282,
});
let budgets;
let publicDeprecatedExportsByEntrypointBudget;
try {
budgets = {
publicEntrypoints: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS", 308),
publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 9920),
publicFunctionExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", 5031),
publicEntrypoints: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_ENTRYPOINTS", 319),
publicExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_EXPORTS", 10269),
publicFunctionExports: readBudgetEnv("OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_FUNCTION_EXPORTS", 5159),
publicDeprecatedExports: readBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS",
3143,
3230,
),
publicWildcardReexports: readBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_WILDCARD_REEXPORTS",
215,
),
};
publicDeprecatedExportsByEntrypointBudget = readEntrypointBudgetEnv(
"OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS_BY_ENTRYPOINT",
defaultPublicDeprecatedExportsByEntrypointBudget,
);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
@@ -194,6 +318,19 @@ function formatStats(label, stats) {
].join("\n");
}
function collectDeprecatedEntrypointBudgetFailures(byEntrypoint) {
const failures = [];
for (const [entrypoint, stats] of byEntrypoint) {
const budget = publicDeprecatedExportsByEntrypointBudget[entrypoint] ?? 0;
if (stats.deprecatedExports > budget) {
failures.push(
`public deprecated exports in ${entrypoint} ${stats.deprecatedExports} > ${budget}`,
);
}
}
return failures;
}
const allStats = collectExportStats(pluginSdkEntrypoints);
const publicStats = collectExportStats(publicPluginSdkEntrypoints);
const localOnlyStats = collectExportStats(privateLocalOnlyPluginSdkEntrypoints);
@@ -246,6 +383,7 @@ if (publicStats.totals.deprecatedExports > budgets.publicDeprecatedExports) {
`public deprecated exports ${publicStats.totals.deprecatedExports} > ${budgets.publicDeprecatedExports}`,
);
}
failures.push(...collectDeprecatedEntrypointBudgetFailures(publicStats.byEntrypoint));
if (publicWildcards.count > budgets.publicWildcardReexports) {
failures.push(
`public wildcard reexports ${publicWildcards.count} > ${budgets.publicWildcardReexports}`,

View File

@@ -29,6 +29,7 @@ const checkCommands = [
{ name: "config docs baseline", args: ["config:docs:check"] },
{ name: "plugin SDK exports", args: ["plugin-sdk:check-exports"] },
{ name: "plugin SDK API baseline", args: ["plugin-sdk:api:check"] },
{ name: "plugin SDK surface budget", args: ["plugin-sdk:surface:check"] },
];
if (fix) {

View File

@@ -78,7 +78,13 @@ export type { OutboundSendDeps } from "../infra/outbound/send-deps.js";
export { sanitizeForPlainText } from "../infra/outbound/sanitize-text.js";
export { logAckFailure, logTypingFailure } from "../channels/logging.js";
export * from "../channels/streaming.js";
export * from "../channels/progress-draft-compositor.js";
export {
createChannelProgressDraftCompositor,
type ChannelProgressDraftCompositor,
type ChannelProgressDraftCompositorLine,
type ChannelProgressDraftMode,
type ChannelProgressDraftUpdateOptions,
} from "../channels/progress-draft-compositor.js";
export {
classifyDurableSendRecoveryState,
createChannelMessageAdapterFromOutbound,

View File

@@ -131,6 +131,21 @@ describe("ci workflow guards", () => {
}
});
it("runs plugin SDK API and surface drift checks in workflow sanity", () => {
const workflow = readWorkflowSanityWorkflow();
const steps = workflow.jobs["generated-doc-baselines"].steps;
const stepNames = steps.map((step) => step.name);
expect(stepNames).toContain("Check plugin SDK API baseline drift");
expect(stepNames).toContain("Check plugin SDK surface budget");
expect(stepNames.indexOf("Check plugin SDK API baseline drift")).toBeLessThan(
stepNames.indexOf("Check plugin SDK surface budget"),
);
expect(steps.find((step) => step.name === "Check plugin SDK surface budget").run).toBe(
"pnpm plugin-sdk:surface:check",
);
});
it("bounds platform checkout fetches without GNU timeout", () => {
const workflow = readCiWorkflow();

View File

@@ -39,4 +39,22 @@ describe("plugin SDK surface report", () => {
);
expect(result.stderr).not.toContain("at ");
});
it("accepts exact deprecated export budget overrides by public entrypoint", () => {
const result = runSurfaceReport({
OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS_BY_ENTRYPOINT: JSON.stringify({ core: 2 }),
});
expect(result.status).toBe(0);
expect(result.stderr).toBe("");
});
it("rejects deprecated export growth by public entrypoint", () => {
const result = runSurfaceReport({
OPENCLAW_PLUGIN_SDK_MAX_PUBLIC_DEPRECATED_EXPORTS_BY_ENTRYPOINT: JSON.stringify({ core: 1 }),
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("public deprecated exports in core 2 > 1");
});
});