mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 17:11:42 -06:00
fix(plugin-sdk): keep published pre-split plugin imports loading after upgrade (#124086)
* fix(plugin-sdk): keep published pre-split plugin imports loading after upgrade Same bug class as #124041: published plugin artifacts import SDK names at module top level, so removing them from the barrel makes the installed plugin fail to load (voice-call/matrix doctor contracts silently never run their migrations; whatsapp and slack channels fail outright) after a core upgrade. Verified against the actual npm tarballs (2026.7.2-beta.7): - openclaw/plugin-sdk/runtime-doctor: voice-call + matrix doctor contracts import repair names (archiveLegacyStateSource, detect/repair state DB schema, plugin install-path repair, removePluginFromConfig, createPluginStateSyncKeyedStore) that moved to doctor-repair-runtime. - openclaw/plugin-sdk/channel-feedback: whatsapp imports shouldAckReactionForWhatsApp (owner policy moved in-plugin by #121257). - openclaw/plugin-sdk/channel-outbound: slack imports resolveChannelProgressDraftRender (render key retired by #122927). Adds deprecated load-only bridges with named removal windows, bumps the SDK surface budgets with comments, and locks behavior with unit tests plus a loader fixture that fails without the bridges. * test(plugin-sdk): cover the repair bridge in the runtime-doctor facade surface lock
This commit is contained in:
committed by
GitHub
parent
f4871eb86b
commit
c83c3bc3a9
@@ -157,7 +157,12 @@ const defaultPublicDeprecatedExportsByEntrypointBudget = Object.freeze({
|
||||
"channel-lifecycle": 23,
|
||||
// +1: shared ingress error factory projected through the deprecated message barrel.
|
||||
// +1: shared ingress retention defaults projected through the deprecated message barrel.
|
||||
"channel-message": 131,
|
||||
// +1: WhatsApp ack-policy bridge counted via channel-message's wildcard re-export.
|
||||
"channel-message": 132,
|
||||
// +2: Slack progress-draft render bridge (function + mode type).
|
||||
"channel-outbound": 2,
|
||||
// +2: WhatsApp ack-policy bridge (function + mode type).
|
||||
"channel-feedback": 2,
|
||||
"channel-pairing": 0,
|
||||
"channel-policy": 7,
|
||||
"channel-send-result": 1,
|
||||
@@ -276,7 +281,10 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
|
||||
// +11: Computer Use schemas/types plus parsers, compiler, and provider registration.
|
||||
// +6: Computer Use v2 action, result, and capability contracts.
|
||||
// +1: opaque channel participant evidence preservation without mint authority.
|
||||
4324,
|
||||
// +6: load-only bridges for published pre-split plugin artifacts
|
||||
// (voice-call/matrix runtime-doctor repair names, WhatsApp ack policy,
|
||||
// Slack progress-draft render) so installed plugins survive upgrade (#124041 class).
|
||||
4330,
|
||||
env,
|
||||
),
|
||||
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
|
||||
@@ -346,7 +354,10 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
|
||||
// -2: retire the dead progress-draft render reader; it counted twice via
|
||||
// channel-outbound and channel-message's wildcard re-export of it.
|
||||
// +4: Computer Use wire parsers, validator compiler, and provider registration.
|
||||
2574,
|
||||
// +3: load-only bridges for published pre-split plugin artifacts
|
||||
// (voice-call/matrix runtime-doctor repair names, WhatsApp ack policy,
|
||||
// Slack progress-draft render) so installed plugins survive upgrade (#124041 class).
|
||||
2577,
|
||||
env,
|
||||
),
|
||||
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
|
||||
@@ -362,7 +373,10 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
|
||||
// +7: restore still-existing deprecated inbound-dispatch compatibility re-exports.
|
||||
// +6: source-compatible harness contracts retained during the V2 migration window.
|
||||
// +4: shipped default-agent resolver projections retained during explicit-owner migration.
|
||||
1146,
|
||||
// +5: load-only bridges for published pre-split plugin artifacts
|
||||
// (voice-call/matrix runtime-doctor repair names, WhatsApp ack policy,
|
||||
// Slack progress-draft render) so installed plugins survive upgrade (#124041 class).
|
||||
1151,
|
||||
env,
|
||||
),
|
||||
publicWildcardReexports: readPluginSdkSurfaceBudgetEnv(
|
||||
|
||||
@@ -12,6 +12,50 @@ export {
|
||||
type AckReactionScope,
|
||||
} from "../channels/ack-reactions.js";
|
||||
export { logAckFailure, logTypingFailure, type LogFn } from "../channels/logging.js";
|
||||
import { shouldAckReaction as sharedAckReactionGate } from "../channels/ack-reactions.js";
|
||||
|
||||
/** @deprecated Owner policy moved into the WhatsApp plugin (#121257). */
|
||||
export type WhatsAppAckReactionMode = "always" | "mentions" | "never";
|
||||
|
||||
/**
|
||||
* @deprecated Load-only bridge: the published WhatsApp channel package
|
||||
* (2026.7.2-beta.7 and earlier) imports this at module top level, so removing
|
||||
* it makes the installed plugin fail to load after a core upgrade. Behavior
|
||||
* preserved verbatim from the pre-#121257 owner. Remove once managed releases
|
||||
* have replaced the old npm latest/extended-stable packages and their upgrade
|
||||
* window has closed.
|
||||
*/
|
||||
export function shouldAckReactionForWhatsApp(params: {
|
||||
emoji: string;
|
||||
isDirect: boolean;
|
||||
isGroup: boolean;
|
||||
directEnabled: boolean;
|
||||
groupMode: WhatsAppAckReactionMode;
|
||||
wasMentioned: boolean;
|
||||
groupActivated: boolean;
|
||||
}): boolean {
|
||||
if (!params.emoji) {
|
||||
return false;
|
||||
}
|
||||
if (params.isDirect) {
|
||||
return params.directEnabled;
|
||||
}
|
||||
if (!params.isGroup || params.groupMode === "never") {
|
||||
return false;
|
||||
}
|
||||
if (params.groupMode === "always") {
|
||||
return true;
|
||||
}
|
||||
return sharedAckReactionGate({
|
||||
scope: "group-mentions",
|
||||
isDirect: false,
|
||||
isGroup: true,
|
||||
isMentionableGroup: true,
|
||||
canDetectMention: true,
|
||||
effectiveWasMentioned: params.wasMentioned,
|
||||
shouldBypassMention: params.groupActivated,
|
||||
});
|
||||
}
|
||||
export { missingTargetError } from "../infra/outbound/target-errors.js";
|
||||
export {
|
||||
BUILD_TOOL_TOKENS,
|
||||
|
||||
@@ -123,6 +123,30 @@ export {
|
||||
createChannelProgressDraftCompositor,
|
||||
createChannelProgressReceiptTracker,
|
||||
} from "../channels/progress-draft-compositor.js";
|
||||
import {
|
||||
resolveChannelProgressDraftConfig as readProgressDraftConfig,
|
||||
type StreamingCompatEntry as ProgressDraftCompatEntry,
|
||||
} from "../channels/streaming.js";
|
||||
|
||||
/** @deprecated The streaming.progress.render key was retired (#122927). */
|
||||
export type ChannelProgressDraftRenderMode = "rich" | "text";
|
||||
|
||||
/**
|
||||
* @deprecated Load-only bridge: the published Slack channel package
|
||||
* (2026.7.2-beta.7 and earlier) imports this at module top level, so removing
|
||||
* it makes the installed plugin fail to load after a core upgrade. The config
|
||||
* key it read is retired and doctor strips it, so this resolves the same
|
||||
* "text"/"rich" answer pre-doctor configs produced and the default otherwise.
|
||||
* Remove once managed releases have replaced the old npm latest/extended-stable
|
||||
* packages and their upgrade window has closed.
|
||||
*/
|
||||
export function resolveChannelProgressDraftRender(
|
||||
entry: ProgressDraftCompatEntry | null | undefined,
|
||||
defaultValue: ChannelProgressDraftRenderMode = "text",
|
||||
): ChannelProgressDraftRenderMode {
|
||||
const configured = (readProgressDraftConfig(entry) as { render?: unknown }).render;
|
||||
return configured === "rich" || configured === "text" ? configured : defaultValue;
|
||||
}
|
||||
export type {
|
||||
ChannelProgressDraftCompositorLine,
|
||||
ChannelProgressDraftCompositorSnapshot,
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createPluginStateSyncKeyedStore } from "../plugin-state/plugin-state-store.js";
|
||||
import * as doctorRepairRuntime from "./doctor-repair-runtime.js";
|
||||
import * as runtimeDoctorMigrations from "./runtime-doctor-migrations.js";
|
||||
import * as legacyRuntimeDoctor from "./runtime-doctor.js";
|
||||
|
||||
describe("legacy runtime-doctor package facade", () => {
|
||||
it("is exactly the dependency-light migration surface", () => {
|
||||
expect(Object.keys(legacyRuntimeDoctor).toSorted()).toEqual(
|
||||
Object.keys(runtimeDoctorMigrations).toSorted(),
|
||||
);
|
||||
it("is exactly the migration surface plus the published-artifact repair bridge", () => {
|
||||
// Bridge names ship for published pre-split doctor artifacts (#124041
|
||||
// class); delete them here alongside the runtime-doctor.ts bridge.
|
||||
const expected = [
|
||||
...new Set([
|
||||
...Object.keys(runtimeDoctorMigrations),
|
||||
...Object.keys(doctorRepairRuntime),
|
||||
"createPluginStateSyncKeyedStore",
|
||||
]),
|
||||
].toSorted();
|
||||
expect(Object.keys(legacyRuntimeDoctor).toSorted()).toEqual(expected);
|
||||
for (const key of Object.keys(runtimeDoctorMigrations)) {
|
||||
expect(legacyRuntimeDoctor[key as keyof typeof legacyRuntimeDoctor]).toBe(
|
||||
runtimeDoctorMigrations[key as keyof typeof runtimeDoctorMigrations],
|
||||
);
|
||||
}
|
||||
for (const key of Object.keys(doctorRepairRuntime)) {
|
||||
expect(legacyRuntimeDoctor[key as keyof typeof legacyRuntimeDoctor]).toBe(
|
||||
doctorRepairRuntime[key as keyof typeof doctorRepairRuntime],
|
||||
);
|
||||
}
|
||||
expect(legacyRuntimeDoctor.createPluginStateSyncKeyedStore).toBe(
|
||||
createPluginStateSyncKeyedStore,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,3 +3,14 @@
|
||||
* Current source must import `runtime-doctor-migrations` directly.
|
||||
*/
|
||||
export * from "./runtime-doctor-migrations.js";
|
||||
/**
|
||||
* @deprecated Load-only bridge: published pre-split doctor artifacts
|
||||
* (voice-call/matrix 2026.7.2-beta.7 and earlier) import these repair names
|
||||
* from this subpath; without them the contract module fails to load and the
|
||||
* plugin's doctor migrations silently never run. Remove once managed releases
|
||||
* have replaced the old npm latest/extended-stable packages and their upgrade
|
||||
* window has closed. Current source imports `doctor-repair-runtime` (heavy)
|
||||
* and `plugin-state-store-runtime` directly.
|
||||
*/
|
||||
export * from "./doctor-repair-runtime.js";
|
||||
export { createPluginStateSyncKeyedStore } from "../plugin-state/plugin-state-store.js";
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldAckReactionForWhatsApp, type WhatsAppAckReactionMode } from "./channel-feedback.js";
|
||||
import { resolveChannelProgressDraftRender } from "./channel-outbound.js";
|
||||
|
||||
// Behavior locks for the deprecated load-only bridges consumed by published
|
||||
// pre-split plugin artifacts (2026.7.2-beta.7 and earlier). Delete alongside
|
||||
// the bridges when their upgrade window closes.
|
||||
describe("shouldAckReactionForWhatsApp bridge", () => {
|
||||
const base = {
|
||||
emoji: "👍",
|
||||
isDirect: false,
|
||||
isGroup: true,
|
||||
directEnabled: true,
|
||||
groupMode: "mentions" as WhatsAppAckReactionMode,
|
||||
wasMentioned: false,
|
||||
groupActivated: false,
|
||||
};
|
||||
|
||||
const cases: Array<{ name: string; params: Partial<typeof base>; expected: boolean }> = [
|
||||
{ name: "empty emoji never acks", params: { emoji: "" }, expected: false },
|
||||
{ name: "direct follows directEnabled true", params: { isDirect: true }, expected: true },
|
||||
{
|
||||
name: "direct follows directEnabled false",
|
||||
params: { isDirect: true, directEnabled: false },
|
||||
expected: false,
|
||||
},
|
||||
{ name: "non-group non-direct never acks", params: { isGroup: false }, expected: false },
|
||||
{ name: "group mode never", params: { groupMode: "never" }, expected: false },
|
||||
{ name: "group mode always", params: { groupMode: "always" }, expected: true },
|
||||
{ name: "mentions mode without mention", params: {}, expected: false },
|
||||
{ name: "mentions mode with mention", params: { wasMentioned: true }, expected: true },
|
||||
{
|
||||
name: "mentions mode activated group bypasses mention",
|
||||
params: { groupActivated: true },
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
it.each(cases)("$name", ({ params, expected }) => {
|
||||
expect(shouldAckReactionForWhatsApp({ ...base, ...params })).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChannelProgressDraftRender bridge", () => {
|
||||
it("returns the default when the retired render key is absent", () => {
|
||||
expect(resolveChannelProgressDraftRender({})).toBe("text");
|
||||
expect(resolveChannelProgressDraftRender(undefined, "rich")).toBe("rich");
|
||||
});
|
||||
|
||||
it("passes through configured rich/text and ignores junk", () => {
|
||||
expect(resolveChannelProgressDraftRender({ streaming: { progress: { render: "rich" } } })).toBe(
|
||||
"rich",
|
||||
);
|
||||
expect(resolveChannelProgressDraftRender({ streaming: { progress: { render: "text" } } })).toBe(
|
||||
"text",
|
||||
);
|
||||
expect(
|
||||
resolveChannelProgressDraftRender({ streaming: { progress: { render: "bogus" } } }, "rich"),
|
||||
).toBe("rich");
|
||||
});
|
||||
});
|
||||
@@ -1113,7 +1113,9 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"shouldAckReaction",
|
||||
"DEFAULT_EMOJIS",
|
||||
]);
|
||||
expectSourceOmits("channel-feedback", [
|
||||
// The load-only WhatsApp bridge lives in the barrel for published
|
||||
// pre-#121257 artifacts; the owner file stays free of channel policy.
|
||||
expectSourceMentions("channel-feedback", [
|
||||
"shouldAckReactionForWhatsApp",
|
||||
"WhatsAppAckReactionMode",
|
||||
]);
|
||||
|
||||
@@ -123,6 +123,71 @@ function writePreManagedLlamaCppProviderFixture() {
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
function writePreSplitSdkBridgeConsumerFixture() {
|
||||
const pluginRoot = tempDirs.make("openclaw-plugin-loader-");
|
||||
fs.mkdirSync(path.join(pluginRoot, "dist"));
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/sdk-bridge-consumer",
|
||||
version: "2026.7.2-beta.7",
|
||||
type: "module",
|
||||
openclaw: {
|
||||
extensions: ["./dist/index.js"],
|
||||
runtimeExtensions: ["./dist/index.js"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "sdk-bridge-consumer",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
// Import shapes copied from published 2026.7.2-beta.7 artifacts:
|
||||
// voice-call/matrix doctor contracts (runtime-doctor), whatsapp ack policy
|
||||
// (channel-feedback), slack progress-draft render (channel-outbound).
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "dist", "index.js"),
|
||||
[
|
||||
'import { archiveLegacyStateSource, detectOpenClawStateDatabaseSchemaMigrations, repairOpenClawStateDatabaseSchema, detectPluginInstallPathIssue, formatPluginInstallPathIssue, removePluginFromConfig, createPluginStateSyncKeyedStore } from "openclaw/plugin-sdk/runtime-doctor";',
|
||||
'import { shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";',
|
||||
'import { resolveChannelProgressDraftRender } from "openclaw/plugin-sdk/channel-outbound";',
|
||||
'export default { id: "sdk-bridge-consumer", register() {',
|
||||
" const bridged = [",
|
||||
" archiveLegacyStateSource,",
|
||||
" detectOpenClawStateDatabaseSchemaMigrations,",
|
||||
" repairOpenClawStateDatabaseSchema,",
|
||||
" detectPluginInstallPathIssue,",
|
||||
" formatPluginInstallPathIssue,",
|
||||
" removePluginFromConfig,",
|
||||
" createPluginStateSyncKeyedStore,",
|
||||
" shouldAckReactionForWhatsApp,",
|
||||
" resolveChannelProgressDraftRender,",
|
||||
" ];",
|
||||
' if (bridged.some((entry) => typeof entry !== "function")) throw new Error("missing bridge");',
|
||||
"} };",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("./plugin-module-loader-cache.js");
|
||||
@@ -242,4 +307,30 @@ describe("createPluginModuleLoader", () => {
|
||||
|
||||
expect(registry.plugins.find((plugin) => plugin.id === "llama-cpp")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("loads published pre-split SDK bridge imports (doctor repair, WhatsApp ack, Slack render)", async () => {
|
||||
const { loadOpenClawPlugins } = await importFreshModule<typeof import("./loader.js")>(
|
||||
import.meta.url,
|
||||
"./loader.js?scope=sdk-bridge-upgrade-compat",
|
||||
);
|
||||
const pluginRoot = writePreSplitSdkBridgeConsumerFixture();
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = tempDirs.make("openclaw-plugin-loader-");
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
onlyPluginIds: ["sdk-bridge-consumer"],
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
load: { paths: [pluginRoot] },
|
||||
allow: ["sdk-bridge-consumer"],
|
||||
entries: { "sdk-bridge-consumer": { enabled: true } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const entry = registry.plugins.find((plugin) => plugin.id === "sdk-bridge-consumer");
|
||||
expect(entry?.error ?? null).toBeNull();
|
||||
expect(entry?.status).toBe("loaded");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user