From 2a74ca12da6278664a8a5d9d87e1639d9daf7646 Mon Sep 17 00:00:00 2001 From: 0xVox Date: Tue, 2 Jun 2026 22:50:03 -0700 Subject: [PATCH] fix(openclaw): forward plugin config via api.pluginConfig (closes #150) register() read plugin config only from the factory-callback argument, but current OpenClaw hosts deliver config via api.pluginConfig (see #150's repro). On those hosts the engine booted with empty config and silently fell back to default serverUrl / userId. Prefer the host-provided api.pluginConfig and fall back to the callback arg so config forwarding works on both host generations; the `?? {}` tail keeps resolveConfig() from throwing when both sources are absent. Adds test/register-config.test.js (node --test, offline) proving api.pluginConfig wins over the callback arg, the callback path is used when api.pluginConfig is absent, and empty/undefined config falls back to defaults without throwing. Scope: index.js + its regression test only. Does not bundle the separate saveMemories PersonalAddRequest envelope fix (#237), which ships as its own PR. Closes #150. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../examples/openclaw-plugin/index.js | 7 +- .../test/register-config.test.js | 73 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 methods/EverCore/examples/openclaw-plugin/test/register-config.test.js diff --git a/methods/EverCore/examples/openclaw-plugin/index.js b/methods/EverCore/examples/openclaw-plugin/index.js index e47063bb..2f5e20fd 100644 --- a/methods/EverCore/examples/openclaw-plugin/index.js +++ b/methods/EverCore/examples/openclaw-plugin/index.js @@ -14,6 +14,11 @@ export default function register(api) { log.info(`[${pluginMeta.id}] Registering EverOS OpenClaw Plugin`); api.registerContextEngine(pluginMeta.id, (pluginConfig) => { - return createContextEngine(pluginMeta, pluginConfig, api.logger); + // The OpenClaw host may deliver plugin config in two ways depending on + // host version: as `api.pluginConfig` (current contract) or via the + // factory-callback argument (legacy). Prefer the host-provided config and + // fall back to the callback arg so config forwarding works on both. #150 + const resolvedConfig = api.pluginConfig ?? pluginConfig ?? {}; + return createContextEngine(pluginMeta, resolvedConfig, api.logger); }); } diff --git a/methods/EverCore/examples/openclaw-plugin/test/register-config.test.js b/methods/EverCore/examples/openclaw-plugin/test/register-config.test.js new file mode 100644 index 00000000..f81e597d --- /dev/null +++ b/methods/EverCore/examples/openclaw-plugin/test/register-config.test.js @@ -0,0 +1,73 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import register from "../index.js"; + +/** + * Regression test for #150: register() must forward plugin config to the + * context engine, preferring `api.pluginConfig` (current OpenClaw host contract) + * and falling back to the factory-callback argument (legacy hosts). + */ + +function makeApi({ pluginConfig } = {}) { + let factory = null; + const api = { + logger: { info: () => {}, warn: () => {} }, + pluginConfig, + registerContextEngine(id, fn) { + factory = fn; + }, + // Test helper: invoke the registered factory the way a host would. + _invokeFactory(callbackConfig) { + assert.ok(factory, "registerContextEngine was not called"); + return factory(callbackConfig); + }, + }; + return api; +} + +/** Run bootstrap() with a stubbed fetch and return the URL it called. + * bootstrap() hits `${cfg.serverUrl}/health`, so the host the engine resolved + * is directly observable — this is the concrete proof of which config won. */ +async function observeBootstrapUrl(engine) { + const original = global.fetch; + let calledUrl = null; + global.fetch = async (url) => { + calledUrl = String(url); + return { ok: true, status: 200, async json() { return {}; }, async text() { return ""; } }; + }; + try { + await engine.bootstrap({ sessionId: "s", sessionKey: "k" }); + } finally { + global.fetch = original; + } + return calledUrl; +} + +test("register() prefers api.pluginConfig over the callback arg", async () => { + const api = makeApi({ pluginConfig: { userId: "from-host", baseUrl: "http://host:1995" } }); + register(api); + const engine = api._invokeFactory({ userId: "from-callback", baseUrl: "http://callback:1995" }); + + // The host-provided baseUrl must win over the callback arg's baseUrl. + const url = await observeBootstrapUrl(engine); + assert.match(url, /^http:\/\/host:1995\/health/, "engine must resolve config from api.pluginConfig"); +}); + +test("register() falls back to the callback arg when api.pluginConfig is absent", async () => { + const api = makeApi({ pluginConfig: undefined }); + register(api); + const engine = api._invokeFactory({ userId: "from-callback", baseUrl: "http://callback:1995" }); + + const url = await observeBootstrapUrl(engine); + assert.match(url, /^http:\/\/callback:1995\/health/, "engine must fall back to the callback config"); +}); + +test("register() tolerates both config sources being empty", () => { + const api = makeApi({ pluginConfig: undefined }); + register(api); + const engine = api._invokeFactory(undefined); + + // resolveConfig() must apply defaults rather than throw on undefined config. + assert.equal(typeof engine.assemble, "function"); +});