diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index a314093f9bd..a2711a70fac 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -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", diff --git a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts index c0d86a45ea8..16d05db2df5 100644 --- a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts @@ -47,7 +47,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { '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 = { '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 = { '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";', diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 13b063810b9..09052bb6d41 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -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", diff --git a/src/plugins/native-module-require.test.ts b/src/plugins/native-module-require.test.ts index 67e97a1c7e3..dc79d491ac1 100644 --- a/src/plugins/native-module-require.test.ts +++ b/src/plugins/native-module-require.test.ts @@ -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", () => { diff --git a/src/plugins/native-module-require.ts b/src/plugins/native-module-require.ts index e70a1079722..0ec8847753b 100644 --- a/src/plugins/native-module-require.ts +++ b/src/plugins/native-module-require.ts @@ -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; + 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 | undefined, +): unknown { + return withNativeRequireAliases(aliasMap, () => nodeRequire(modulePath)); +} + +export function withNativeRequireAliases( + aliasMap: Record | 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; } } diff --git a/src/plugins/plugin-module-loader-cache.test.ts b/src/plugins/plugin-module-loader-cache.test.ts index 2fd80e1a34a..785ab7c777b 100644 --- a/src/plugins/plugin-module-loader-cache.test.ts +++ b/src/plugins/plugin-module-loader-cache.test.ts @@ -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 }), diff --git a/src/plugins/plugin-module-loader-cache.ts b/src/plugins/plugin-module-loader-cache.ts index f73850b2ba0..76b6a991a72 100644 --- a/src/plugins/plugin-module-loader-cache.ts +++ b/src/plugins/plugin-module-loader-cache.ts @@ -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(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 { @@ -185,7 +194,7 @@ export function resolvePluginModuleLoaderCacheEntry( function createLazySourceTransformLoader(params: { loaderFilename: string; aliasMap: Record; - 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; diff --git a/src/plugins/runtime/runtime-plugin-boundary.ts b/src/plugins/runtime/runtime-plugin-boundary.ts index aa8a5f533cd..8a860c5042e 100644 --- a/src/plugins/runtime/runtime-plugin-boundary.ts +++ b/src/plugins/runtime/runtime-plugin-boundary.ts @@ -128,7 +128,10 @@ export function loadPluginBoundaryModule( 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; } diff --git a/src/plugins/session-entry-slot-keys.ts b/src/plugins/session-entry-slot-keys.ts index 2873878c3c9..57c37e703fc 100644 --- a/src/plugins/session-entry-slot-keys.ts +++ b/src/plugins/session-entry-slot-keys.ts @@ -73,6 +73,7 @@ const SESSION_ENTRY_RESERVED_SLOT_KEY_LIST = [ "pendingFinalDeliveryLastError", "pendingFinalDeliveryText", "pendingFinalDeliveryContext", + "pendingFinalDeliveryIntentId", "totalTokensFresh", "estimatedCostUsd", "cacheRead", diff --git a/src/plugins/setup-registry.test.ts b/src/plugins/setup-registry.test.ts index 009bb3cb419..c052f0b1f76 100644 --- a/src/plugins/setup-registry.test.ts +++ b/src/plugins/setup-registry.test.ts @@ -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, }), ); }); diff --git a/src/plugins/source-checkout-runtime.test.ts b/src/plugins/source-checkout-runtime.test.ts index 041dcb7f169..90823ad828d 100644 --- a/src/plugins/source-checkout-runtime.test.ts +++ b/src/plugins/source-checkout-runtime.test.ts @@ -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); }); });