From b732f58285533e7192409a23e0af7a171ecc10e2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 00:52:54 +0100 Subject: [PATCH] fix: stabilize channel configured probes --- src/channels/config-presence.test.ts | 26 ++++++++------------ src/channels/config-presence.ts | 5 ++-- src/channels/plugins/module-loader.test.ts | 28 +++++++++++++++------- src/channels/plugins/module-loader.ts | 27 +++++++++++++++++++++ src/config/channel-configured.test.ts | 24 ------------------- 5 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/channels/config-presence.test.ts b/src/channels/config-presence.test.ts index 2979889a73b..e070f7b26bd 100644 --- a/src/channels/config-presence.test.ts +++ b/src/channels/config-presence.test.ts @@ -11,23 +11,17 @@ import { listPotentialConfiguredChannelIds, } from "./config-presence.js"; -vi.mock("./plugins/bundled-ids.js", () => ({ - listBundledChannelPluginIds: () => ["matrix"], -})); - -vi.mock("../channels/plugins/persisted-auth-state.js", () => ({ - listBundledChannelIdsWithPersistedAuthState: () => ["matrix"], - hasBundledChannelPersistedAuthState: ({ - channelId, - env, - }: { - channelId: string; - env?: NodeJS.ProcessEnv; - }) => channelId === "matrix" && env?.OPENCLAW_STATE_DIR?.includes("persisted-matrix"), -})); - const tempDirs: string[] = []; +const matrixPresenceOptions = { + channelIds: ["matrix"], + persistedAuthStateProbe: { + listChannelIds: () => ["matrix"], + hasState: ({ channelId, env }: { channelId: string; env?: NodeJS.ProcessEnv }) => + channelId === "matrix" && Boolean(env?.OPENCLAW_STATE_DIR?.includes("persisted-matrix")), + }, +}; + function makeTempStateDir() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-config-presence-")); tempDirs.push(dir); @@ -41,7 +35,7 @@ function expectPotentialConfiguredChannelCase(params: { expectedConfigured: boolean; options?: Parameters[2]; }) { - const options = params.options ?? {}; + const options = params.options ?? matrixPresenceOptions; expect(listPotentialConfiguredChannelIds(params.cfg, params.env, options)).toEqual( params.expectedIds, ); diff --git a/src/channels/config-presence.ts b/src/channels/config-presence.ts index b59dae9e861..9114f2211bb 100644 --- a/src/channels/config-presence.ts +++ b/src/channels/config-presence.ts @@ -14,6 +14,7 @@ import { listBundledChannelPluginIds } from "./plugins/bundled-ids.js"; const IGNORED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]); type ChannelPresenceOptions = { + channelIds?: readonly string[]; includePersistedAuthState?: boolean; persistedAuthStateProbe?: { listChannelIds: () => readonly string[]; @@ -120,7 +121,7 @@ export function listPotentialConfiguredChannelPresenceSignals( signals.push({ channelId, source }); }; const configuredChannelIds = new Set(); - const channelIds = listBundledChannelPluginIds(env); + const channelIds = options.channelIds ?? listBundledChannelPluginIds(env); const channelEnvPrefixes = listChannelEnvPrefixes(channelIds); const channels = isRecord(cfg.channels) ? cfg.channels : null; if (channels) { @@ -164,7 +165,7 @@ function hasEnvConfiguredChannel( env: NodeJS.ProcessEnv, options: ChannelPresenceOptions = {}, ): boolean { - const channelIds = listBundledChannelPluginIds(env); + const channelIds = options.channelIds ?? listBundledChannelPluginIds(env); const channelEnvPrefixes = listChannelEnvPrefixes(channelIds); for (const [key, value] of Object.entries(env)) { if (!hasNonEmptyString(value)) { diff --git a/src/channels/plugins/module-loader.test.ts b/src/channels/plugins/module-loader.test.ts index 7e5e2403fdb..b2682c48f9c 100644 --- a/src/channels/plugins/module-loader.test.ts +++ b/src/channels/plugins/module-loader.test.ts @@ -12,7 +12,9 @@ afterEach(() => { for (const tempDir of tempDirs.splice(0)) { fs.rmSync(tempDir, { recursive: true, force: true }); } + vi.restoreAllMocks(); vi.resetModules(); + vi.doUnmock("jiti"); }); function createTempDir(): string { @@ -61,29 +63,39 @@ describe("channel plugin module loader helpers", () => { expect(createJiti).not.toHaveBeenCalled(); }); - it("rejects TypeScript modules without creating Jiti", async () => { - const createJiti = vi.fn(() => { - throw new Error("channel module loader must not create jiti"); - }); + it("loads TypeScript channel plugin modules through Jiti when no native hook exists", async () => { + const loadWithJiti = vi.fn((target: string) => ({ + loadedBy: "jiti", + target, + })); + const createJiti = vi.fn(() => loadWithJiti); vi.resetModules(); vi.doMock("jiti", () => ({ createJiti, })); const loaderModule = await importFreshModule( import.meta.url, - "./module-loader.js?scope=source-ts-native-hook", + "./module-loader.js?scope=source-ts-jiti-fallback", ); const rootDir = createTempDir(); const modulePath = path.join(rootDir, "extensions", "demo", "index.ts"); fs.mkdirSync(path.dirname(modulePath), { recursive: true }); fs.writeFileSync(modulePath, "export const ok = true;\n", "utf8"); - expect(() => + expect( loaderModule.loadChannelPluginModule({ modulePath, rootDir, }), - ).toThrow(/must be built JavaScript/u); - expect(createJiti).not.toHaveBeenCalled(); + ).toEqual({ + loadedBy: "jiti", + target: modulePath, + }); + expect(createJiti).toHaveBeenCalledOnce(); + expect(createJiti).toHaveBeenCalledWith( + expect.stringContaining("module-loader.ts"), + expect.objectContaining({ tryNative: false }), + ); + expect(loadWithJiti).toHaveBeenCalledWith(modulePath); }); }); diff --git a/src/channels/plugins/module-loader.ts b/src/channels/plugins/module-loader.ts index 0e8205cd8dc..fff32363142 100644 --- a/src/channels/plugins/module-loader.ts +++ b/src/channels/plugins/module-loader.ts @@ -2,10 +2,15 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { openBoundaryFileSync } from "../../infra/boundary-file-read.js"; +import { + getCachedPluginJitiLoader, + type PluginJitiLoaderCache, +} from "../../plugins/jiti-loader-cache.js"; import { isJavaScriptModulePath } from "../../plugins/native-module-require.js"; const nodeRequire = createRequire(import.meta.url); const SOURCE_MODULE_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]); +const jitiLoaders: PluginJitiLoaderCache = new Map(); function hasNativeSourceRequireHook(modulePath: string): boolean { const extension = path.extname(modulePath).toLowerCase(); @@ -15,13 +20,35 @@ function hasNativeSourceRequireHook(modulePath: string): boolean { ); } +function isSourceModulePath(modulePath: string): boolean { + return SOURCE_MODULE_EXTENSIONS.has(path.extname(modulePath).toLowerCase()); +} + +function loadModuleWithJiti(modulePath: string): unknown { + const loadWithJiti = getCachedPluginJitiLoader({ + cache: jitiLoaders, + modulePath, + importerUrl: import.meta.url, + jitiFilename: import.meta.url, + tryNative: false, + cacheScopeKey: "channel-plugin-module-loader", + }); + return loadWithJiti(modulePath); +} + function loadModule(modulePath: string): unknown { if (!isJavaScriptModulePath(modulePath) && !hasNativeSourceRequireHook(modulePath)) { + if (isSourceModulePath(modulePath)) { + return loadModuleWithJiti(modulePath); + } throw new Error(`channel plugin module must be built JavaScript: ${modulePath}`); } try { return nodeRequire(modulePath); } catch (error) { + if (isSourceModulePath(modulePath)) { + return loadModuleWithJiti(modulePath); + } throw new Error(`failed to load channel plugin module with native require: ${modulePath}`, { cause: error, }); diff --git a/src/config/channel-configured.test.ts b/src/config/channel-configured.test.ts index 4b1410af109..8229a6bb988 100644 --- a/src/config/channel-configured.test.ts +++ b/src/config/channel-configured.test.ts @@ -1,30 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { isChannelConfigured } from "./channel-configured.js"; -vi.mock("../channels/plugins/configured-state.js", () => ({ - hasBundledChannelConfiguredState: ({ - channelId, - env, - }: { - channelId: string; - env?: NodeJS.ProcessEnv; - }) => { - if (channelId === "telegram") { - return Boolean(env?.TELEGRAM_BOT_TOKEN); - } - if (channelId === "discord") { - return Boolean(env?.DISCORD_BOT_TOKEN); - } - if (channelId === "slack") { - return Boolean(env?.SLACK_BOT_TOKEN); - } - if (channelId === "irc") { - return Boolean(env?.IRC_HOST && env?.IRC_NICK); - } - return false; - }, -})); - vi.mock("../channels/plugins/bootstrap-registry.js", () => ({ getBootstrapChannelPlugin: () => undefined, }));