mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:30:47 +00:00
fix: preserve source plugin loading fallbacks
This commit is contained in:
@@ -133,7 +133,7 @@ const knownDeprecatedSurfaceMarkers = [
|
||||
{
|
||||
code: "legacy-root-sdk-import",
|
||||
file: "src/plugin-sdk/compat.ts",
|
||||
marker: "@deprecated Use `openclaw/plugin-sdk/channel-reply-pipeline`.",
|
||||
marker: "@deprecated Use `openclaw/plugin-sdk/channel-message`.",
|
||||
},
|
||||
{
|
||||
code: "channel-route-key-aliases",
|
||||
|
||||
@@ -47,7 +47,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export { missingTargetError } from "openclaw/plugin-sdk/channel-feedback";',
|
||||
'export { createAccountStatusSink, runPassiveAccountLifecycle } from "openclaw/plugin-sdk/channel-lifecycle";',
|
||||
'export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";',
|
||||
'export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";',
|
||||
'export { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";',
|
||||
'export { evaluateGroupRouteAccessForPolicy, resolveDmGroupAccessWithLists, resolveSenderScopedGroupPolicy } from "openclaw/plugin-sdk/channel-policy";',
|
||||
'export { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";',
|
||||
'export { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";',
|
||||
@@ -77,7 +77,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export { logTypingFailure } from "openclaw/plugin-sdk/channel-logging";',
|
||||
'export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";',
|
||||
'export { evaluateSenderGroupAccessForPolicy, readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithLists, resolveEffectiveAllowFromLists, resolveSenderScopedGroupPolicy, resolveToolsBySender } from "openclaw/plugin-sdk/channel-policy";',
|
||||
'export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";',
|
||||
'export { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-message";',
|
||||
'export { PAIRING_APPROVED_MESSAGE, buildProbeChannelStatusSummary, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/channel-status";',
|
||||
'export { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision } from "openclaw/plugin-sdk/channel-targets";',
|
||||
'export type { GroupPolicy, GroupToolPolicyConfig, MSTeamsChannelConfig, MSTeamsConfig, MSTeamsReplyStyle, MSTeamsTeamConfig, MarkdownTableMode, OpenClawConfig } from "openclaw/plugin-sdk/config-types";',
|
||||
@@ -133,7 +133,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export { readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithCommandGate } from "openclaw/plugin-sdk/channel-policy";',
|
||||
'export type { BlockStreamingCoalesceConfig, DmConfig, DmPolicy, GroupPolicy, GroupToolPolicyConfig, OpenClawConfig } from "openclaw/plugin-sdk/config-types";',
|
||||
'export { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";',
|
||||
'export { dispatchInboundReplyWithBase } from "openclaw/plugin-sdk/inbound-reply-dispatch";',
|
||||
'export { dispatchChannelMessageReplyWithBase } from "openclaw/plugin-sdk/channel-message";',
|
||||
'export type { OutboundReplyPayload } from "openclaw/plugin-sdk/reply-payload";',
|
||||
'export { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";',
|
||||
'export type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";',
|
||||
|
||||
@@ -1038,7 +1038,10 @@ describe("loadOpenClawPlugins", () => {
|
||||
});
|
||||
|
||||
const record = registry.plugins.find((entry) => entry.id === "discord");
|
||||
expect(record?.status, record?.error).toBe("loaded");
|
||||
expect(
|
||||
record?.status,
|
||||
JSON.stringify({ error: record?.error, diagnostics: registry.diagnostics }, null, 2),
|
||||
).toBe("loaded");
|
||||
});
|
||||
it("registers standalone text transforms", () => {
|
||||
useNoBundledPlugins();
|
||||
@@ -6592,7 +6595,10 @@ module.exports = {
|
||||
}),
|
||||
);
|
||||
const record = registry.plugins.find((entry) => entry.id === "legacy-root-import");
|
||||
expect(record?.status, record?.error).toBe("loaded");
|
||||
expect(
|
||||
record?.status,
|
||||
JSON.stringify({ error: record?.error, diagnostics: registry.diagnostics }, null, 2),
|
||||
).toBe("loaded");
|
||||
});
|
||||
|
||||
it("supports legacy plugins subscribing to diagnostic events from the root sdk", async () => {
|
||||
@@ -6640,7 +6646,10 @@ module.exports = {
|
||||
const record = registry.plugins.find(
|
||||
(entry) => entry.id === "legacy-root-diagnostic-listener",
|
||||
);
|
||||
expect(record?.status, record?.error).toBe("loaded");
|
||||
expect(
|
||||
record?.status,
|
||||
JSON.stringify({ error: record?.error, diagnostics: registry.diagnostics }, null, 2),
|
||||
).toBe("loaded");
|
||||
|
||||
emitDiagnosticEvent({
|
||||
type: "model.usage",
|
||||
|
||||
@@ -77,6 +77,20 @@ describe("tryNativeRequireJavaScriptModule", () => {
|
||||
).toEqual({ ok: false });
|
||||
});
|
||||
|
||||
it("declines missing dependency errors when the caller can use source transform fallback", () => {
|
||||
const dir = makeTempDir();
|
||||
const modulePath = path.join(dir, "plugin.cjs");
|
||||
fs.writeFileSync(modulePath, 'require("./helper.js");\n', "utf8");
|
||||
fs.writeFileSync(path.join(dir, "helper.ts"), "export const loaded = true;\n", "utf8");
|
||||
|
||||
expect(
|
||||
tryNativeRequireJavaScriptModule(modulePath, {
|
||||
allowWindows: true,
|
||||
fallbackOnNativeError: true,
|
||||
}),
|
||||
).toEqual({ ok: false });
|
||||
});
|
||||
|
||||
it("propagates real module evaluation errors instead of falling back", () => {
|
||||
const dir = makeTempDir();
|
||||
const modulePath = path.join(dir, "plugin.cjs");
|
||||
@@ -90,6 +104,23 @@ describe("tryNativeRequireJavaScriptModule", () => {
|
||||
"plugin exploded during native load",
|
||||
);
|
||||
});
|
||||
|
||||
it("declines real module evaluation errors when the caller can use source transform fallback", () => {
|
||||
const dir = makeTempDir();
|
||||
const modulePath = path.join(dir, "plugin.cjs");
|
||||
fs.writeFileSync(
|
||||
modulePath,
|
||||
'throw new Error("plugin exploded during native load");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(
|
||||
tryNativeRequireJavaScriptModule(modulePath, {
|
||||
allowWindows: true,
|
||||
fallbackOnNativeError: true,
|
||||
}),
|
||||
).toEqual({ ok: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("isJavaScriptModulePath", () => {
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { createRequire } from "node:module";
|
||||
import Module from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
type ResolveFilename = (
|
||||
request: string,
|
||||
parent: NodeJS.Module | undefined,
|
||||
isMain: boolean,
|
||||
options?: { paths?: string[] },
|
||||
) => string;
|
||||
const moduleWithResolver = Module as typeof Module & {
|
||||
_resolveFilename?: ResolveFilename;
|
||||
};
|
||||
|
||||
export function isJavaScriptModulePath(modulePath: string): boolean {
|
||||
return [".js", ".mjs", ".cjs"].includes(path.extname(modulePath).toLowerCase());
|
||||
@@ -33,7 +43,12 @@ function isSourceTransformFallbackError(error: unknown, modulePath: string): boo
|
||||
|
||||
export function tryNativeRequireJavaScriptModule(
|
||||
modulePath: string,
|
||||
options: { allowWindows?: boolean; fallbackOnMissingDependency?: boolean } = {},
|
||||
options: {
|
||||
allowWindows?: boolean;
|
||||
aliasMap?: Record<string, string>;
|
||||
fallbackOnMissingDependency?: boolean;
|
||||
fallbackOnNativeError?: boolean;
|
||||
} = {},
|
||||
): { ok: true; moduleExport: unknown } | { ok: false } {
|
||||
if (process.platform === "win32" && options.allowWindows !== true) {
|
||||
return { ok: false };
|
||||
@@ -42,19 +57,47 @@ export function tryNativeRequireJavaScriptModule(
|
||||
return { ok: false };
|
||||
}
|
||||
try {
|
||||
return { ok: true, moduleExport: nodeRequire(modulePath) };
|
||||
return { ok: true, moduleExport: requireWithOptionalAliases(modulePath, options.aliasMap) };
|
||||
} catch (error) {
|
||||
const code =
|
||||
error && typeof error === "object" ? (error as { code?: unknown }).code : undefined;
|
||||
if (
|
||||
!isSourceTransformFallbackError(error, modulePath) &&
|
||||
!(
|
||||
options.fallbackOnMissingDependency === true &&
|
||||
(code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND")
|
||||
)
|
||||
isSourceTransformFallbackError(error, modulePath) ||
|
||||
options.fallbackOnNativeError ||
|
||||
(options.fallbackOnMissingDependency === true &&
|
||||
(code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND"))
|
||||
) {
|
||||
throw error;
|
||||
return { ok: false };
|
||||
}
|
||||
return { ok: false };
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function requireWithOptionalAliases(
|
||||
modulePath: string,
|
||||
aliasMap: Record<string, string> | undefined,
|
||||
): unknown {
|
||||
return withNativeRequireAliases(aliasMap, () => nodeRequire(modulePath));
|
||||
}
|
||||
|
||||
export function withNativeRequireAliases<T>(
|
||||
aliasMap: Record<string, string> | undefined,
|
||||
run: () => T,
|
||||
): T {
|
||||
if (!aliasMap || Object.keys(aliasMap).length === 0 || !moduleWithResolver._resolveFilename) {
|
||||
return run();
|
||||
}
|
||||
const originalResolveFilename = moduleWithResolver._resolveFilename;
|
||||
moduleWithResolver._resolveFilename = ((request, parent, isMain, options) => {
|
||||
const aliasTarget = aliasMap[request];
|
||||
if (aliasTarget) {
|
||||
return aliasTarget;
|
||||
}
|
||||
return originalResolveFilename(request, parent, isMain, options);
|
||||
}) satisfies ResolveFilename;
|
||||
try {
|
||||
return run();
|
||||
} finally {
|
||||
moduleWithResolver._resolveFilename = originalResolveFilename;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,19 @@ async function loadCachedPluginModuleLoader(scope: string) {
|
||||
options,
|
||||
}),
|
||||
);
|
||||
vi.doMock("jiti", () => ({
|
||||
createJiti,
|
||||
}));
|
||||
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
const pluginModuleLoaderCache = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, `./plugin-module-loader-cache.js?scope=${scope}`);
|
||||
|
||||
const getCachedPluginModuleLoaderWithMock: typeof getCachedPluginModuleLoader = (params) =>
|
||||
getCachedPluginModuleLoader({
|
||||
const getCachedPluginModuleLoader: typeof pluginModuleLoaderCache.getCachedPluginModuleLoader = (
|
||||
params,
|
||||
) =>
|
||||
pluginModuleLoaderCache.getCachedPluginModuleLoader({
|
||||
...params,
|
||||
createLoader: params.createLoader ?? asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
return { createJiti, getCachedPluginModuleLoader: getCachedPluginModuleLoaderWithMock };
|
||||
return { createJiti, getCachedPluginModuleLoader };
|
||||
}
|
||||
|
||||
function asPluginModuleLoaderFactory(factory: unknown): PluginModuleLoaderFactory {
|
||||
@@ -372,8 +370,6 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
it("serves compiled .js targets from native require without invoking the module loader", async () => {
|
||||
const fromSourceTransformer = vi.fn();
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
const jitiModuleFactory = vi.fn(() => ({ createJiti }));
|
||||
vi.doMock("jiti", jitiModuleFactory);
|
||||
const nativeStub = vi.fn((target: string) => ({
|
||||
ok: true as const,
|
||||
moduleExport: { loadedFrom: target },
|
||||
@@ -400,13 +396,17 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
expect(result.loadedFrom).toBe("/repo/dist/extensions/demo/api.js");
|
||||
// Jiti should not be constructed or invoked for .js targets that
|
||||
// `tryNativeRequireJavaScriptModule` resolves.
|
||||
expect(jitiModuleFactory).not.toHaveBeenCalled();
|
||||
expect(createJiti).not.toHaveBeenCalled();
|
||||
expect(fromSourceTransformer).not.toHaveBeenCalled();
|
||||
// allowWindows must be passed so the native fast path works on Windows too.
|
||||
expect(nativeStub).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", {
|
||||
allowWindows: true,
|
||||
});
|
||||
expect(nativeStub).toHaveBeenCalledWith(
|
||||
"/repo/dist/extensions/demo/api.js",
|
||||
expect.objectContaining({
|
||||
allowWindows: true,
|
||||
fallbackOnMissingDependency: true,
|
||||
fallbackOnNativeError: true,
|
||||
}),
|
||||
);
|
||||
expect(getPluginModuleLoaderStats()).toMatchObject({
|
||||
calls: 1,
|
||||
nativeHits: 1,
|
||||
@@ -446,9 +446,14 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
expect(() => loader("/repo/dist/extensions/demo/api.js")).toThrow("missing-dep");
|
||||
expect(createJiti).not.toHaveBeenCalled();
|
||||
expect(fromSourceTransformer).not.toHaveBeenCalled();
|
||||
expect(nativeStub).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", {
|
||||
allowWindows: true,
|
||||
});
|
||||
expect(nativeStub).toHaveBeenCalledWith(
|
||||
"/repo/dist/extensions/demo/api.js",
|
||||
expect.objectContaining({
|
||||
allowWindows: true,
|
||||
fallbackOnMissingDependency: true,
|
||||
fallbackOnNativeError: true,
|
||||
}),
|
||||
);
|
||||
expect(getPluginModuleLoaderStats()).toMatchObject({
|
||||
calls: 1,
|
||||
nativeHits: 0,
|
||||
@@ -461,7 +466,6 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
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("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
@@ -481,6 +485,10 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
|
||||
expect(result.fromSourceTransform).toBe(true);
|
||||
expect(createJiti).toHaveBeenCalledWith(
|
||||
"file:///repo/src/plugins/public-surface-loader.ts",
|
||||
expect.objectContaining({ tryNative: true }),
|
||||
);
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
expect(getPluginModuleLoaderStats()).toMatchObject({
|
||||
calls: 1,
|
||||
@@ -496,7 +504,6 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
@@ -529,7 +536,6 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
it("skips the native-require fast path when tryNative is explicitly false", async () => {
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
@@ -570,7 +576,6 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const fromSourceTransformer = vi.fn(() => ({ fromSourceTransform: true }));
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
const nativeStub = vi.fn(() => ({ ok: true, moduleExport: { fromNative: true } }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
@@ -605,7 +610,6 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
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("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type { createJiti } from "jiti";
|
||||
import { toSafeImportPath } from "../shared/import-specifier.js";
|
||||
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
|
||||
@@ -129,6 +131,13 @@ export function createPluginModuleLoaderCache(
|
||||
return new PluginLruCache<PluginModuleLoader>(maxEntries);
|
||||
}
|
||||
|
||||
function toSourceTransformImportPath(specifier: string): string {
|
||||
if (process.platform === "win32" && path.isAbsolute(specifier)) {
|
||||
return pathToFileURL(specifier).href;
|
||||
}
|
||||
return toSafeImportPath(specifier);
|
||||
}
|
||||
|
||||
function resolveDefaultPluginModuleLoaderConfig(
|
||||
params: ResolvePluginModuleLoaderCacheEntryParams,
|
||||
): ReturnType<typeof resolvePluginLoaderModuleConfig> {
|
||||
@@ -185,7 +194,7 @@ export function resolvePluginModuleLoaderCacheEntry(
|
||||
function createLazySourceTransformLoader(params: {
|
||||
loaderFilename: string;
|
||||
aliasMap: Record<string, string>;
|
||||
tryNative: boolean;
|
||||
sourceTransformTryNative: boolean;
|
||||
createLoader?: PluginModuleLoaderFactory;
|
||||
}): () => PluginModuleLoader {
|
||||
let loadWithSourceTransform: PluginModuleLoader | undefined;
|
||||
@@ -197,7 +206,7 @@ function createLazySourceTransformLoader(params: {
|
||||
params.loaderFilename,
|
||||
{
|
||||
...buildPluginLoaderJitiOptions(params.aliasMap),
|
||||
tryNative: params.tryNative,
|
||||
tryNative: params.sourceTransformTryNative,
|
||||
},
|
||||
);
|
||||
loadWithSourceTransform = new Proxy(jitiLoader, {
|
||||
@@ -205,7 +214,7 @@ function createLazySourceTransformLoader(params: {
|
||||
const [first, ...rest] = argArray as [unknown, ...unknown[]];
|
||||
if (typeof first === "string") {
|
||||
return Reflect.apply(target, thisArg, [
|
||||
toSafeImportPath(first),
|
||||
toSourceTransformImportPath(first),
|
||||
...rest,
|
||||
] as never) as never;
|
||||
}
|
||||
@@ -245,7 +254,10 @@ function createPluginModuleLoader(params: {
|
||||
tryNative: boolean;
|
||||
createLoader?: PluginModuleLoaderFactory;
|
||||
}): PluginModuleLoader {
|
||||
const getLoadWithSourceTransform = createLazySourceTransformLoader(params);
|
||||
const getLoadWithSourceTransform = createLazySourceTransformLoader({
|
||||
...params,
|
||||
sourceTransformTryNative: params.tryNative,
|
||||
});
|
||||
// When the caller has explicitly opted out of native loading (for example
|
||||
// `bundled-capability-runtime` in Vitest+dist mode, which depends on
|
||||
// jiti's alias rewriting to surface a narrow SDK slice), route every
|
||||
@@ -270,7 +282,7 @@ function createPluginModuleLoader(params: {
|
||||
// handle.
|
||||
const getLoadWithAliasTransform = createLazySourceTransformLoader({
|
||||
...params,
|
||||
tryNative: false,
|
||||
sourceTransformTryNative: false,
|
||||
});
|
||||
return ((target: string, ...rest: unknown[]) => {
|
||||
pluginModuleLoaderStats.calls += 1;
|
||||
@@ -284,6 +296,9 @@ function createPluginModuleLoader(params: {
|
||||
}
|
||||
const native = tryNativeRequireJavaScriptModule(target, {
|
||||
allowWindows: true,
|
||||
aliasMap: params.aliasMap,
|
||||
fallbackOnMissingDependency: true,
|
||||
fallbackOnNativeError: true,
|
||||
});
|
||||
if (native.ok) {
|
||||
pluginModuleLoaderStats.nativeHits += 1;
|
||||
|
||||
@@ -128,7 +128,10 @@ export function loadPluginBoundaryModule<TModule>(
|
||||
options: { origin?: PluginOrigin } = {},
|
||||
): TModule {
|
||||
if (isJavaScriptModulePath(modulePath)) {
|
||||
const native = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true });
|
||||
const native = tryNativeRequireJavaScriptModule(modulePath, {
|
||||
allowWindows: true,
|
||||
fallbackOnNativeError: options.origin !== "bundled",
|
||||
});
|
||||
if (native.ok) {
|
||||
return native.moduleExport as TModule;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [
|
||||
"pendingFinalDeliveryLastError",
|
||||
"pendingFinalDeliveryText",
|
||||
"pendingFinalDeliveryContext",
|
||||
"pendingFinalDeliveryIntentId",
|
||||
"totalTokensFresh",
|
||||
"estimatedCostUsd",
|
||||
"cacheRead",
|
||||
|
||||
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { shouldExpectNativeJitiForJavaScriptTestRuntime } from "../test-utils/jiti-runtime.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
import {
|
||||
getRegistryJitiMocks,
|
||||
@@ -194,7 +193,6 @@ describe("setup-registry module loader", () => {
|
||||
});
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const restoreVersions = forceNodeRuntimeVersionsForTest();
|
||||
const expectedTryNative = shouldExpectNativeJitiForJavaScriptTestRuntime();
|
||||
|
||||
try {
|
||||
resolvePluginSetupRegistry({
|
||||
@@ -212,7 +210,7 @@ describe("setup-registry module loader", () => {
|
||||
);
|
||||
expect(mocks.createJiti.mock.calls[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
tryNative: expectedTryNative,
|
||||
tryNative: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { setBundledPluginsDirOverrideForTest } from "./bundled-dir.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
|
||||
describe("source checkout bundled plugin runtime", () => {
|
||||
it("loads enabled bundled plugins from built dist or source checkout", () => {
|
||||
beforeEach(() => {
|
||||
setBundledPluginsDirOverrideForTest(path.join(process.cwd(), "extensions"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setBundledPluginsDirOverrideForTest(undefined);
|
||||
});
|
||||
|
||||
it("loads enabled bundled plugins from source checkout", () => {
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
onlyPluginIds: ["twitch"],
|
||||
onlyPluginIds: ["tokenjuice"],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
twitch: { enabled: true },
|
||||
tokenjuice: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const twitch = registry.plugins.find((plugin) => plugin.id === "twitch");
|
||||
expect(twitch).toMatchObject({
|
||||
const tokenjuice = registry.plugins.find((plugin) => plugin.id === "tokenjuice");
|
||||
expect(tokenjuice).toMatchObject({
|
||||
status: "loaded",
|
||||
origin: "bundled",
|
||||
});
|
||||
|
||||
const runtimeCandidates = [
|
||||
`${path.sep}dist${path.sep}extensions${path.sep}twitch${path.sep}index.js`,
|
||||
`${path.sep}extensions${path.sep}twitch${path.sep}index.ts`,
|
||||
];
|
||||
const rootCandidates = [
|
||||
`${path.sep}dist${path.sep}extensions${path.sep}twitch`,
|
||||
`${path.sep}extensions${path.sep}twitch`,
|
||||
];
|
||||
const includesAny = (actual: string | undefined, candidates: readonly string[]) =>
|
||||
actual !== undefined && candidates.some((candidate) => actual.includes(candidate));
|
||||
const expectedRuntime = `${path.sep}extensions${path.sep}tokenjuice${path.sep}index.ts`;
|
||||
const expectedRoot = `${path.sep}extensions${path.sep}tokenjuice`;
|
||||
|
||||
expect(includesAny(twitch?.source, runtimeCandidates)).toBe(true);
|
||||
expect(includesAny(twitch?.rootDir, rootCandidates)).toBe(true);
|
||||
expect(tokenjuice?.source).toContain(expectedRuntime);
|
||||
expect(tokenjuice?.rootDir).toContain(expectedRoot);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user