mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 09:40:43 +00:00
1388 lines
48 KiB
TypeScript
1388 lines
48 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import {
|
|
bundledDistPluginFile,
|
|
bundledPluginFile,
|
|
bundledPluginRoot,
|
|
} from "openclaw/plugin-sdk/test-fixtures";
|
|
import { afterAll, describe, expect, it, vi } from "vitest";
|
|
import { withEnv } from "../test-utils/env.js";
|
|
import {
|
|
buildPluginLoaderAliasMap,
|
|
createPluginLoaderModuleCacheKey,
|
|
buildPluginLoaderJitiOptions,
|
|
isBundledPluginExtensionPath,
|
|
listPluginSdkAliasCandidates,
|
|
listPluginSdkExportedSubpaths,
|
|
normalizeJitiAliasTargetPath,
|
|
resolvePluginLoaderModuleConfig,
|
|
resolvePluginLoaderTryNative,
|
|
resolveExtensionApiAlias,
|
|
resolvePluginRuntimeModulePath,
|
|
resolvePluginSdkAliasFile,
|
|
shouldPreferNativeModuleLoad,
|
|
} from "./sdk-alias.js";
|
|
import {
|
|
cleanupTrackedTempDirs,
|
|
makeTrackedTempDir,
|
|
mkdirSafeDir,
|
|
} from "./test-helpers/fs-fixtures.js";
|
|
|
|
type CreateJiti = typeof import("jiti").createJiti;
|
|
|
|
let createJitiPromise: Promise<CreateJiti> | undefined;
|
|
|
|
async function getCreateJiti() {
|
|
createJitiPromise ??= import("jiti").then(({ createJiti }) => createJiti);
|
|
return createJitiPromise;
|
|
}
|
|
|
|
const fixtureTempDirs: string[] = [];
|
|
const fixtureRoot = makeTrackedTempDir("openclaw-sdk-alias-root", fixtureTempDirs);
|
|
let tempDirIndex = 0;
|
|
|
|
function makeTempDir() {
|
|
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
|
|
mkdirSafeDir(dir);
|
|
return dir;
|
|
}
|
|
|
|
function withCwd<T>(cwd: string, run: () => T): T {
|
|
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(cwd);
|
|
try {
|
|
return run();
|
|
} finally {
|
|
cwdSpy.mockRestore();
|
|
}
|
|
}
|
|
|
|
function createPluginSdkAliasFixture(params?: {
|
|
srcFile?: string;
|
|
distFile?: string;
|
|
srcBody?: string;
|
|
distBody?: string;
|
|
packageExports?: Record<string, unknown>;
|
|
trustedRootIndicators?: boolean;
|
|
trustedRootIndicatorMode?: "bin+marker" | "cli-entry-only" | "none";
|
|
}) {
|
|
const root = makeTempDir();
|
|
const srcFile = path.join(root, "src", "plugin-sdk", params?.srcFile ?? "index.ts");
|
|
const distFile = path.join(root, "dist", "plugin-sdk", params?.distFile ?? "index.js");
|
|
mkdirSafeDir(path.dirname(srcFile));
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
const trustedRootIndicatorMode =
|
|
params?.trustedRootIndicatorMode ??
|
|
(params?.trustedRootIndicators === false ? "none" : "bin+marker");
|
|
const packageJson: Record<string, unknown> = {
|
|
name: "openclaw",
|
|
type: "module",
|
|
};
|
|
if (trustedRootIndicatorMode === "bin+marker") {
|
|
packageJson.bin = {
|
|
openclaw: "openclaw.mjs",
|
|
};
|
|
}
|
|
if (params?.packageExports || trustedRootIndicatorMode === "cli-entry-only") {
|
|
const trustedExports: Record<string, unknown> =
|
|
trustedRootIndicatorMode === "cli-entry-only"
|
|
? { "./cli-entry": { default: "./dist/cli-entry.js" } }
|
|
: {};
|
|
packageJson.exports = {
|
|
"./plugin-sdk": { default: "./dist/plugin-sdk/index.js" },
|
|
...trustedExports,
|
|
...params?.packageExports,
|
|
};
|
|
}
|
|
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify(packageJson, null, 2), "utf-8");
|
|
if (trustedRootIndicatorMode === "bin+marker") {
|
|
fs.writeFileSync(path.join(root, "openclaw.mjs"), "export {};\n", "utf-8");
|
|
}
|
|
mkdirSafeDir(path.join(root, "scripts", "lib"));
|
|
fs.writeFileSync(
|
|
path.join(root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
|
|
JSON.stringify(["qa-channel", "qa-channel-protocol", "qa-lab", "qa-runtime"], null, 2),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8");
|
|
fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8");
|
|
return { root, srcFile, distFile };
|
|
}
|
|
|
|
function createExtensionApiAliasFixture(params?: {
|
|
srcBody?: string;
|
|
distBody?: string;
|
|
srcExtension?: ".ts" | ".mts" | ".js" | ".mjs" | ".cts" | ".cjs";
|
|
}) {
|
|
const root = makeTempDir();
|
|
const srcFile = path.join(root, "src", `extensionAPI${params?.srcExtension ?? ".ts"}`);
|
|
const distFile = path.join(root, "dist", "extensionAPI.js");
|
|
mkdirSafeDir(path.dirname(srcFile));
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
fs.writeFileSync(
|
|
path.join(root, "package.json"),
|
|
JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(path.join(root, "openclaw.mjs"), "export {};\n", "utf-8");
|
|
fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8");
|
|
fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8");
|
|
return { root, srcFile, distFile };
|
|
}
|
|
|
|
function createPluginRuntimeAliasFixture(params?: { srcBody?: string; distBody?: string }) {
|
|
const root = makeTempDir();
|
|
const srcFile = path.join(root, "src", "plugins", "runtime", "index.ts");
|
|
const distFile = path.join(root, "dist", "plugins", "runtime", "index.js");
|
|
mkdirSafeDir(path.dirname(srcFile));
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
fs.writeFileSync(
|
|
path.join(root, "package.json"),
|
|
JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
srcFile,
|
|
params?.srcBody ?? "export const createPluginRuntime = () => ({});\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
distFile,
|
|
params?.distBody ?? "export const createPluginRuntime = () => ({});\n",
|
|
"utf-8",
|
|
);
|
|
return { root, srcFile, distFile };
|
|
}
|
|
|
|
function createPluginSdkAliasTargetFixture(params?: {
|
|
sourceChannelRuntimeExtension?: ".ts" | ".mts" | ".js" | ".mjs" | ".cts" | ".cjs";
|
|
}) {
|
|
const sourceChannelRuntimeExtension = params?.sourceChannelRuntimeExtension ?? ".ts";
|
|
const fixture = createPluginSdkAliasFixture({
|
|
srcFile: `channel-runtime${sourceChannelRuntimeExtension}`,
|
|
distFile: "channel-runtime.js",
|
|
packageExports: {
|
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
|
"./plugin-sdk/plugin-entry": { default: "./dist/plugin-sdk/plugin-entry.js" },
|
|
},
|
|
});
|
|
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
|
|
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
|
const sourcePluginEntryPath = path.join(fixture.root, "src", "plugin-sdk", "plugin-entry.ts");
|
|
const distPluginEntryPath = path.join(fixture.root, "dist", "plugin-sdk", "plugin-entry.js");
|
|
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.writeFileSync(
|
|
sourcePluginEntryPath,
|
|
"export const definePluginEntry = (entry) => entry;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
distPluginEntryPath,
|
|
"export const definePluginEntry = (entry) => entry;\n",
|
|
"utf-8",
|
|
);
|
|
return {
|
|
fixture,
|
|
sourceRootAlias,
|
|
distRootAlias,
|
|
sourceChannelRuntimePath: path.join(
|
|
fixture.root,
|
|
"src",
|
|
"plugin-sdk",
|
|
`channel-runtime${sourceChannelRuntimeExtension}`,
|
|
),
|
|
distChannelRuntimePath: path.join(fixture.root, "dist", "plugin-sdk", "channel-runtime.js"),
|
|
sourcePluginEntryPath,
|
|
distPluginEntryPath,
|
|
};
|
|
}
|
|
|
|
function writePluginEntry(root: string, relativePath: string) {
|
|
const pluginEntry = path.join(root, relativePath);
|
|
fs.mkdirSync(path.dirname(pluginEntry), { recursive: true });
|
|
fs.writeFileSync(pluginEntry, 'export const plugin = "demo";\n', "utf-8");
|
|
return pluginEntry;
|
|
}
|
|
|
|
function createUserInstalledPluginSdkAliasFixture() {
|
|
const { fixture, sourcePluginEntryPath, sourceRootAlias, sourceChannelRuntimePath } =
|
|
createPluginSdkAliasTargetFixture();
|
|
const externalPluginRoot = path.join(makeTempDir(), ".openclaw", "extensions", "demo");
|
|
const externalPluginEntry = path.join(externalPluginRoot, "index.ts");
|
|
mkdirSafeDir(externalPluginRoot);
|
|
fs.writeFileSync(
|
|
externalPluginEntry,
|
|
[
|
|
'import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";',
|
|
'export default definePluginEntry({ id: "demo", register() {} });',
|
|
"",
|
|
].join("\n"),
|
|
"utf-8",
|
|
);
|
|
return {
|
|
externalPluginEntry,
|
|
externalPluginRoot,
|
|
fixture,
|
|
sourcePluginEntryPath,
|
|
sourceRootAlias,
|
|
sourceChannelRuntimePath,
|
|
};
|
|
}
|
|
|
|
function resolvePluginSdkAlias(params: {
|
|
srcFile: string;
|
|
distFile: string;
|
|
modulePath: string;
|
|
argv1?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
}) {
|
|
const run = () =>
|
|
resolvePluginSdkAliasFile({
|
|
srcFile: params.srcFile,
|
|
distFile: params.distFile,
|
|
modulePath: params.modulePath,
|
|
argv1: params.argv1,
|
|
});
|
|
return params.env ? withEnv(params.env, run) : run();
|
|
}
|
|
|
|
function resolvePluginRuntimeModule(params: {
|
|
modulePath: string;
|
|
argv1?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
}) {
|
|
const run = () =>
|
|
resolvePluginRuntimeModulePath({
|
|
modulePath: params.modulePath,
|
|
argv1: params.argv1,
|
|
});
|
|
return params.env ? withEnv(params.env, run) : run();
|
|
}
|
|
|
|
function expectResolvedFixturePath(params: {
|
|
resolved: string | null;
|
|
fixture: { srcFile: string; distFile: string };
|
|
expected: "src" | "dist";
|
|
}) {
|
|
expect(params.resolved).toBe(
|
|
params.expected === "dist" ? params.fixture.distFile : params.fixture.srcFile,
|
|
);
|
|
}
|
|
|
|
function expectPluginSdkAliasTargets(
|
|
aliases: Record<string, string | undefined>,
|
|
params: {
|
|
rootAliasPath: string;
|
|
channelRuntimePath?: string;
|
|
pluginEntryPath?: string;
|
|
},
|
|
) {
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(params.rootAliasPath),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(params.rootAliasPath),
|
|
);
|
|
if (params.channelRuntimePath) {
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe(
|
|
fs.realpathSync(params.channelRuntimePath),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe(
|
|
fs.realpathSync(params.channelRuntimePath),
|
|
);
|
|
}
|
|
if (params.pluginEntryPath) {
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/plugin-entry"] ?? "")).toBe(
|
|
fs.realpathSync(params.pluginEntryPath),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/plugin-sdk/plugin-entry"] ?? "")).toBe(
|
|
fs.realpathSync(params.pluginEntryPath),
|
|
);
|
|
}
|
|
}
|
|
|
|
function expectPluginSdkAliasResolution(params: {
|
|
fixture: { root: string; srcFile: string; distFile: string };
|
|
srcFile: string;
|
|
distFile: string;
|
|
modulePath: (root: string) => string;
|
|
argv1?: (root: string) => string;
|
|
env?: NodeJS.ProcessEnv;
|
|
expected: "src" | "dist";
|
|
}) {
|
|
const resolved = resolvePluginSdkAlias({
|
|
srcFile: params.srcFile,
|
|
distFile: params.distFile,
|
|
modulePath: params.modulePath(params.fixture.root),
|
|
argv1: params.argv1?.(params.fixture.root),
|
|
env: params.env,
|
|
});
|
|
expectResolvedFixturePath({
|
|
resolved,
|
|
fixture: params.fixture,
|
|
expected: params.expected,
|
|
});
|
|
}
|
|
|
|
function expectExtensionApiAliasResolution(params: {
|
|
fixture: { root: string; srcFile: string; distFile: string };
|
|
modulePath: (root: string) => string;
|
|
argv1?: (root: string) => string;
|
|
env?: NodeJS.ProcessEnv;
|
|
expected: "src" | "dist";
|
|
}) {
|
|
const resolved = withEnv(params.env ?? {}, () =>
|
|
resolveExtensionApiAlias({
|
|
modulePath: params.modulePath(params.fixture.root),
|
|
argv1: params.argv1?.(params.fixture.root),
|
|
}),
|
|
);
|
|
expectResolvedFixturePath({
|
|
resolved,
|
|
fixture: params.fixture,
|
|
expected: params.expected,
|
|
});
|
|
}
|
|
|
|
function expectExportedSubpaths(params: {
|
|
fixture: { root: string };
|
|
modulePath: string;
|
|
expected: readonly string[];
|
|
cwd?: string;
|
|
}) {
|
|
const run = () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: params.modulePath,
|
|
});
|
|
const subpaths = params.cwd ? withCwd(params.cwd, run) : run();
|
|
expect(subpaths).toEqual(params.expected);
|
|
}
|
|
|
|
function expectCwdFallbackPluginSdkAliasResolution(params: {
|
|
fixture: { root: string; srcFile: string; distFile: string };
|
|
expected: "src" | "dist" | null;
|
|
}) {
|
|
const resolved = withCwd(params.fixture.root, () =>
|
|
resolvePluginSdkAlias({
|
|
srcFile: "channel-runtime.ts",
|
|
distFile: "channel-runtime.js",
|
|
modulePath: "/tmp/tsx-cache/openclaw-loader.js",
|
|
env: { NODE_ENV: undefined },
|
|
}),
|
|
);
|
|
if (params.expected === null) {
|
|
expect(resolved).toBeNull();
|
|
return;
|
|
}
|
|
expectResolvedFixturePath({
|
|
resolved,
|
|
fixture: params.fixture,
|
|
expected: params.expected,
|
|
});
|
|
}
|
|
|
|
afterAll(() => {
|
|
cleanupTrackedTempDirs(fixtureTempDirs);
|
|
});
|
|
|
|
describe("plugin sdk alias helpers", () => {
|
|
it.each([
|
|
{
|
|
name: "prefers dist plugin-sdk alias when loader runs from dist",
|
|
buildFixture: () => createPluginSdkAliasFixture(),
|
|
modulePath: (root: string) => path.join(root, "dist", "plugins", "loader.js"),
|
|
srcFile: "index.ts",
|
|
distFile: "index.js",
|
|
expected: "dist" as const,
|
|
},
|
|
{
|
|
name: "prefers src plugin-sdk alias when loader runs from src in non-production",
|
|
buildFixture: () => createPluginSdkAliasFixture(),
|
|
modulePath: (root: string) => path.join(root, "src", "plugins", "loader.ts"),
|
|
srcFile: "index.ts",
|
|
distFile: "index.js",
|
|
env: { NODE_ENV: undefined },
|
|
expected: "src" as const,
|
|
},
|
|
{
|
|
name: "falls back to src plugin-sdk alias when dist is missing in production",
|
|
buildFixture: () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
fs.rmSync(fixture.distFile);
|
|
return fixture;
|
|
},
|
|
modulePath: (root: string) => path.join(root, "src", "plugins", "loader.ts"),
|
|
srcFile: "index.ts",
|
|
distFile: "index.js",
|
|
env: { NODE_ENV: "production", VITEST: undefined },
|
|
expected: "src" as const,
|
|
},
|
|
{
|
|
name: "prefers dist root-alias shim when loader runs from dist",
|
|
buildFixture: () =>
|
|
createPluginSdkAliasFixture({
|
|
srcFile: "root-alias.cjs",
|
|
distFile: "root-alias.cjs",
|
|
srcBody: "module.exports = {};\n",
|
|
distBody: "module.exports = {};\n",
|
|
}),
|
|
modulePath: (root: string) => path.join(root, "dist", "plugins", "loader.js"),
|
|
srcFile: "root-alias.cjs",
|
|
distFile: "root-alias.cjs",
|
|
expected: "dist" as const,
|
|
},
|
|
{
|
|
name: "prefers src root-alias shim when loader runs from src in non-production",
|
|
buildFixture: () =>
|
|
createPluginSdkAliasFixture({
|
|
srcFile: "root-alias.cjs",
|
|
distFile: "root-alias.cjs",
|
|
srcBody: "module.exports = {};\n",
|
|
distBody: "module.exports = {};\n",
|
|
}),
|
|
modulePath: (root: string) => path.join(root, "src", "plugins", "loader.ts"),
|
|
srcFile: "root-alias.cjs",
|
|
distFile: "root-alias.cjs",
|
|
env: { NODE_ENV: undefined },
|
|
expected: "src" as const,
|
|
},
|
|
{
|
|
name: "resolves plugin-sdk alias from package root when loader runs from transpiler cache path",
|
|
buildFixture: () =>
|
|
createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/index": { default: "./dist/plugin-sdk/index.js" },
|
|
},
|
|
}),
|
|
modulePath: () => "/tmp/tsx-cache/openclaw-loader.js",
|
|
argv1: (root: string) => path.join(root, "openclaw.mjs"),
|
|
srcFile: "index.ts",
|
|
distFile: "index.js",
|
|
env: { NODE_ENV: undefined },
|
|
expected: "src" as const,
|
|
},
|
|
])("$name", ({ buildFixture, modulePath, argv1, srcFile, distFile, env, expected }) => {
|
|
const fixture = buildFixture();
|
|
expectPluginSdkAliasResolution({
|
|
fixture,
|
|
srcFile,
|
|
distFile,
|
|
modulePath,
|
|
argv1,
|
|
env,
|
|
expected,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "prefers dist extension-api alias when loader runs from dist",
|
|
modulePath: (root: string) => path.join(root, "dist", "plugins", "loader.js"),
|
|
expected: "dist" as const,
|
|
},
|
|
{
|
|
name: "prefers src extension-api alias when loader runs from src in non-production",
|
|
modulePath: (root: string) => path.join(root, "src", "plugins", "loader.ts"),
|
|
env: { NODE_ENV: undefined },
|
|
expected: "src" as const,
|
|
},
|
|
{
|
|
name: "resolves extension-api alias from package root when loader runs from transpiler cache path",
|
|
modulePath: () => "/tmp/tsx-cache/openclaw-loader.js",
|
|
argv1: (root: string) => path.join(root, "openclaw.mjs"),
|
|
env: { NODE_ENV: undefined },
|
|
expected: "src" as const,
|
|
},
|
|
])("$name", ({ modulePath, argv1, env, expected }) => {
|
|
const fixture = createExtensionApiAliasFixture();
|
|
expectExtensionApiAliasResolution({
|
|
fixture,
|
|
modulePath,
|
|
argv1,
|
|
env,
|
|
expected,
|
|
});
|
|
});
|
|
|
|
it("resolves source extension-api aliases through the wider source extension family", () => {
|
|
const fixture = createExtensionApiAliasFixture({ srcExtension: ".mts" });
|
|
expectExtensionApiAliasResolution({
|
|
fixture,
|
|
modulePath: (root: string) => path.join(root, "src", "plugins", "loader.ts"),
|
|
env: { NODE_ENV: undefined },
|
|
expected: "src",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "prefers dist candidates first for production src runtime",
|
|
env: { NODE_ENV: "production", VITEST: undefined },
|
|
expectedFirst: "dist" as const,
|
|
},
|
|
{
|
|
name: "prefers src candidates first for non-production src runtime",
|
|
env: { NODE_ENV: undefined },
|
|
expectedFirst: "src" as const,
|
|
},
|
|
])("$name", ({ env, expectedFirst }) => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
const candidates = withEnv(env ?? {}, () =>
|
|
listPluginSdkAliasCandidates({
|
|
srcFile: "index.ts",
|
|
distFile: "index.js",
|
|
modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"),
|
|
}),
|
|
);
|
|
const first = expectedFirst === "dist" ? fixture.distFile : fixture.srcFile;
|
|
const second = expectedFirst === "dist" ? fixture.srcFile : fixture.distFile;
|
|
expect(candidates.indexOf(first)).toBeLessThan(candidates.indexOf(second));
|
|
});
|
|
|
|
it("derives plugin-sdk subpaths from package exports", () => {
|
|
const fixture = createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/compat": { default: "./dist/plugin-sdk/compat.js" },
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
"./plugin-sdk/nested/value": { default: "./dist/plugin-sdk/nested/value.js" },
|
|
"./plugin-sdk/..\\..\\evil": { default: "./dist/plugin-sdk/evil.js" },
|
|
"./plugin-sdk/C:temp": { default: "./dist/plugin-sdk/drive.js" },
|
|
"./plugin-sdk/.hidden": { default: "./dist/plugin-sdk/hidden.js" },
|
|
},
|
|
});
|
|
const subpaths = listPluginSdkExportedSubpaths({
|
|
modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"),
|
|
});
|
|
expect(subpaths).toEqual(["compat", "core"]);
|
|
});
|
|
|
|
it("adds private qa plugin-sdk subpaths for trusted local checkouts when enabled", () => {
|
|
const fixture = createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
},
|
|
});
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "qa-channel.ts"),
|
|
"export const qaChannel = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "qa-channel-protocol.ts"),
|
|
"export const qaChannelProtocol = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts"),
|
|
"export const qaRuntime = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "dist", "plugin-sdk", "qa-lab.js"),
|
|
"export const qaLab = true;\n",
|
|
"utf-8",
|
|
);
|
|
|
|
const subpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"),
|
|
}),
|
|
);
|
|
expect(subpaths).toEqual(["core", "qa-channel", "qa-channel-protocol", "qa-lab", "qa-runtime"]);
|
|
});
|
|
|
|
it("does not reuse a non-private cached subpath list after private qa gets enabled", () => {
|
|
const fixture = createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
},
|
|
});
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "qa-channel.ts"),
|
|
"export const qaChannel = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "qa-channel-protocol.ts"),
|
|
"export const qaChannelProtocol = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts"),
|
|
"export const qaRuntime = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "dist", "plugin-sdk", "qa-lab.js"),
|
|
"export const qaLab = true;\n",
|
|
"utf-8",
|
|
);
|
|
|
|
expect(
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"),
|
|
}),
|
|
).toEqual(["core"]);
|
|
|
|
const privateSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: path.join(fixture.root, "src", "plugins", "loader.ts"),
|
|
}),
|
|
);
|
|
expect(privateSubpaths).toEqual([
|
|
"core",
|
|
"qa-channel",
|
|
"qa-channel-protocol",
|
|
"qa-lab",
|
|
"qa-runtime",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "does not derive plugin-sdk subpaths from cwd fallback when package root is not an OpenClaw root",
|
|
fixture: () =>
|
|
createPluginSdkAliasFixture({
|
|
trustedRootIndicators: false,
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
|
},
|
|
}),
|
|
expected: [],
|
|
},
|
|
{
|
|
name: "derives plugin-sdk subpaths via cwd fallback when trusted root indicator is cli-entry export",
|
|
fixture: () =>
|
|
createPluginSdkAliasFixture({
|
|
trustedRootIndicatorMode: "cli-entry-only",
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
|
},
|
|
}),
|
|
expected: ["channel-runtime", "core"],
|
|
},
|
|
] as const)("$name", ({ fixture: buildFixture, expected }) => {
|
|
const fixture = buildFixture();
|
|
expectExportedSubpaths({
|
|
fixture,
|
|
cwd: fixture.root,
|
|
modulePath: "/tmp/tsx-cache/openclaw-loader.js",
|
|
expected,
|
|
});
|
|
});
|
|
|
|
it("builds plugin-sdk aliases from the module being loaded, not the loader location", () => {
|
|
const {
|
|
fixture,
|
|
sourceRootAlias,
|
|
distRootAlias,
|
|
sourceChannelRuntimePath,
|
|
distChannelRuntimePath,
|
|
} = createPluginSdkAliasTargetFixture();
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
|
|
const sourceAliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry),
|
|
);
|
|
expectPluginSdkAliasTargets(sourceAliases, {
|
|
rootAliasPath: sourceRootAlias,
|
|
channelRuntimePath: sourceChannelRuntimePath,
|
|
});
|
|
|
|
const distPluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledDistPluginFile("demo", "index.js"),
|
|
);
|
|
|
|
const distAliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(distPluginEntry),
|
|
);
|
|
expectPluginSdkAliasTargets(distAliases, {
|
|
rootAliasPath: distRootAlias,
|
|
channelRuntimePath: distChannelRuntimePath,
|
|
});
|
|
});
|
|
|
|
it("adds private qa plugin-sdk aliases for source plugins when enabled", () => {
|
|
const fixture = createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
},
|
|
});
|
|
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
|
|
const sourceQaChannelPath = path.join(fixture.root, "src", "plugin-sdk", "qa-channel.ts");
|
|
const sourceQaChannelProtocolPath = path.join(
|
|
fixture.root,
|
|
"src",
|
|
"plugin-sdk",
|
|
"qa-channel-protocol.ts",
|
|
);
|
|
const sourceQaRuntimePath = path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts");
|
|
const distQaLabPath = path.join(fixture.root, "dist", "plugin-sdk", "qa-lab.js");
|
|
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.writeFileSync(sourceQaChannelPath, "export const qaChannel = true;\n", "utf-8");
|
|
fs.writeFileSync(
|
|
sourceQaChannelProtocolPath,
|
|
"export const qaChannelProtocol = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(sourceQaRuntimePath, "export const qaRuntime = true;\n", "utf-8");
|
|
fs.writeFileSync(distQaLabPath, "export const qaLab = true;\n", "utf-8");
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("qa-matrix", "src/index.ts"),
|
|
);
|
|
|
|
const aliases = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1", NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(sourceRootAlias),
|
|
);
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/qa-runtime"] ?? "")).toBe(
|
|
fs.realpathSync(sourceQaRuntimePath),
|
|
);
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/qa-channel"] ?? "")).toBe(
|
|
fs.realpathSync(sourceQaChannelPath),
|
|
);
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/qa-channel-protocol"] ?? "")).toBe(
|
|
fs.realpathSync(sourceQaChannelProtocolPath),
|
|
);
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/qa-lab"] ?? "")).toBe(
|
|
fs.realpathSync(distQaLabPath),
|
|
);
|
|
});
|
|
|
|
it("applies explicit dist resolution to plugin-sdk subpath aliases too", () => {
|
|
const { fixture, distRootAlias, distChannelRuntimePath } = createPluginSdkAliasTargetFixture();
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
|
|
const distAliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
|
|
);
|
|
|
|
expectPluginSdkAliasTargets(distAliases, {
|
|
rootAliasPath: distRootAlias,
|
|
channelRuntimePath: distChannelRuntimePath,
|
|
});
|
|
});
|
|
|
|
it("falls back to source plugin-sdk subpath aliases when dist chunks are stale", () => {
|
|
const fixture = createPluginSdkAliasFixture({
|
|
srcFile: "provider-entry.ts",
|
|
distFile: "provider-entry.js",
|
|
distBody: 'import { entry } from "../missing-provider-entry-chunk.js";\nexport { entry };\n',
|
|
packageExports: {
|
|
"./plugin-sdk/provider-entry": { default: "./dist/plugin-sdk/provider-entry.js" },
|
|
},
|
|
});
|
|
const sourceProviderEntryPath = path.join(
|
|
fixture.root,
|
|
"src",
|
|
"plugin-sdk",
|
|
"provider-entry.ts",
|
|
);
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
|
|
const distAliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
|
|
);
|
|
|
|
expect(fs.realpathSync(distAliases["openclaw/plugin-sdk/provider-entry"] ?? "")).toBe(
|
|
fs.realpathSync(sourceProviderEntryPath),
|
|
);
|
|
});
|
|
|
|
it("builds source plugin-sdk subpath aliases through the wider source extension family", () => {
|
|
const { fixture, sourceRootAlias, sourceChannelRuntimePath } =
|
|
createPluginSdkAliasTargetFixture({
|
|
sourceChannelRuntimeExtension: ".mts",
|
|
});
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
|
|
const sourceAliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry),
|
|
);
|
|
|
|
expectPluginSdkAliasTargets(sourceAliases, {
|
|
rootAliasPath: sourceRootAlias,
|
|
channelRuntimePath: sourceChannelRuntimePath,
|
|
});
|
|
});
|
|
|
|
it("resolves plugin-sdk aliases for user-installed plugins via the running openclaw argv hint", () => {
|
|
const {
|
|
externalPluginEntry,
|
|
externalPluginRoot,
|
|
fixture,
|
|
sourcePluginEntryPath,
|
|
sourceRootAlias,
|
|
sourceChannelRuntimePath,
|
|
} = createUserInstalledPluginSdkAliasFixture();
|
|
|
|
const aliases = withCwd(externalPluginRoot, () =>
|
|
withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(externalPluginEntry, path.join(fixture.root, "openclaw.mjs")),
|
|
),
|
|
);
|
|
|
|
expectPluginSdkAliasTargets(aliases, {
|
|
rootAliasPath: sourceRootAlias,
|
|
channelRuntimePath: sourceChannelRuntimePath,
|
|
pluginEntryPath: sourcePluginEntryPath,
|
|
});
|
|
});
|
|
|
|
it("resolves plugin-sdk aliases for user-installed plugins via moduleUrl hint", () => {
|
|
const {
|
|
externalPluginEntry,
|
|
externalPluginRoot,
|
|
fixture,
|
|
sourcePluginEntryPath,
|
|
sourceRootAlias,
|
|
sourceChannelRuntimePath,
|
|
} = createUserInstalledPluginSdkAliasFixture();
|
|
|
|
// Simulate loader.ts passing its own import.meta.url as the moduleUrl hint.
|
|
// This covers installations where argv1 does not resolve to the openclaw root
|
|
// (e.g. single-binary distributions or custom process launchers).
|
|
// Use openclaw.mjs which is created by createPluginSdkAliasFixture (bin+marker mode).
|
|
// Use fixture.root as cwd so process.cwd() fallback also resolves to fixture, not the
|
|
// real openclaw repo root in the test runner environment.
|
|
const loaderModuleUrl = pathToFileURL(path.join(fixture.root, "openclaw.mjs")).href;
|
|
|
|
// Use externalPluginRoot as cwd so process.cwd() fallback cannot accidentally
|
|
// resolve to the fixture root — only the moduleUrl hint can bridge the gap.
|
|
// Pass "" for argv1: undefined would trigger the STARTUP_ARGV1 default (the vitest
|
|
// runner binary, inside the openclaw repo), which resolves before moduleUrl is checked.
|
|
// An empty string is falsy so resolveTrustedOpenClawRootFromArgvHint returns null,
|
|
// meaning only the moduleUrl hint can bridge the gap.
|
|
const aliases = withCwd(externalPluginRoot, () =>
|
|
withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(
|
|
externalPluginEntry,
|
|
"", // explicitly disable argv1 (empty string bypasses STARTUP_ARGV1 default)
|
|
loaderModuleUrl,
|
|
),
|
|
),
|
|
);
|
|
|
|
expectPluginSdkAliasTargets(aliases, {
|
|
rootAliasPath: sourceRootAlias,
|
|
channelRuntimePath: sourceChannelRuntimePath,
|
|
pluginEntryPath: sourcePluginEntryPath,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "does not resolve plugin-sdk alias files from cwd fallback when package root is not an OpenClaw root",
|
|
fixture: () =>
|
|
createPluginSdkAliasFixture({
|
|
srcFile: "channel-runtime.ts",
|
|
distFile: "channel-runtime.js",
|
|
trustedRootIndicators: false,
|
|
packageExports: {
|
|
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
|
|
},
|
|
}),
|
|
expected: null,
|
|
},
|
|
] as const)("$name", ({ fixture: buildFixture, expected }) => {
|
|
const fixture = buildFixture();
|
|
expectCwdFallbackPluginSdkAliasResolution({
|
|
fixture,
|
|
expected,
|
|
});
|
|
});
|
|
|
|
it("configures the plugin loader native-first boundary to prefer native dist modules", () => {
|
|
const options = buildPluginLoaderJitiOptions({});
|
|
|
|
expect(options.tryNative).toBe(true);
|
|
expect(options.interopDefault).toBe(true);
|
|
expect(options.extensions).toContain(".js");
|
|
expect(options.extensions).toContain(".ts");
|
|
expect("alias" in options).toBe(false);
|
|
});
|
|
|
|
it("uses transpiled module loads for source TypeScript plugin entries", () => {
|
|
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
|
expect(
|
|
shouldPreferNativeModuleLoad(
|
|
`/repo/${bundledPluginFile("discord", "src/channel.runtime.ts")}`,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("disables native module loads under Bun even for built JavaScript entries", () => {
|
|
const originalVersions = process.versions;
|
|
Object.defineProperty(process, "versions", {
|
|
configurable: true,
|
|
value: {
|
|
...originalVersions,
|
|
bun: "1.2.0",
|
|
},
|
|
});
|
|
|
|
try {
|
|
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(false);
|
|
expect(
|
|
shouldPreferNativeModuleLoad(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
|
).toBe(false);
|
|
} finally {
|
|
Object.defineProperty(process, "versions", {
|
|
configurable: true,
|
|
value: originalVersions,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("enables native module loads on Windows for built JavaScript entries", () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, "platform", {
|
|
configurable: true,
|
|
value: "win32",
|
|
});
|
|
|
|
try {
|
|
expect(shouldPreferNativeModuleLoad("/repo/dist/plugins/runtime/index.js")).toBe(true);
|
|
expect(
|
|
shouldPreferNativeModuleLoad(`/repo/${bundledDistPluginFile("browser", "index.js")}`),
|
|
).toBe(true);
|
|
} finally {
|
|
Object.defineProperty(process, "platform", {
|
|
configurable: true,
|
|
value: originalPlatform,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("keeps plugin loader dist shortcuts on native module loading on Windows for JS entries", () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, "platform", {
|
|
configurable: true,
|
|
value: "win32",
|
|
});
|
|
|
|
try {
|
|
expect(
|
|
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
|
preferBuiltDist: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
|
|
preferBuiltDist: true,
|
|
}),
|
|
).toBe(false);
|
|
} finally {
|
|
Object.defineProperty(process, "platform", {
|
|
configurable: true,
|
|
value: originalPlatform,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("prefers native module loading for bundled plugin dist .js modules, keeps .ts on aliased path", () => {
|
|
// Built .js/.mjs/.cjs files under dist/extensions/ should now delegate
|
|
// to shouldPreferNativeModuleLoad() — which returns true on Node for
|
|
// compiled artifacts, avoiding the slow jiti transform path.
|
|
expect(
|
|
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, {
|
|
preferBuiltDist: true,
|
|
}),
|
|
).toBe(true);
|
|
// TypeScript source files still need jiti's transform pipeline.
|
|
expect(
|
|
resolvePluginLoaderTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, {
|
|
preferBuiltDist: true,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
resolvePluginLoaderTryNative("/repo/dist/plugins/runtime/index.js", {
|
|
preferBuiltDist: true,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("keeps plugin loader module cache keys stable across alias insertion order", () => {
|
|
expect(
|
|
createPluginLoaderModuleCacheKey({
|
|
tryNative: true,
|
|
aliasMap: {
|
|
zeta: "/repo/zeta.js",
|
|
alpha: "/repo/alpha.js",
|
|
},
|
|
}),
|
|
).toBe(
|
|
createPluginLoaderModuleCacheKey({
|
|
tryNative: true,
|
|
aliasMap: {
|
|
alpha: "/repo/alpha.js",
|
|
zeta: "/repo/zeta.js",
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("returns plugin loader module config with stable cache keys", () => {
|
|
const first = resolvePluginLoaderModuleConfig({
|
|
modulePath: `/repo/${bundledDistPluginFile("browser", "index.js")}`,
|
|
argv1: "/repo/openclaw.mjs",
|
|
moduleUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
|
preferBuiltDist: true,
|
|
});
|
|
const second = resolvePluginLoaderModuleConfig({
|
|
modulePath: `/repo/${bundledDistPluginFile("browser", "index.js")}`,
|
|
argv1: "/repo/openclaw.mjs",
|
|
moduleUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
|
preferBuiltDist: true,
|
|
});
|
|
|
|
expect(second).toBe(first);
|
|
});
|
|
|
|
it("scopes plugin loader module config by plugin-sdk resolution", () => {
|
|
const { fixture, sourceRootAlias, distRootAlias } = createPluginSdkAliasTargetFixture();
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
|
|
const { auto, dist, distAgain } = withEnv({ NODE_ENV: undefined }, () => ({
|
|
auto: resolvePluginLoaderModuleConfig({
|
|
modulePath: sourcePluginEntry,
|
|
argv1: path.join(fixture.root, "openclaw.mjs"),
|
|
moduleUrl: pathToFileURL(path.join(fixture.root, "src/plugins/loader.ts")).href,
|
|
pluginSdkResolution: "auto",
|
|
}),
|
|
dist: resolvePluginLoaderModuleConfig({
|
|
modulePath: sourcePluginEntry,
|
|
argv1: path.join(fixture.root, "openclaw.mjs"),
|
|
moduleUrl: pathToFileURL(path.join(fixture.root, "src/plugins/loader.ts")).href,
|
|
pluginSdkResolution: "dist",
|
|
}),
|
|
distAgain: resolvePluginLoaderModuleConfig({
|
|
modulePath: sourcePluginEntry,
|
|
argv1: path.join(fixture.root, "openclaw.mjs"),
|
|
moduleUrl: pathToFileURL(path.join(fixture.root, "src/plugins/loader.ts")).href,
|
|
pluginSdkResolution: "dist",
|
|
}),
|
|
}));
|
|
|
|
expect(distAgain).toBe(dist);
|
|
expect(auto).not.toBe(dist);
|
|
expect(fs.realpathSync(auto.aliasMap["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(sourceRootAlias),
|
|
);
|
|
expect(fs.realpathSync(dist.aliasMap["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(distRootAlias),
|
|
);
|
|
});
|
|
|
|
it("detects bundled plugin extension paths across source and dist roots", () => {
|
|
expect(
|
|
isBundledPluginExtensionPath({
|
|
modulePath: "/repo/extensions/demo/api.js",
|
|
openClawPackageRoot: "/repo",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isBundledPluginExtensionPath({
|
|
modulePath: "/repo/dist/extensions/demo/api.js",
|
|
openClawPackageRoot: "/repo",
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isBundledPluginExtensionPath({
|
|
modulePath: "/repo/vendor/demo/api.js",
|
|
openClawPackageRoot: "/repo",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("normalizes Windows alias targets before handing them to the source transformer", () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, "platform", {
|
|
configurable: true,
|
|
value: "win32",
|
|
});
|
|
|
|
try {
|
|
expect(normalizeJitiAliasTargetPath(String.raw`C:\repo\dist\plugin-sdk\root-alias.cjs`)).toBe(
|
|
"C:/repo/dist/plugin-sdk/root-alias.cjs",
|
|
);
|
|
} finally {
|
|
Object.defineProperty(process, "platform", {
|
|
configurable: true,
|
|
value: originalPlatform,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("loads source runtime shims through the non-native module loading boundary", async () => {
|
|
const copiedExtensionRoot = path.join(makeTempDir(), bundledPluginRoot("discord"));
|
|
const copiedSourceDir = path.join(copiedExtensionRoot, "src");
|
|
const copiedPluginSdkDir = path.join(copiedExtensionRoot, "plugin-sdk");
|
|
mkdirSafeDir(copiedSourceDir);
|
|
mkdirSafeDir(copiedPluginSdkDir);
|
|
const sourceLoaderBaseFile = path.join(copiedSourceDir, "__jiti-base__.mjs");
|
|
fs.writeFileSync(sourceLoaderBaseFile, "export {};\n", "utf-8");
|
|
fs.writeFileSync(
|
|
path.join(copiedSourceDir, "channel.runtime.ts"),
|
|
`import { resolveOutboundSendDep } from "@openclaw/plugin-sdk/outbound-send-deps";
|
|
|
|
export const syntheticRuntimeMarker = {
|
|
resolveOutboundSendDep,
|
|
};
|
|
`,
|
|
"utf-8",
|
|
);
|
|
const copiedChannelRuntimeShim = path.join(copiedPluginSdkDir, "outbound-send-deps.ts");
|
|
fs.writeFileSync(
|
|
copiedChannelRuntimeShim,
|
|
`export function resolveOutboundSendDep() {
|
|
return "shimmed";
|
|
}
|
|
`,
|
|
"utf-8",
|
|
);
|
|
const copiedChannelRuntime = path.join(copiedExtensionRoot, "src", "channel.runtime.ts");
|
|
const sourceLoaderBaseUrl = pathToFileURL(sourceLoaderBaseFile).href;
|
|
|
|
const createJiti = await getCreateJiti();
|
|
const withoutAlias = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions({}),
|
|
tryNative: false,
|
|
});
|
|
expect(() => withoutAlias(copiedChannelRuntime)).toThrow();
|
|
|
|
const withAlias = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions({
|
|
"openclaw/plugin-sdk/outbound-send-deps": copiedChannelRuntimeShim,
|
|
"@openclaw/plugin-sdk/outbound-send-deps": copiedChannelRuntimeShim,
|
|
}),
|
|
tryNative: false,
|
|
});
|
|
expect(withAlias(copiedChannelRuntime)).toMatchObject({
|
|
syntheticRuntimeMarker: {
|
|
resolveOutboundSendDep: expect.any(Function),
|
|
},
|
|
});
|
|
}, 240_000);
|
|
|
|
it.each([
|
|
{
|
|
name: "prefers dist plugin runtime module when loader runs from dist",
|
|
modulePath: (root: string) => path.join(root, "dist", "plugins", "loader.js"),
|
|
expected: "dist" as const,
|
|
},
|
|
{
|
|
name: "resolves plugin runtime module from package root when loader runs from transpiler cache path",
|
|
modulePath: () => "/tmp/tsx-cache/openclaw-loader.js",
|
|
argv1: (root: string) => path.join(root, "openclaw.mjs"),
|
|
env: { NODE_ENV: undefined },
|
|
expected: "src" as const,
|
|
},
|
|
])("$name", ({ modulePath, argv1, env, expected }) => {
|
|
const fixture = createPluginRuntimeAliasFixture();
|
|
const resolved = resolvePluginRuntimeModule({
|
|
modulePath: modulePath(fixture.root),
|
|
argv1: argv1?.(fixture.root),
|
|
env,
|
|
});
|
|
expect(resolved).toBe(expected === "dist" ? fixture.distFile : fixture.srcFile);
|
|
});
|
|
});
|
|
|
|
describe("buildPluginLoaderAliasMap memoization", () => {
|
|
it("returns the same object reference for identical effective context", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
|
|
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("memo-demo", "src/index.ts"),
|
|
);
|
|
|
|
const first = buildPluginLoaderAliasMap(sourcePluginEntry);
|
|
const second = buildPluginLoaderAliasMap(sourcePluginEntry);
|
|
|
|
expect(second).toBe(first);
|
|
});
|
|
|
|
it("returns different references for different modulePath inputs", () => {
|
|
const fixtureA = createPluginSdkAliasFixture();
|
|
const fixtureB = createPluginSdkAliasFixture();
|
|
fs.writeFileSync(
|
|
path.join(fixtureA.root, "src", "plugin-sdk", "root-alias.cjs"),
|
|
"module.exports = {};\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixtureB.root, "src", "plugin-sdk", "root-alias.cjs"),
|
|
"module.exports = {};\n",
|
|
"utf-8",
|
|
);
|
|
const entryA = writePluginEntry(fixtureA.root, bundledPluginFile("a", "src/index.ts"));
|
|
const entryB = writePluginEntry(fixtureB.root, bundledPluginFile("b", "src/index.ts"));
|
|
|
|
const aliasA = buildPluginLoaderAliasMap(entryA);
|
|
const aliasB = buildPluginLoaderAliasMap(entryB);
|
|
|
|
expect(aliasA).not.toBe(aliasB);
|
|
expect(aliasA["openclaw/plugin-sdk"]).not.toBe(aliasB["openclaw/plugin-sdk"]);
|
|
});
|
|
|
|
it("returns different references when pluginSdkResolution differs", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"),
|
|
"module.exports = {};\n",
|
|
"utf-8",
|
|
);
|
|
const entry = writePluginEntry(fixture.root, bundledPluginFile("res", "src/index.ts"));
|
|
|
|
const auto = buildPluginLoaderAliasMap(entry, undefined, undefined, "auto");
|
|
const dist = buildPluginLoaderAliasMap(entry, undefined, undefined, "dist");
|
|
|
|
expect(auto).not.toBe(dist);
|
|
});
|
|
|
|
it("returns different references when argv1 differs", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"),
|
|
"module.exports = {};\n",
|
|
"utf-8",
|
|
);
|
|
const entry = writePluginEntry(fixture.root, bundledPluginFile("argv", "src/index.ts"));
|
|
|
|
const a = buildPluginLoaderAliasMap(entry, "/path/to/cli-a.mjs");
|
|
const b = buildPluginLoaderAliasMap(entry, "/path/to/cli-b.mjs");
|
|
|
|
expect(a).not.toBe(b);
|
|
});
|
|
|
|
it("does not reuse a public alias map after private qa aliases are enabled", () => {
|
|
const fixture = createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
},
|
|
});
|
|
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
|
|
const sourceQaRuntimePath = path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts");
|
|
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.writeFileSync(sourceQaRuntimePath, "export const qaRuntime = true;\n", "utf-8");
|
|
const entry = writePluginEntry(fixture.root, bundledPluginFile("private-qa", "src/index.ts"));
|
|
|
|
const publicAliases = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
buildPluginLoaderAliasMap(entry),
|
|
);
|
|
const privateAliases = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" }, () =>
|
|
buildPluginLoaderAliasMap(entry),
|
|
);
|
|
|
|
expect(publicAliases).not.toBe(privateAliases);
|
|
expect(publicAliases["openclaw/plugin-sdk/qa-runtime"]).toBeUndefined();
|
|
expect(fs.realpathSync(privateAliases["openclaw/plugin-sdk/qa-runtime"] ?? "")).toBe(
|
|
fs.realpathSync(sourceQaRuntimePath),
|
|
);
|
|
});
|
|
|
|
it("does not reuse a development alias map in production mode", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
|
|
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
|
fs.writeFileSync(sourceRootAlias, "module.exports = { source: true };\n", "utf-8");
|
|
fs.writeFileSync(distRootAlias, "module.exports = { dist: true };\n", "utf-8");
|
|
const entry = writePluginEntry(fixture.root, bundledPluginFile("env-mode", "src/index.ts"));
|
|
|
|
const developmentAliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(entry),
|
|
);
|
|
const productionAliases = withEnv({ NODE_ENV: "production" }, () =>
|
|
buildPluginLoaderAliasMap(entry),
|
|
);
|
|
|
|
expect(developmentAliases).not.toBe(productionAliases);
|
|
expect(fs.realpathSync(developmentAliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(sourceRootAlias),
|
|
);
|
|
expect(fs.realpathSync(productionAliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(distRootAlias),
|
|
);
|
|
});
|
|
|
|
it("memoized result has identical content to a freshly computed map", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"),
|
|
"module.exports = {};\n",
|
|
"utf-8",
|
|
);
|
|
const entry = writePluginEntry(fixture.root, bundledPluginFile("eq", "src/index.ts"));
|
|
|
|
const first = buildPluginLoaderAliasMap(entry);
|
|
const second = buildPluginLoaderAliasMap(entry);
|
|
|
|
// Same reference (cache hit)
|
|
expect(second).toBe(first);
|
|
// Same content
|
|
expect(second).toEqual(first);
|
|
// Same key set
|
|
expect(Object.keys(second).toSorted()).toEqual(Object.keys(first).toSorted());
|
|
});
|
|
});
|
|
|
|
describe("buildPluginLoaderJitiOptions", () => {
|
|
it("pre-normalizes and marks alias maps for source transforms", () => {
|
|
const marker = Symbol.for("pathe:normalizedAlias");
|
|
const aliasMap = {
|
|
"openclaw/plugin-sdk/core": "/repo/src/plugin-sdk/core.ts",
|
|
"openclaw/plugin-sdk": "/repo/src/plugin-sdk/root-alias.cjs",
|
|
"@openclaw/plugin-sdk": "/repo/src/plugin-sdk/root-alias.cjs",
|
|
};
|
|
|
|
const first = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
|
|
const second = buildPluginLoaderJitiOptions({ ...aliasMap }).alias as Record<string, string>;
|
|
|
|
expect(second).toBe(first);
|
|
expect((first as Record<symbol, unknown>)[marker]).toBe(true);
|
|
expect(Object.prototype.propertyIsEnumerable.call(first, marker)).toBe(false);
|
|
});
|
|
|
|
it("applies source-transform alias-target normalization before caching", () => {
|
|
const aliasMap = {
|
|
alpha: "/repo/alpha",
|
|
beta: "alpha/sub",
|
|
};
|
|
|
|
const alias = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
|
|
|
|
expect(alias).not.toBe(aliasMap);
|
|
expect(alias.beta).toBe("/repo/alpha/sub");
|
|
});
|
|
|
|
it("does not attach an empty alias map", () => {
|
|
expect(buildPluginLoaderJitiOptions({})).not.toHaveProperty("alias");
|
|
});
|
|
});
|