fix(plugins): isolate legacy runtime stores

This commit is contained in:
Gustavo Madeira Santana
2026-04-15 10:22:40 -04:00
parent c9c212dbeb
commit c460c05e10
4 changed files with 62 additions and 12 deletions

View File

@@ -40,7 +40,7 @@ describe("createPluginRuntimeStore", () => {
expect(rightStore.tryGetRuntime()).toBeNull();
});
test("keeps legacy string callers working", () => {
test("keeps legacy string callers isolated per store", () => {
const firstStore = createPluginRuntimeStore<{ value: string }>(
"legacy runtime not initialized",
);
@@ -51,7 +51,8 @@ describe("createPluginRuntimeStore", () => {
firstStore.clearRuntime();
firstStore.setRuntime({ value: "legacy" });
expect(secondStore.getRuntime()).toEqual({ value: "legacy" });
expect(firstStore.getRuntime()).toEqual({ value: "legacy" });
expect(secondStore.tryGetRuntime()).toBeNull();
});
test("still supports explicit custom store keys", () => {

View File

@@ -64,12 +64,18 @@ export function createPluginRuntimeStore<T>(options: string | PluginRuntimeStore
getRuntime: () => T;
} {
const resolved = resolvePluginRuntimeStoreOptions(options);
const registry = getPluginRuntimeStoreRegistry();
let slot = registry.get(resolved.key);
if (!slot) {
slot = { runtime: null };
registry.set(resolved.key, slot);
}
const slot =
typeof options === "string"
? { runtime: null }
: (() => {
const registry = getPluginRuntimeStoreRegistry();
let existingSlot = registry.get(resolved.key);
if (!existingSlot) {
existingSlot = { runtime: null };
registry.set(resolved.key, existingSlot);
}
return existingSlot;
})();
return {
setRuntime(next: T) {

View File

@@ -523,6 +523,7 @@ function createSetupEntryChannelPluginFixture(params: {
configured: boolean;
startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean;
useBundledFullEntryContract?: boolean;
bundledFullEntryId?: string;
useBundledSetupEntryContract?: boolean;
splitBundledSetupSecrets?: boolean;
bundledSetupRuntimeMarker?: string;
@@ -580,7 +581,7 @@ function createSetupEntryChannelPluginFixture(params: {
? `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
kind: "bundled-channel-entry",
id: ${JSON.stringify(params.id)},
id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)},
name: ${JSON.stringify(params.label)},
description: ${JSON.stringify(params.fullBlurb)},
loadChannelPlugin: () => {
@@ -592,12 +593,12 @@ module.exports = {
: ""
}
return {
id: ${JSON.stringify(params.id)},
id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)},
meta: {
id: ${JSON.stringify(params.id)},
id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)},
label: ${JSON.stringify(params.label)},
selectionLabel: ${JSON.stringify(params.label)},
docsPath: ${JSON.stringify(`/channels/${params.id}`)},
docsPath: ${JSON.stringify(`/channels/${params.bundledFullEntryId ?? params.id}`)},
blurb: ${JSON.stringify(params.fullBlurb)},
},
capabilities: { chatTypes: ["direct"] },
@@ -3589,6 +3590,39 @@ module.exports = {
);
});
it("rejects mismatched bundled runtime plugin ids during setup-runtime merge", () => {
const built = createSetupEntryChannelPluginFixture({
id: "setup-runtime-mismatch-test",
bundledFullEntryId: "wrong-runtime-id",
label: "Setup Runtime Mismatch Test",
packageName: "@openclaw/setup-runtime-mismatch-test",
fullBlurb: "full runtime plugin",
setupBlurb: "setup runtime override",
configured: false,
useBundledFullEntryContract: true,
useBundledSetupEntryContract: true,
bundledFullRuntimeMarker: path.join(makeTempDir(), "setup-runtime-mismatch.txt"),
});
const registry = loadOpenClawPlugins({
cache: false,
config: {
plugins: {
load: { paths: [built.pluginDir] },
allow: ["setup-runtime-mismatch-test"],
},
},
});
expect(
registry.plugins.find((entry) => entry.id === "setup-runtime-mismatch-test")?.status,
).toBe("error");
expect(
registry.plugins.find((entry) => entry.id === "setup-runtime-mismatch-test")?.error,
).toContain('runtime export uses "wrong-runtime-id"');
expect(registry.channels).toHaveLength(0);
});
it("isolates loadSetupPlugin errors as per-plugin diagnostics instead of crashing registry load", () => {
useNoBundledPlugins();
const pluginDir = makeTempDir();

View File

@@ -1889,6 +1889,15 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
continue;
}
if (runtimePluginRegistration.plugin) {
if (
runtimePluginRegistration.plugin.id &&
runtimePluginRegistration.plugin.id !== record.id
) {
pushPluginLoadError(
`plugin id mismatch (config uses "${record.id}", runtime export uses "${runtimePluginRegistration.plugin.id}")`,
);
continue;
}
mergedSetupRegistration = {
...setupRegistration,
plugin: mergeSetupRuntimeChannelPlugin(