From 8043923910e9d8f18030580aa5ae826d4fef8d2d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 1 May 2026 23:43:09 +0100 Subject: [PATCH] refactor(plugins): remove extension jiti test hooks --- .../thread-bindings.shared-state.test.ts | 4 +- .../src/monitor/thread-bindings.state.ts | 5 +- extensions/matrix/CHANGELOG.md | 2 +- .../matrix/src/plugin-entry.runtime.test.ts | 7 +- extensions/zalo/runtime-api.test.ts | 26 +- .../check-no-extension-test-core-imports.ts | 1 - src/plugin-sdk/plugin-test-contracts.ts | 1 - .../test-helpers/jiti-runtime-api.ts | 245 ------------------ 8 files changed, 19 insertions(+), 272 deletions(-) delete mode 100644 src/plugin-sdk/test-helpers/jiti-runtime-api.ts diff --git a/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts b/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts index bce65c99c10..f852908d819 100644 --- a/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts +++ b/extensions/discord/src/monitor/thread-bindings.shared-state.test.ts @@ -21,7 +21,7 @@ describe("thread binding manager state", () => { }); it("shares managers between ESM and alternate-loaded module instances", async () => { - const viaJiti = await loadThreadBindingsViaAlternateLoader(); + const viaAlternateLoader = await loadThreadBindingsViaAlternateLoader(); createThreadBindingManager({ cfg: EMPTY_DISCORD_TEST_CONFIG, @@ -31,6 +31,6 @@ describe("thread binding manager state", () => { }); expect(getThreadBindingManager("work")).not.toBeNull(); - expect(viaJiti.getThreadBindingManager("work")).not.toBeNull(); + expect(viaAlternateLoader.getThreadBindingManager("work")).not.toBeNull(); }); }); diff --git a/extensions/discord/src/monitor/thread-bindings.state.ts b/extensions/discord/src/monitor/thread-bindings.state.ts index 7f3e4a98070..6a0ce04a2c2 100644 --- a/extensions/discord/src/monitor/thread-bindings.state.ts +++ b/extensions/discord/src/monitor/thread-bindings.state.ts @@ -32,8 +32,9 @@ type ThreadBindingsGlobalState = { lastPersistedAtMs: number; }; -// Plugin hooks can load this module via Jiti while core imports it via ESM. -// Store mutable state on globalThis so both loader paths share one registry. +// Plugin hooks can load this module through a separate runtime path while core +// imports it via ESM. Store mutable state on globalThis so both paths share one +// registry. const THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.discordThreadBindingsState"); let threadBindingsState: ThreadBindingsGlobalState | undefined; diff --git a/extensions/matrix/CHANGELOG.md b/extensions/matrix/CHANGELOG.md index 7793ebf1044..cab206b0f18 100644 --- a/extensions/matrix/CHANGELOG.md +++ b/extensions/matrix/CHANGELOG.md @@ -192,7 +192,7 @@ collapsed here. ### Fixes - Plugins/bundled runtimes: ship bundled plugin runtime sidecars like WhatsApp `light-runtime-api.js`, Matrix `runtime-api.js`, and other plugin runtime entry files in the npm package again, so global installs stop failing on missing bundled plugin runtime surfaces. -- Plugins/Matrix: avoid duplicate `resolveMatrixAccountStringValues` runtime-api exports under Jiti so bundled Matrix installs no longer crash at startup with `Cannot redefine property: resolveMatrixAccountStringValues`. Fixes #52909 and #52891. Thanks @vincentkoc. +- Plugins/Matrix: avoid duplicate `resolveMatrixAccountStringValues` runtime-api exports under source loaders so bundled Matrix installs no longer crash at startup with `Cannot redefine property: resolveMatrixAccountStringValues`. Fixes #52909 and #52891. Thanks @vincentkoc. ## 2026.3.22 diff --git a/extensions/matrix/src/plugin-entry.runtime.test.ts b/extensions/matrix/src/plugin-entry.runtime.test.ts index 0f1fcfd622d..cdac1ae7916 100644 --- a/extensions/matrix/src/plugin-entry.runtime.test.ts +++ b/extensions/matrix/src/plugin-entry.runtime.test.ts @@ -123,16 +123,11 @@ it("loads the packaged runtime wrapper without recursing through the stable root ); }, 240_000); -it("does not use Jiti when only a TypeScript Matrix runtime shim exists", async () => { +it("does not load when only a TypeScript Matrix runtime shim exists", async () => { const fixtureRoot = makeFixtureRoot(".tmp-matrix-runtime-ts-only-"); writeOpenClawPackageFixture(fixtureRoot); writeSourceRuntimeWrapperFixture(fixtureRoot, { runtimeExtension: ".ts" }); - writeFixtureFile( - fixtureRoot, - "node_modules/jiti/index.js", - "throw new Error('matrix wrapper must not require jiti');\n", - ); await expect( importFixtureModule(fixtureRoot, "extensions/matrix/src/plugin-entry.runtime.js"), diff --git a/extensions/zalo/runtime-api.test.ts b/extensions/zalo/runtime-api.test.ts index ccbeb17c5ec..accd0858c8d 100644 --- a/extensions/zalo/runtime-api.test.ts +++ b/extensions/zalo/runtime-api.test.ts @@ -1,19 +1,17 @@ -import path from "node:path"; -import { loadRuntimeApiExportTypesViaJiti } from "openclaw/plugin-sdk/plugin-test-contracts"; +import { runDirectImportSmoke } from "openclaw/plugin-sdk/plugin-test-contracts"; import { describe, expect, it } from "vitest"; describe("zalo runtime api", () => { - it("loads the narrow runtime api without reentering setup surfaces", () => { - const runtimeApiPath = path.join(process.cwd(), "extensions", "zalo", "runtime-api.ts"); + it("loads the narrow runtime api without reentering setup surfaces", async () => { + const stdout = await runDirectImportSmoke( + `const runtime = await import("./extensions/zalo/runtime-api.ts"); +process.stdout.write(JSON.stringify({ + hasZaloPlugin: Object.hasOwn(runtime, "zaloPlugin"), + hasZaloSetupWizard: Object.hasOwn(runtime, "zaloSetupWizard"), + type: typeof runtime.setZaloRuntime, +}));`, + ); - expect( - loadRuntimeApiExportTypesViaJiti({ - modulePath: runtimeApiPath, - exportNames: ["setZaloRuntime"], - realPluginSdkSpecifiers: ["openclaw/plugin-sdk/runtime-store"], - }), - ).toEqual({ - setZaloRuntime: "function", - }); - }); + expect(stdout).toBe('{"hasZaloPlugin":false,"hasZaloSetupWizard":false,"type":"function"}'); + }, 45_000); }); diff --git a/scripts/check-no-extension-test-core-imports.ts b/scripts/check-no-extension-test-core-imports.ts index c894063f4c3..c7729ca74a2 100644 --- a/scripts/check-no-extension-test-core-imports.ts +++ b/scripts/check-no-extension-test-core-imports.ts @@ -100,7 +100,6 @@ const RETIRED_EXTENSION_TEST_HELPER_BRIDGE_FILES = [ "test/helpers/plugins/contracts-testkit.ts", "test/helpers/plugins/direct-smoke.ts", "test/helpers/plugins/directory.ts", - "test/helpers/plugins/jiti-runtime-api.ts", "test/helpers/plugins/onboard-config.ts", "test/helpers/plugins/outbound-delivery.ts", "test/helpers/plugins/package-manifest-contract.ts", diff --git a/src/plugin-sdk/plugin-test-contracts.ts b/src/plugin-sdk/plugin-test-contracts.ts index 7e8c8ee49c3..37dd3c7d0e3 100644 --- a/src/plugin-sdk/plugin-test-contracts.ts +++ b/src/plugin-sdk/plugin-test-contracts.ts @@ -8,7 +8,6 @@ export { uniqueSortedStrings, } from "./test-helpers/contracts-testkit.js"; export { runDirectImportSmoke } from "./test-helpers/direct-smoke.js"; -export { loadRuntimeApiExportTypesViaJiti } from "./test-helpers/jiti-runtime-api.js"; export { describePackageManifestContract } from "./test-helpers/package-manifest-contract.js"; export { pluginRegistrationContractCases } from "./test-helpers/plugin-registration-contract-cases.js"; export { describePluginRegistrationContract } from "./test-helpers/plugin-registration-contract.js"; diff --git a/src/plugin-sdk/test-helpers/jiti-runtime-api.ts b/src/plugin-sdk/test-helpers/jiti-runtime-api.ts deleted file mode 100644 index 60d2ca97002..00000000000 --- a/src/plugin-sdk/test-helpers/jiti-runtime-api.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; -import { createRequire } from "node:module"; -import path from "node:path"; - -const nodeRequire = createRequire(import.meta.url); - -function loadTypeScript(): typeof import("typescript") { - return nodeRequire("typescript") as typeof import("typescript"); -} - -const JITI_EXTENSIONS = [ - ".ts", - ".tsx", - ".mts", - ".cts", - ".mtsx", - ".ctsx", - ".js", - ".mjs", - ".cjs", - ".json", -] as const; - -const PLUGIN_SDK_SPECIFIER_PREFIX = "openclaw/plugin-sdk/"; -const SOURCE_MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"] as const; - -type SourceModuleRef = { - specifier: string; - typeOnly: boolean; -}; - -function listPluginSdkExportedSubpaths(root: string): string[] { - const packageJsonPath = path.join(root, "package.json"); - const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { - exports?: Record; - }; - return Object.keys(packageJson.exports ?? {}) - .filter((key) => key.startsWith("./plugin-sdk/")) - .map((key) => key.slice("./plugin-sdk/".length)); -} - -function resolvePluginSdkAliasTarget(root: string, subpath: string): string | null { - const distCandidate = path.join(root, "dist", "plugin-sdk", `${subpath}.js`); - if (existsSync(distCandidate)) { - return distCandidate; - } - - for (const ext of SOURCE_MODULE_EXTENSIONS) { - const srcCandidate = path.join(root, "src", "plugin-sdk", `${subpath}${ext}`); - if (existsSync(srcCandidate)) { - return srcCandidate; - } - } - - return null; -} - -function resolveLocalModulePath(filePath: string, specifier: string): string | null { - const basePath = path.resolve(path.dirname(filePath), specifier); - const candidates = new Set([basePath]); - - for (const ext of SOURCE_MODULE_EXTENSIONS) { - candidates.add(`${basePath}${ext}`); - } - - if (/\.[cm]?[jt]sx?$/u.test(basePath)) { - const withoutExt = basePath.replace(/\.[cm]?[jt]sx?$/u, ""); - for (const ext of SOURCE_MODULE_EXTENSIONS) { - candidates.add(`${withoutExt}${ext}`); - } - } - - for (const ext of SOURCE_MODULE_EXTENSIONS) { - candidates.add(path.join(basePath, `index${ext}`)); - } - - for (const candidate of candidates) { - if (existsSync(candidate)) { - return candidate; - } - } - - return null; -} - -function collectSourceModuleRefs(filePath: string): SourceModuleRef[] { - const ts = loadTypeScript(); - const sourceText = readFileSync(filePath, "utf8"); - const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); - const refs: SourceModuleRef[] = []; - - for (const statement of sourceFile.statements) { - if (ts.isImportDeclaration(statement)) { - const specifier = - statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) - ? statement.moduleSpecifier.text - : undefined; - if (specifier) { - refs.push({ - specifier, - typeOnly: Boolean(statement.importClause?.isTypeOnly), - }); - } - continue; - } - - if (!ts.isExportDeclaration(statement)) { - continue; - } - - const specifier = - statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) - ? statement.moduleSpecifier.text - : undefined; - if (!specifier) { - continue; - } - - const typeOnly = Boolean( - statement.isTypeOnly || - (statement.exportClause && - ts.isNamedExports(statement.exportClause) && - statement.exportClause.elements.length > 0 && - statement.exportClause.elements.every((element) => element.isTypeOnly)), - ); - - refs.push({ specifier, typeOnly }); - } - - return refs; -} - -function collectPluginSdkAliases(params: { - modulePath: string; - root: string; - realPluginSdkSpecifiers?: readonly string[]; -}): Record { - const realSpecifiers = new Set(); - const stubSpecifiers = new Set(); - const visitedFiles = new Set(); - const stubPath = path.join(params.root, "test", "helpers", "plugins", "plugin-sdk-stub.cjs"); - const explicitRealSpecifiers = new Set(params.realPluginSdkSpecifiers ?? []); - - function visitModule(filePath: string, rootModule: boolean): void { - if (visitedFiles.has(filePath)) { - return; - } - visitedFiles.add(filePath); - - for (const ref of collectSourceModuleRefs(filePath)) { - if (ref.specifier.startsWith(PLUGIN_SDK_SPECIFIER_PREFIX)) { - const shouldKeepReal = - rootModule && - !ref.typeOnly && - (explicitRealSpecifiers.size === 0 || explicitRealSpecifiers.has(ref.specifier)); - if (shouldKeepReal) { - realSpecifiers.add(ref.specifier); - const subpath = ref.specifier.slice(PLUGIN_SDK_SPECIFIER_PREFIX.length); - const target = resolvePluginSdkAliasTarget(params.root, subpath); - if (target?.endsWith(".ts")) { - visitModule(target, false); - } - } else { - stubSpecifiers.add(ref.specifier); - } - continue; - } - - if (!ref.specifier.startsWith(".")) { - continue; - } - - const resolved = resolveLocalModulePath(filePath, ref.specifier); - if (resolved) { - visitModule(resolved, false); - } - } - } - - visitModule(params.modulePath, true); - - const aliasEntries = new Map(); - for (const specifier of listPluginSdkExportedSubpaths(params.root).map( - (subpath) => `${PLUGIN_SDK_SPECIFIER_PREFIX}${subpath}`, - )) { - if (realSpecifiers.has(specifier)) { - const subpath = specifier.slice(PLUGIN_SDK_SPECIFIER_PREFIX.length); - aliasEntries.set(specifier, resolvePluginSdkAliasTarget(params.root, subpath) ?? stubPath); - continue; - } - if (stubSpecifiers.has(specifier)) { - aliasEntries.set(specifier, stubPath); - } - } - - return Object.fromEntries(aliasEntries); -} - -export function loadRuntimeApiExportTypesViaJiti(params: { - modulePath: string; - exportNames: readonly string[]; - additionalAliases?: Record; - realPluginSdkSpecifiers?: readonly string[]; -}): Record { - const root = process.cwd(); - const alias = { - ...collectPluginSdkAliases({ - modulePath: params.modulePath, - root, - realPluginSdkSpecifiers: params.realPluginSdkSpecifiers, - }), - ...params.additionalAliases, - }; - - const script = ` -import path from "node:path"; -import { createJiti } from "jiti"; - -const modulePath = ${JSON.stringify(params.modulePath)}; -const exportNames = ${JSON.stringify(params.exportNames)}; -const alias = ${JSON.stringify(alias)}; -const jiti = createJiti(path.join(${JSON.stringify(root)}, "openclaw.mjs"), { - interopDefault: true, - tryNative: false, - fsCache: false, - moduleCache: false, - extensions: ${JSON.stringify(JITI_EXTENSIONS)}, - alias, -}); -const mod = jiti(modulePath); -console.log( - JSON.stringify( - Object.fromEntries(exportNames.map((name) => [name, typeof mod[name]])), - ), -); -`; - - const raw = execFileSync(process.execPath, ["--input-type=module", "--eval", script], { - cwd: root, - encoding: "utf-8", - }); - - return JSON.parse(raw) as Record; -}