Files
openclaw/src/plugins/plugin-module-loader-cache.test.ts
Jason (Json) 1610b4983f fix: scope jiti transform cache by OpenClaw install
Scope jiti filesystem transform caches for OpenClaw plugin loaders by package version and package.json install metadata so stale transforms cannot survive upgrades or package reinstalls.

Covers the central plugin module loader and the plugin SDK root alias CJS loader, while preserving jiti filesystem-cache env opt-outs and the TMPDIR cwd guard.

Verification: CI run 26601117143 passed; Real behavior proof run 26601445285 passed; CodeQL selected checks passed in run 26601117126; CodeQL Critical Quality plugin-boundary and plugin-sdk-package-contract passed in run 26601117074; OpenGrep PR diff passed in run 26601117137.

Refs: https://github.com/openclaw/openclaw/pull/87745
Thanks @fuller-stack-dev.
2026-05-29 00:17:04 +01:00

803 lines
31 KiB
TypeScript

import path from "node:path";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { PluginModuleLoaderFactory } from "./plugin-module-loader-cache.js";
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
vi.doUnmock("jiti");
});
async function loadCachedPluginModuleLoader(scope: string) {
const createJiti = vi.fn((filename: string, options?: Record<string, unknown>) =>
Object.assign(vi.fn(), {
filename,
options,
}),
);
const pluginModuleLoaderCache = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, `./plugin-module-loader-cache.js?scope=${scope}`);
const getCachedPluginModuleLoader: typeof pluginModuleLoaderCache.getCachedPluginModuleLoader = (
params,
) =>
pluginModuleLoaderCache.getCachedPluginModuleLoader({
...params,
createLoader: params.createLoader ?? asPluginModuleLoaderFactory(createJiti),
});
return { createJiti, getCachedPluginModuleLoader };
}
function asPluginModuleLoaderFactory(factory: unknown): PluginModuleLoaderFactory {
return factory as PluginModuleLoaderFactory;
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object") {
throw new Error(`expected ${label}`);
}
return value as Record<string, unknown>;
}
function callArg(mock: unknown, callIndex: number, argIndex: number, label: string) {
const calls = (mock as { mock?: { calls?: Array<Array<unknown>> } }).mock?.calls ?? [];
const call = calls.at(callIndex);
if (!call) {
throw new Error(`${label} call ${callIndex} was missing`);
}
return call[argIndex];
}
function expectJitiOptions(
mock: unknown,
callIndex: number,
filename: string,
fields: Record<string, unknown>,
) {
expect(callArg(mock, callIndex, 0, "jiti filename")).toBe(filename);
const options = requireRecord(callArg(mock, callIndex, 1, "jiti options"), "jiti options");
for (const [key, expected] of Object.entries(fields)) {
expect(options[key]).toBe(expected);
}
return options;
}
function expectNativeOptions(mock: unknown, target: string) {
expect(callArg(mock, 0, 0, "native target")).toBe(target);
const options = requireRecord(callArg(mock, 0, 1, "native options"), "native options");
expect(options.allowWindows).toBe(true);
expect(options.fallbackOnMissingDependency).toBe(true);
expect(options.fallbackOnNativeError).toBe(true);
}
function expectStats(value: unknown, fields: Record<string, unknown>) {
const stats = requireRecord(value, "loader stats");
for (const [key, expected] of Object.entries(fields)) {
expect(stats[key]).toEqual(expected);
}
return stats;
}
describe("getCachedPluginModuleLoader", () => {
it("resolves deterministic cache entries for equivalent alias maps", async () => {
const { resolvePluginModuleLoaderCacheEntry } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=cache-entry-alias-order");
const first = resolvePluginModuleLoaderCacheEntry({
modulePath: "/repo/extensions/demo/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/src/plugins/loader.ts",
aliasMap: {
alpha: "/repo/alpha.js",
zeta: "/repo/zeta.js",
},
tryNative: false,
});
const second = resolvePluginModuleLoaderCacheEntry({
modulePath: "/repo/extensions/demo/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/src/plugins/loader.ts",
aliasMap: {
zeta: "/repo/zeta.js",
alpha: "/repo/alpha.js",
},
tryNative: false,
});
expect(second.cacheKey).toBe(first.cacheKey);
expect(second.scopedCacheKey).toBe(first.scopedCacheKey);
expect(first.loaderFilename).toBe("/repo/src/plugins/loader.ts");
});
it("keeps explicit shared cache scope keys independent of loader options", async () => {
const { resolvePluginModuleLoaderCacheEntry } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=cache-entry-shared-scope");
const first = resolvePluginModuleLoaderCacheEntry({
modulePath: "/repo/dist/extensions/demo-a/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "/repo/src/plugins/public-surface-loader.ts",
aliasMap: { demo: "/repo/demo-a.js" },
tryNative: true,
sharedCacheScopeKey: "bundled:native",
});
const second = resolvePluginModuleLoaderCacheEntry({
modulePath: "/repo/dist/extensions/demo-b/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "/repo/src/plugins/public-surface-loader.ts",
aliasMap: { demo: "/repo/demo-b.js" },
tryNative: false,
sharedCacheScopeKey: "bundled:native",
});
expect(first.cacheKey).not.toBe(second.cacheKey);
expect(first.scopedCacheKey).toBe(second.scopedCacheKey);
});
it("reuses cached loaders for the same module config and filename", async () => {
const { createJiti, getCachedPluginModuleLoader } =
await loadCachedPluginModuleLoader("cached-loader");
const cache = new Map();
const params = {
cache,
modulePath: "/repo/extensions/demo/index.ts",
importerUrl: "file:///repo/src/plugins/setup-registry.ts",
argvEntry: "/repo/openclaw.mjs",
loaderFilename: "file:///repo/src/plugins/source-loader.ts",
} as const;
const first = getCachedPluginModuleLoader(params);
const second = getCachedPluginModuleLoader(params);
expect(second).toBe(first);
first("/repo/extensions/demo/index.ts");
expect(createJiti).toHaveBeenCalledTimes(1);
expect(cache.size).toBe(1);
});
it("creates bounded loader caches", async () => {
const { createJiti, getCachedPluginModuleLoader } =
await loadCachedPluginModuleLoader("bounded-loader-cache");
const { createPluginModuleLoaderCache } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=bounded-loader-cache-factory");
const cache = createPluginModuleLoaderCache(1);
const first = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo-a/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/extensions/demo-a/index.ts",
});
getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo-b/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/extensions/demo-b/index.ts",
});
const reloadedFirst = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo-a/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/extensions/demo-a/index.ts",
});
expect(cache.size).toBe(1);
expect(reloadedFirst).not.toBe(first);
reloadedFirst("/repo/extensions/demo-a/index.ts");
expect(createJiti).toHaveBeenCalledOnce();
});
it("keeps loader caches scoped by loader filename and dist preference", async () => {
const { createJiti, getCachedPluginModuleLoader } =
await loadCachedPluginModuleLoader("filename-scope");
const cache = new Map();
const first = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.ts",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
argvEntry: "/repo/openclaw.mjs",
preferBuiltDist: true,
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
});
const second = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.ts",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
argvEntry: "/repo/openclaw.mjs",
preferBuiltDist: true,
loaderFilename: "file:///repo/src/plugins/bundled-channel-config-metadata.ts",
});
expect(second).not.toBe(first);
first("/repo/dist/extensions/demo/api.ts");
second("/repo/dist/extensions/demo/api.ts");
const firstOptions = expectJitiOptions(
createJiti,
0,
"file:///repo/src/plugins/public-surface-loader.ts",
{ tryNative: false, interopDefault: true },
);
expect(firstOptions.alias).toBeTypeOf("object");
const secondOptions = expectJitiOptions(
createJiti,
1,
"file:///repo/src/plugins/bundled-channel-config-metadata.ts",
{ tryNative: false, interopDefault: true },
);
expect(secondOptions.alias).toBeTypeOf("object");
expect(cache.size).toBe(2);
});
it("lets callers override alias maps and tryNative while keeping cache keys stable", async () => {
const { createJiti, getCachedPluginModuleLoader } =
await loadCachedPluginModuleLoader("overrides");
const cache = new Map();
const first = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "file:///repo/src/plugins/loader.ts",
aliasMap: {
alpha: "/repo/alpha.js",
zeta: "/repo/zeta.js",
},
tryNative: false,
});
const second = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "file:///repo/src/plugins/loader.ts",
aliasMap: {
zeta: "/repo/zeta.js",
alpha: "/repo/alpha.js",
},
tryNative: false,
});
expect(second).toBe(first);
first("/repo/extensions/demo/index.ts");
expect(createJiti).toHaveBeenCalledTimes(1);
const options = expectJitiOptions(createJiti, 0, "file:///repo/src/plugins/loader.ts", {
tryNative: false,
});
expect(options.fsCache).toEqual(expect.any(String));
expect(String(options.fsCache)).toContain(`${path.sep}jiti${path.sep}openclaw${path.sep}`);
expect(options.alias).toEqual({
alpha: "/repo/alpha.js",
zeta: "/repo/zeta.js",
});
});
it("keeps cache scope keys separated by loader options", async () => {
const { createJiti, getCachedPluginModuleLoader } =
await loadCachedPluginModuleLoader("cache-scope-key");
const cache = new Map();
const first = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo-a/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
aliasMap: {
demo: "/repo/demo-a.js",
},
tryNative: true,
cacheScopeKey: "bundled:native",
});
const second = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo-b/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
aliasMap: {
demo: "/repo/demo-b.js",
},
tryNative: true,
cacheScopeKey: "bundled:native",
});
expect(second).not.toBe(first);
first("/repo/dist/extensions/demo-a/api.js");
second("/repo/dist/extensions/demo-b/api.js");
expect(createJiti).toHaveBeenCalledTimes(2);
expect(cache.size).toBe(2);
});
it("lets callers explicitly share loaders behind an unsafe shared cache scope key", async () => {
const { createJiti, getCachedPluginModuleLoader } =
await loadCachedPluginModuleLoader("shared-cache-scope-key");
const cache = new Map();
const first = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo-a/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
aliasMap: {
demo: "/repo/demo-a.js",
},
tryNative: true,
sharedCacheScopeKey: "bundled:native",
});
const second = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo-b/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
aliasMap: {
demo: "/repo/demo-b.js",
},
tryNative: true,
sharedCacheScopeKey: "bundled:native",
});
expect(second).toBe(first);
second("/repo/dist/extensions/demo-b/api.js");
expect(createJiti).toHaveBeenCalledTimes(1);
expect(cache.size).toBe(1);
});
it("reuses pre-normalized alias options across module-scoped loader filenames", async () => {
const { createJiti, getCachedPluginModuleLoader } =
await loadCachedPluginModuleLoader("module-filename-aliases");
const cache = new Map();
getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo-a/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/extensions/demo-a/index.ts",
aliasMap: {
alpha: "/repo/alpha",
beta: "alpha/sub",
},
tryNative: false,
});
getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo-b/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/extensions/demo-b/index.ts",
aliasMap: {
beta: "alpha/sub",
alpha: "/repo/alpha",
},
tryNative: false,
});
getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo-a/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/extensions/demo-a/index.ts",
aliasMap: {
alpha: "/repo/alpha",
beta: "alpha/sub",
},
tryNative: false,
})("/repo/extensions/demo-a/index.ts");
getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo-b/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
loaderFilename: "/repo/extensions/demo-b/index.ts",
aliasMap: {
beta: "alpha/sub",
alpha: "/repo/alpha",
},
tryNative: false,
})("/repo/extensions/demo-b/index.ts");
const marker = Symbol.for("pathe:normalizedAlias");
const firstAlias = (
callArg(createJiti, 0, 1, "first jiti options") as {
alias?: Record<string, string>;
}
).alias;
const secondAlias = (
callArg(createJiti, 1, 1, "second jiti options") as {
alias?: Record<string, string>;
}
).alias;
expect(createJiti).toHaveBeenCalledTimes(2);
expect(cache.size).toBe(2);
expect(secondAlias).toBe(firstAlias);
expect(firstAlias?.beta).toBe("/repo/alpha/sub");
expect((firstAlias as Record<symbol, unknown>)[marker]).toBe(true);
});
it("serves compiled .js targets from native require without invoking the module loader", async () => {
const fromSourceTransformer = vi.fn();
const createJiti = vi.fn(() => fromSourceTransformer);
const nativeStub = vi.fn((target: string) => ({
ok: true as const,
moduleExport: { loadedFrom: target },
}));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: (p: string) =>
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-fastpath");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
createLoader: asPluginModuleLoaderFactory(createJiti),
});
const result = loader("/repo/dist/extensions/demo/api.js") as { loadedFrom: string };
expect(result.loadedFrom).toBe("/repo/dist/extensions/demo/api.js");
// Jiti should not be constructed or invoked for .js targets that
// `tryNativeRequireJavaScriptModule` resolves.
expect(createJiti).not.toHaveBeenCalled();
expect(fromSourceTransformer).not.toHaveBeenCalled();
// allowWindows must be passed so the native fast path works on Windows too.
expectNativeOptions(nativeStub, "/repo/dist/extensions/demo/api.js");
expectStats(getPluginModuleLoaderStats(), {
calls: 1,
nativeHits: 1,
nativeMisses: 0,
sourceTransformFallbacks: 0,
sourceTransformForced: 0,
});
});
it("lets native require handle compiled plugin SDK aliases before source-transform fallback", async () => {
const fromSourceTransformer = vi.fn();
const createJiti = vi.fn(() => fromSourceTransformer);
const nativeStub = vi.fn((target: string) => ({
ok: true as const,
moduleExport: { loadedFrom: target },
}));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: (p: string) =>
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-plugin-sdk-alias");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
aliasMap: {
"openclaw/plugin-sdk": "/repo/dist/plugin-sdk/root-alias.cjs",
"openclaw/plugin-sdk/core": "/repo/dist/plugin-sdk/core.js",
},
createLoader: asPluginModuleLoaderFactory(createJiti),
});
const result = loader("/repo/dist/extensions/demo/api.js") as { loadedFrom: string };
expect(result.loadedFrom).toBe("/repo/dist/extensions/demo/api.js");
expect(createJiti).not.toHaveBeenCalled();
expect(fromSourceTransformer).not.toHaveBeenCalled();
expectNativeOptions(nativeStub, "/repo/dist/extensions/demo/api.js");
const options = callArg(nativeStub, 0, 1, "native options") as {
aliasMap?: Record<string, string>;
};
expect(options.aliasMap?.["openclaw/plugin-sdk/core"]).toBe("/repo/dist/plugin-sdk/core.js");
expectStats(getPluginModuleLoaderStats(), {
calls: 1,
nativeHits: 1,
nativeMisses: 0,
sourceTransformFallbacks: 0,
sourceTransformForced: 0,
});
});
it("reuses successful native module exports inside one loader", async () => {
const fromSourceTransformer = vi.fn();
const createJiti = vi.fn(() => fromSourceTransformer);
const moduleExport = { marker: "native-cached" };
const nativeStub = vi.fn(() => ({
ok: true as const,
moduleExport,
}));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-export-cache");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
createLoader: asPluginModuleLoaderFactory(createJiti),
});
expect(loader("/repo/dist/extensions/demo/api.js")).toBe(moduleExport);
expect(loader("/repo/dist/extensions/demo/api.js")).toBe(moduleExport);
expect(nativeStub).toHaveBeenCalledTimes(1);
expect(createJiti).not.toHaveBeenCalled();
expectStats(getPluginModuleLoaderStats(), {
calls: 1,
nativeHits: 1,
nativeMisses: 0,
sourceTransformFallbacks: 0,
sourceTransformForced: 0,
});
});
it("does not source-transform fallback after native loading reaches a missing dependency", async () => {
const fromSourceTransformer = vi.fn();
const createJiti = vi.fn(() => fromSourceTransformer);
vi.doMock("jiti", () => ({ createJiti }));
const missingDependency = Object.assign(new Error("Cannot find module 'missing-dep'"), {
code: "MODULE_NOT_FOUND",
});
const nativeStub = vi.fn(() => {
throw missingDependency;
});
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-missing-dependency");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
createLoader: asPluginModuleLoaderFactory(createJiti),
});
expect(() => loader("/repo/dist/extensions/demo/api.js")).toThrow("missing-dep");
expect(createJiti).not.toHaveBeenCalled();
expect(fromSourceTransformer).not.toHaveBeenCalled();
expectNativeOptions(nativeStub, "/repo/dist/extensions/demo/api.js");
expectStats(getPluginModuleLoaderStats(), {
calls: 1,
nativeHits: 0,
nativeMisses: 0,
sourceTransformFallbacks: 0,
sourceTransformForced: 0,
});
});
it("falls back to source transform when the native-require helper declines", async () => {
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
const createJiti = vi.fn(() => fromSourceTransformer);
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
}));
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-fallback");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
createLoader: asPluginModuleLoaderFactory(createJiti),
});
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
expect(result.fromSourceTransform).toBe(true);
expectJitiOptions(createJiti, 0, "file:///repo/src/plugins/public-surface-loader.ts", {
tryNative: true,
});
expect(fromSourceTransformer).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
const stats = expectStats(getPluginModuleLoaderStats(), {
calls: 1,
nativeHits: 0,
nativeMisses: 1,
sourceTransformFallbacks: 1,
sourceTransformForced: 0,
});
expect(stats.topSourceTransformTargets).toEqual([
{ target: "/repo/dist/extensions/demo/api.js", count: 1 },
]);
});
it("normalizes Windows absolute paths before creating and calling the source transformer", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
const createJiti = vi.fn(() => fromSourceTransformer);
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
}));
const { getCachedPluginModuleLoader } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=windows-jiti-paths");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
importerUrl: "file:///C:/Users/alice/openclaw/dist/src/plugins/public-surface-loader.js",
loaderFilename: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
tryNative: true,
createLoader: asPluginModuleLoaderFactory(createJiti),
});
loader("C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js");
expectJitiOptions(
createJiti,
0,
"file:///C:/Users/alice/openclaw/dist/extensions/feishu/api.js",
{ tryNative: true },
);
expect(fromSourceTransformer).toHaveBeenCalledWith(
"file:///C:/Users/alice/openclaw/dist/extensions/feishu/api.js",
);
});
it("skips the native-require fast path when tryNative is explicitly false", async () => {
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
const createJiti = vi.fn(() => fromSourceTransformer);
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-opt-out");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/bundled-capability-runtime.ts",
loaderFilename: "file:///repo/src/plugins/bundled-capability-runtime.ts",
aliasMap: { "openclaw/plugin-sdk": "/repo/shim.js" },
tryNative: false,
createLoader: asPluginModuleLoaderFactory(createJiti),
});
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
expect(result.fromSourceTransform).toBe(true);
// With tryNative: false the wrapper must route every target through the source transformer
// so its alias rewrites still apply; native require must not be consulted.
expect(nativeStub).not.toHaveBeenCalled();
expect(fromSourceTransformer).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
const stats = expectStats(getPluginModuleLoaderStats(), {
calls: 1,
nativeHits: 0,
nativeMisses: 0,
sourceTransformFallbacks: 0,
sourceTransformForced: 1,
});
expect(stats.topSourceTransformTargets).toEqual([
{ target: "/repo/dist/extensions/demo/api.js", count: 1 },
]);
});
it("reuses successful source-transform module exports inside one loader", async () => {
const moduleExport = { marker: "source-cached" };
const fromSourceTransformer = vi.fn(() => moduleExport);
const createJiti = vi.fn(() => fromSourceTransformer);
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=source-export-cache");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/extensions/demo/api.ts",
importerUrl: "file:///repo/src/plugins/bundled-capability-runtime.ts",
loaderFilename: "file:///repo/src/plugins/bundled-capability-runtime.ts",
tryNative: false,
createLoader: asPluginModuleLoaderFactory(createJiti),
});
expect(loader("/repo/extensions/demo/api.ts")).toBe(moduleExport);
expect(loader("/repo/extensions/demo/api.ts")).toBe(moduleExport);
expect(nativeStub).not.toHaveBeenCalled();
expect(fromSourceTransformer).toHaveBeenCalledTimes(1);
const stats = expectStats(getPluginModuleLoaderStats(), {
calls: 1,
nativeHits: 0,
nativeMisses: 0,
sourceTransformFallbacks: 0,
sourceTransformForced: 1,
});
expect(stats.topSourceTransformTargets).toEqual([
{ target: "/repo/extensions/demo/api.ts", count: 1 },
]);
});
it("normalizes Windows absolute paths when native loading is disabled", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
const createJiti = vi.fn(() => fromSourceTransformer);
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: nativeStub,
}));
const { getCachedPluginModuleLoader } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=windows-jiti-no-native");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
importerUrl: "file:///C:/Users/alice/openclaw/src/plugins/loader.ts",
loaderFilename: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
tryNative: false,
createLoader: asPluginModuleLoaderFactory(createJiti),
});
loader("C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts");
expect(nativeStub).not.toHaveBeenCalled();
expectJitiOptions(createJiti, 0, "file:///C:/Users/alice/openclaw/extensions/feishu/api.ts", {
tryNative: false,
});
expect(fromSourceTransformer).toHaveBeenCalledWith(
"file:///C:/Users/alice/openclaw/extensions/feishu/api.ts",
);
});
it("forwards extra loader arguments through to the source-transform fallback", async () => {
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
const createJiti = vi.fn(() => fromSourceTransformer);
vi.doMock("./native-module-require.js", () => ({
isJavaScriptModulePath: () => true,
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
}));
const { getCachedPluginModuleLoader } = await importFreshModule<
typeof import("./plugin-module-loader-cache.js")
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-rest-args");
const cache = new Map();
const loader = getCachedPluginModuleLoader({
cache,
modulePath: "/repo/dist/extensions/demo/api.js",
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
createLoader: asPluginModuleLoaderFactory(createJiti),
});
const loose = loader as unknown as (t: string, ...a: unknown[]) => unknown;
loose("/repo/dist/extensions/demo/api.js", { hint: "x" }, 42);
expect(fromSourceTransformer).toHaveBeenCalledWith(
"/repo/dist/extensions/demo/api.js",
{ hint: "x" },
42,
);
});
});