mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
* refactor: derive acp core package subpath maps * refactor: split acp manager task and timeout helpers * refactor: split acp translator presentation helpers * fix: keep packaged acp core plugin aliases * ci: split gateway control plane runtime shard
2763 lines
100 KiB
TypeScript
2763 lines
100 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,
|
|
resolvePluginLoaderJitiFsCacheDir,
|
|
resolvePluginLoaderJitiFsCacheOption,
|
|
resolvePluginLoaderModuleConfig,
|
|
resolvePluginLoaderTryNative,
|
|
resolveExtensionApiAlias,
|
|
resolvePluginRuntimeModulePath,
|
|
resolvePluginRuntimeModulePathWithDiagnostics,
|
|
resolvePluginSdkAliasFile,
|
|
shouldPreferNativeModuleLoad,
|
|
type PluginSdkResolutionPreference,
|
|
} 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 createTrustedOpenClawPackageFixture(version: string) {
|
|
const root = makeTempDir();
|
|
fs.writeFileSync(path.join(root, "openclaw.mjs"), "export {};\n", "utf-8");
|
|
fs.writeFileSync(
|
|
path.join(root, "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
name: "openclaw",
|
|
version,
|
|
bin: { openclaw: "openclaw.mjs" },
|
|
exports: { "./plugin-sdk": { default: "./dist/plugin-sdk/index.js" } },
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf-8",
|
|
);
|
|
mkdirSafeDir(path.join(root, "dist", "plugins"));
|
|
return root;
|
|
}
|
|
|
|
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 writeWorkspacePackageEntry(params: {
|
|
root: string;
|
|
packageDir: string;
|
|
srcFile: string;
|
|
distFile: string;
|
|
}) {
|
|
const srcFile = path.join(params.root, "packages", params.packageDir, "src", params.srcFile);
|
|
const distFile = path.join(params.root, "packages", params.packageDir, "dist", params.distFile);
|
|
mkdirSafeDir(path.dirname(srcFile));
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
fs.writeFileSync(srcFile, "export {};\n", "utf-8");
|
|
fs.writeFileSync(distFile, "export {};\n", "utf-8");
|
|
return { 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 createBundledPluginPackagePublicSurfaceAliasFixture() {
|
|
const fixture = createPluginSdkAliasTargetFixture();
|
|
const extensionRoot = path.join(fixture.fixture.root, bundledPluginRoot("slack"));
|
|
const distExtensionRoot = path.join(fixture.fixture.root, "dist", "extensions", "slack");
|
|
mkdirSafeDir(extensionRoot);
|
|
mkdirSafeDir(distExtensionRoot);
|
|
fs.writeFileSync(
|
|
path.join(extensionRoot, "package.json"),
|
|
JSON.stringify({ name: "@openclaw/slack", type: "module" }, null, 2),
|
|
"utf-8",
|
|
);
|
|
const sourceApiPath = path.join(extensionRoot, "api.ts");
|
|
const sourceRuntimeApiPath = path.join(extensionRoot, "runtime-api.ts");
|
|
const sourceTestApiPath = path.join(extensionRoot, "test-api.ts");
|
|
const distApiPath = path.join(distExtensionRoot, "api.js");
|
|
const distRuntimeApiPath = path.join(distExtensionRoot, "runtime-api.js");
|
|
const distTestApiPath = path.join(distExtensionRoot, "test-api.js");
|
|
fs.writeFileSync(sourceApiPath, "export const slackApi = 'source';\n", "utf-8");
|
|
fs.writeFileSync(sourceRuntimeApiPath, "export const slackRuntimeApi = 'source';\n", "utf-8");
|
|
fs.writeFileSync(sourceTestApiPath, "export const slackTestApi = 'source';\n", "utf-8");
|
|
fs.writeFileSync(distApiPath, "export const slackApi = 'dist';\n", "utf-8");
|
|
fs.writeFileSync(distRuntimeApiPath, "export const slackRuntimeApi = 'dist';\n", "utf-8");
|
|
fs.writeFileSync(distTestApiPath, "export const slackTestApi = 'dist';\n", "utf-8");
|
|
fs.writeFileSync(
|
|
path.join(extensionRoot, "internal.ts"),
|
|
"export const internal = true;\n",
|
|
"utf-8",
|
|
);
|
|
return {
|
|
...fixture,
|
|
distApiPath,
|
|
distRuntimeApiPath,
|
|
distTestApiPath,
|
|
sourceApiPath,
|
|
sourceRuntimeApiPath,
|
|
sourceTestApiPath,
|
|
};
|
|
}
|
|
|
|
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 writeInstalledPluginEntry(params: {
|
|
installRoot: string;
|
|
packageName: string;
|
|
entry?: string;
|
|
}) {
|
|
const entry = params.entry ?? "dist/index.js";
|
|
const packageRoot = path.join(
|
|
params.installRoot,
|
|
"node_modules",
|
|
...params.packageName.split("/"),
|
|
);
|
|
const pluginEntry = path.join(packageRoot, entry);
|
|
mkdirSafeDir(path.dirname(pluginEntry));
|
|
fs.writeFileSync(
|
|
path.join(packageRoot, "package.json"),
|
|
JSON.stringify({ name: params.packageName, type: "module" }, null, 2),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(pluginEntry, 'export const plugin = "installed";\n', "utf-8");
|
|
return { packageRoot, 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;
|
|
devSourceRoot?: string | null;
|
|
env?: NodeJS.ProcessEnv;
|
|
pluginSdkResolution?: PluginSdkResolutionPreference;
|
|
}) {
|
|
const run = () =>
|
|
resolvePluginRuntimeModulePath({
|
|
modulePath: params.modulePath,
|
|
argv1: params.argv1,
|
|
devSourceRoot: params.devSourceRoot,
|
|
pluginSdkResolution: params.pluginSdkResolution,
|
|
});
|
|
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("resolves extension-api aliases from an explicit dev source root", () => {
|
|
const stableFixture = createExtensionApiAliasFixture({
|
|
distBody: "export const stableExtensionApi = true;\n",
|
|
});
|
|
const devFixture = createExtensionApiAliasFixture({
|
|
distBody: "export const devExtensionApi = true;\n",
|
|
});
|
|
mkdirSafeDir(path.join(devFixture.root, "extensions"));
|
|
const entry = path.join(stableFixture.root, "dist", "plugins", "loader.js");
|
|
mkdirSafeDir(path.dirname(entry));
|
|
fs.writeFileSync(entry, "export {};\n", "utf-8");
|
|
|
|
const aliases = buildPluginLoaderAliasMap(entry, undefined, undefined, "dist", devFixture.root);
|
|
|
|
expect(fs.realpathSync(aliases["openclaw/extension-api"] ?? "")).toBe(
|
|
fs.realpathSync(devFixture.distFile),
|
|
);
|
|
});
|
|
|
|
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("adds non-QA private Codex helper subpaths only for trusted Codex plugins", () => {
|
|
const fixture = createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
},
|
|
});
|
|
fs.rmSync(
|
|
path.join(fixture.root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
|
|
{ force: true },
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "codex-mcp-projection.ts"),
|
|
"export const codexMcpProjection = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "codex-native-task-runtime.ts"),
|
|
"export const codexNativeTaskRuntime = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts"),
|
|
"export const qaRuntime = true;\n",
|
|
"utf-8",
|
|
);
|
|
const sourceCodexEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("codex", "src/index.ts"),
|
|
);
|
|
const sourceOtherEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
const { packageRoot: installedCodexRoot, pluginEntry: installedCodexEntry } =
|
|
writeInstalledPluginEntry({
|
|
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
|
|
packageName: "@openclaw/codex",
|
|
});
|
|
const { packageRoot: installedOtherRoot, pluginEntry: installedOtherEntry } =
|
|
writeInstalledPluginEntry({
|
|
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
|
|
packageName: "@openclaw/demo",
|
|
});
|
|
const shadowCodexRoot = path.join(makeTempDir(), ".openclaw", "extensions", "codex-shadow");
|
|
const shadowCodexEntry = path.join(shadowCodexRoot, "dist", "index.js");
|
|
mkdirSafeDir(path.dirname(shadowCodexEntry));
|
|
fs.writeFileSync(
|
|
path.join(shadowCodexRoot, "package.json"),
|
|
JSON.stringify({ name: "@openclaw/codex", type: "module" }, null, 2),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(shadowCodexEntry, 'export const plugin = "shadow";\n', "utf-8");
|
|
|
|
const codexSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: sourceCodexEntry,
|
|
}),
|
|
);
|
|
const otherSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: sourceOtherEntry,
|
|
}),
|
|
);
|
|
const installedCodexSubpaths = withCwd(installedCodexRoot, () =>
|
|
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: installedCodexEntry,
|
|
argv1: path.join(fixture.root, "openclaw.mjs"),
|
|
}),
|
|
),
|
|
);
|
|
const installedOtherSubpaths = withCwd(installedOtherRoot, () =>
|
|
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: installedOtherEntry,
|
|
argv1: path.join(fixture.root, "openclaw.mjs"),
|
|
}),
|
|
),
|
|
);
|
|
const shadowCodexSubpaths = withCwd(shadowCodexRoot, () =>
|
|
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: shadowCodexEntry,
|
|
argv1: path.join(fixture.root, "openclaw.mjs"),
|
|
}),
|
|
),
|
|
);
|
|
|
|
expect(codexSubpaths).toEqual(["codex-mcp-projection", "codex-native-task-runtime", "core"]);
|
|
expect(installedCodexSubpaths).toEqual([
|
|
"codex-mcp-projection",
|
|
"codex-native-task-runtime",
|
|
"core",
|
|
]);
|
|
expect(otherSubpaths).toEqual(["core"]);
|
|
expect(installedOtherSubpaths).toEqual(["core"]);
|
|
expect(shadowCodexSubpaths).toEqual(["core"]);
|
|
});
|
|
|
|
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("aliases non-QA private plugin-sdk subpaths for trusted Codex runtime loading", () => {
|
|
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 sourceCodexMcpProjectionPath = path.join(
|
|
fixture.root,
|
|
"src",
|
|
"plugin-sdk",
|
|
"codex-mcp-projection.ts",
|
|
);
|
|
const sourceCodexNativeTaskRuntimePath = path.join(
|
|
fixture.root,
|
|
"src",
|
|
"plugin-sdk",
|
|
"codex-native-task-runtime.ts",
|
|
);
|
|
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
|
const distCodexMcpProjectionPath = path.join(
|
|
fixture.root,
|
|
"dist",
|
|
"plugin-sdk",
|
|
"codex-mcp-projection.js",
|
|
);
|
|
const distCodexNativeTaskRuntimePath = path.join(
|
|
fixture.root,
|
|
"dist",
|
|
"plugin-sdk",
|
|
"codex-native-task-runtime.js",
|
|
);
|
|
const sourceQaRuntimePath = path.join(fixture.root, "src", "plugin-sdk", "qa-runtime.ts");
|
|
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.rmSync(
|
|
path.join(fixture.root, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
|
|
{ force: true },
|
|
);
|
|
fs.writeFileSync(
|
|
sourceCodexMcpProjectionPath,
|
|
"export const codexMcpProjection = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
sourceCodexNativeTaskRuntimePath,
|
|
"export const codexNativeTaskRuntime = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
distCodexMcpProjectionPath,
|
|
"export const codexMcpProjection = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
distCodexNativeTaskRuntimePath,
|
|
"export const codexNativeTaskRuntime = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(sourceQaRuntimePath, "export const qaRuntime = true;\n", "utf-8");
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("codex", "src/index.ts"),
|
|
);
|
|
const distCodexEntry = writePluginEntry(
|
|
fixture.root,
|
|
path.join("dist", "extensions", "codex", "index.js"),
|
|
);
|
|
const sourceOtherPluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
const devFixture = createPluginSdkAliasFixture({
|
|
packageExports: {
|
|
"./plugin-sdk/core": { default: "./dist/plugin-sdk/core.js" },
|
|
},
|
|
});
|
|
const devRootAlias = path.join(devFixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
|
const devCodexMcpProjectionPath = path.join(
|
|
devFixture.root,
|
|
"dist",
|
|
"plugin-sdk",
|
|
"codex-mcp-projection.js",
|
|
);
|
|
const devCodexNativeTaskRuntimePath = path.join(
|
|
devFixture.root,
|
|
"dist",
|
|
"plugin-sdk",
|
|
"codex-native-task-runtime.js",
|
|
);
|
|
mkdirSafeDir(path.join(devFixture.root, "extensions"));
|
|
fs.writeFileSync(devRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.writeFileSync(
|
|
devCodexMcpProjectionPath,
|
|
"export const devCodexMcpProjection = true;\n",
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
devCodexNativeTaskRuntimePath,
|
|
"export const devCodexNativeTaskRuntime = true;\n",
|
|
"utf-8",
|
|
);
|
|
const { packageRoot: installedCodexRoot, pluginEntry: installedCodexEntry } =
|
|
writeInstalledPluginEntry({
|
|
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
|
|
packageName: "@openclaw/codex",
|
|
});
|
|
const { packageRoot: installedOtherRoot, pluginEntry: installedOtherEntry } =
|
|
writeInstalledPluginEntry({
|
|
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
|
|
packageName: "@openclaw/demo",
|
|
});
|
|
const shadowCodexRoot = path.join(makeTempDir(), ".openclaw", "extensions", "codex-shadow");
|
|
const shadowCodexEntry = path.join(shadowCodexRoot, "dist", "index.js");
|
|
mkdirSafeDir(path.dirname(shadowCodexEntry));
|
|
fs.writeFileSync(
|
|
path.join(shadowCodexRoot, "package.json"),
|
|
JSON.stringify({ name: "@openclaw/codex", type: "module" }, null, 2),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(shadowCodexEntry, 'export const plugin = "shadow";\n', "utf-8");
|
|
|
|
const aliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(sourcePluginEntry),
|
|
);
|
|
const otherAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(sourceOtherPluginEntry),
|
|
);
|
|
const devRootAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() =>
|
|
buildPluginLoaderAliasMap(
|
|
distCodexEntry,
|
|
path.join(fixture.root, "openclaw.mjs"),
|
|
undefined,
|
|
"dist",
|
|
devFixture.root,
|
|
),
|
|
);
|
|
const installedAliases = withCwd(installedCodexRoot, () =>
|
|
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(
|
|
installedCodexEntry,
|
|
path.join(fixture.root, "openclaw.mjs"),
|
|
undefined,
|
|
"dist",
|
|
),
|
|
),
|
|
);
|
|
const shadowCodexAliases = withCwd(shadowCodexRoot, () =>
|
|
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(
|
|
shadowCodexEntry,
|
|
path.join(fixture.root, "openclaw.mjs"),
|
|
undefined,
|
|
"dist",
|
|
),
|
|
),
|
|
);
|
|
const installedOtherAliases = withCwd(installedOtherRoot, () =>
|
|
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(
|
|
installedOtherEntry,
|
|
path.join(fixture.root, "openclaw.mjs"),
|
|
undefined,
|
|
"dist",
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(sourceRootAlias),
|
|
);
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/codex-mcp-projection"] ?? "")).toBe(
|
|
fs.realpathSync(sourceCodexMcpProjectionPath),
|
|
);
|
|
expect(fs.realpathSync(aliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? "")).toBe(
|
|
fs.realpathSync(sourceCodexNativeTaskRuntimePath),
|
|
);
|
|
expect(
|
|
fs.realpathSync(installedAliases["openclaw/plugin-sdk/codex-mcp-projection"] ?? ""),
|
|
).toBe(fs.realpathSync(distCodexMcpProjectionPath));
|
|
expect(
|
|
fs.realpathSync(installedAliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? ""),
|
|
).toBe(fs.realpathSync(distCodexNativeTaskRuntimePath));
|
|
expect(fs.realpathSync(devRootAliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(devRootAlias),
|
|
);
|
|
expect(fs.realpathSync(devRootAliases["openclaw/plugin-sdk/codex-mcp-projection"] ?? "")).toBe(
|
|
fs.realpathSync(devCodexMcpProjectionPath),
|
|
);
|
|
expect(
|
|
fs.realpathSync(devRootAliases["openclaw/plugin-sdk/codex-native-task-runtime"] ?? ""),
|
|
).toBe(fs.realpathSync(devCodexNativeTaskRuntimePath));
|
|
expect(aliases["openclaw/plugin-sdk/qa-runtime"]).toBeUndefined();
|
|
expect(otherAliases["openclaw/plugin-sdk/codex-mcp-projection"]).toBeUndefined();
|
|
expect(otherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
|
|
expect(installedOtherAliases["openclaw/plugin-sdk/codex-mcp-projection"]).toBeUndefined();
|
|
expect(installedOtherAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
|
|
expect(shadowCodexAliases["openclaw/plugin-sdk/codex-mcp-projection"]).toBeUndefined();
|
|
expect(shadowCodexAliases["openclaw/plugin-sdk/codex-native-task-runtime"]).toBeUndefined();
|
|
});
|
|
|
|
it("aliases the SSRF internal helper only for bundled local IPC owner plugins", async () => {
|
|
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 distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
|
const sourceSsrFInternalPath = path.join(
|
|
fixture.root,
|
|
"src",
|
|
"plugin-sdk",
|
|
"ssrf-runtime-internal.ts",
|
|
);
|
|
const distSsrFInternalPath = path.join(
|
|
fixture.root,
|
|
"dist",
|
|
"plugin-sdk",
|
|
"ssrf-runtime-internal.js",
|
|
);
|
|
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8");
|
|
fs.rmSync(path.join(fixture.root, "scripts"), { force: true, recursive: true });
|
|
fs.writeFileSync(sourceSsrFInternalPath, "export const ssrfInternal = true;\n", "utf-8");
|
|
fs.writeFileSync(distSsrFInternalPath, "export const ssrfInternal = true;\n", "utf-8");
|
|
const sourceOllamaEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("ollama", "index.ts"),
|
|
);
|
|
const sourceBrowserEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("browser", "index.ts"),
|
|
);
|
|
const sourceOtherPluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "index.ts"),
|
|
);
|
|
const entryBody = [
|
|
'import { ssrfInternal } from "openclaw/plugin-sdk/ssrf-runtime-internal";',
|
|
"export const loadedSsrFInternal = ssrfInternal;",
|
|
"",
|
|
].join("\n");
|
|
fs.writeFileSync(sourceOllamaEntry, entryBody, "utf-8");
|
|
fs.writeFileSync(sourceBrowserEntry, entryBody, "utf-8");
|
|
fs.writeFileSync(sourceOtherPluginEntry, entryBody, "utf-8");
|
|
const distOllamaEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledDistPluginFile("ollama", "index.js"),
|
|
);
|
|
const distBrowserEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledDistPluginFile("browser", "index.js"),
|
|
);
|
|
const distRuntimeOllamaEntry = writePluginEntry(
|
|
fixture.root,
|
|
path.join("dist-runtime", "extensions", "ollama", "index.js"),
|
|
);
|
|
const distRuntimeBrowserEntry = writePluginEntry(
|
|
fixture.root,
|
|
path.join("dist-runtime", "extensions", "browser", "index.js"),
|
|
);
|
|
fs.writeFileSync(distOllamaEntry, entryBody, "utf-8");
|
|
fs.writeFileSync(distBrowserEntry, entryBody, "utf-8");
|
|
fs.writeFileSync(distRuntimeOllamaEntry, entryBody, "utf-8");
|
|
fs.writeFileSync(distRuntimeBrowserEntry, entryBody, "utf-8");
|
|
const { packageRoot: installedOllamaRoot, pluginEntry: installedOllamaEntry } =
|
|
writeInstalledPluginEntry({
|
|
installRoot: path.join(makeTempDir(), ".openclaw", "npm"),
|
|
packageName: "@openclaw/ollama",
|
|
});
|
|
|
|
const sourceSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: sourceOllamaEntry,
|
|
}),
|
|
);
|
|
const sourceBrowserSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: sourceBrowserEntry,
|
|
}),
|
|
);
|
|
const privateQaOtherSubpaths = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" }, () =>
|
|
listPluginSdkExportedSubpaths({
|
|
modulePath: sourceOtherPluginEntry,
|
|
}),
|
|
);
|
|
const sourceAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(sourceOllamaEntry),
|
|
);
|
|
const sourceBrowserAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(sourceBrowserEntry),
|
|
);
|
|
const distAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(distOllamaEntry, undefined, undefined, "dist"),
|
|
);
|
|
const distBrowserAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(distBrowserEntry, undefined, undefined, "dist"),
|
|
);
|
|
const distRuntimeAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(distRuntimeOllamaEntry, undefined, undefined, "dist"),
|
|
);
|
|
const distRuntimeBrowserAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(distRuntimeBrowserEntry, undefined, undefined, "dist"),
|
|
);
|
|
const otherAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(sourceOtherPluginEntry),
|
|
);
|
|
const privateQaOtherAliases = withEnv(
|
|
{ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1", NODE_ENV: undefined },
|
|
() => buildPluginLoaderAliasMap(sourceOtherPluginEntry),
|
|
);
|
|
const installedAliases = withCwd(installedOllamaRoot, () =>
|
|
withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: undefined, NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(
|
|
installedOllamaEntry,
|
|
path.join(fixture.root, "openclaw.mjs"),
|
|
undefined,
|
|
"dist",
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(sourceSubpaths).toEqual(["core", "ssrf-runtime-internal"]);
|
|
expect(sourceBrowserSubpaths).toEqual(["core", "ssrf-runtime-internal"]);
|
|
expect(privateQaOtherSubpaths).toEqual(["core"]);
|
|
expect(fs.realpathSync(sourceAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? "")).toBe(
|
|
fs.realpathSync(sourceSsrFInternalPath),
|
|
);
|
|
expect(
|
|
fs.realpathSync(sourceBrowserAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? ""),
|
|
).toBe(fs.realpathSync(sourceSsrFInternalPath));
|
|
expect(fs.realpathSync(distAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? "")).toBe(
|
|
fs.realpathSync(distSsrFInternalPath),
|
|
);
|
|
expect(
|
|
fs.realpathSync(distBrowserAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? ""),
|
|
).toBe(fs.realpathSync(distSsrFInternalPath));
|
|
expect(
|
|
fs.realpathSync(distRuntimeAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? ""),
|
|
).toBe(fs.realpathSync(distSsrFInternalPath));
|
|
expect(
|
|
fs.realpathSync(distRuntimeBrowserAliases["openclaw/plugin-sdk/ssrf-runtime-internal"] ?? ""),
|
|
).toBe(fs.realpathSync(distSsrFInternalPath));
|
|
expect(otherAliases["openclaw/plugin-sdk/ssrf-runtime-internal"]).toBeUndefined();
|
|
expect(privateQaOtherAliases["openclaw/plugin-sdk/ssrf-runtime-internal"]).toBeUndefined();
|
|
expect(installedAliases["openclaw/plugin-sdk/ssrf-runtime-internal"]).toBeUndefined();
|
|
|
|
const createJiti = await getCreateJiti();
|
|
const sourceLoaderBaseUrl = pathToFileURL(
|
|
path.join(fixture.root, "src", "plugins", "loader.ts"),
|
|
).href;
|
|
const ollamaLoader = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions(sourceAliases),
|
|
tryNative: false,
|
|
});
|
|
const loadedOllama = ollamaLoader(sourceOllamaEntry) as { loadedSsrFInternal?: unknown };
|
|
expect(loadedOllama.loadedSsrFInternal).toBe(true);
|
|
const browserLoader = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions(sourceBrowserAliases),
|
|
tryNative: false,
|
|
});
|
|
const loadedBrowser = browserLoader(sourceBrowserEntry) as { loadedSsrFInternal?: unknown };
|
|
expect(loadedBrowser.loadedSsrFInternal).toBe(true);
|
|
|
|
const distLoader = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions(distAliases),
|
|
tryNative: true,
|
|
});
|
|
const loadedDistOllama = distLoader(distOllamaEntry) as {
|
|
loadedSsrFInternal?: unknown;
|
|
};
|
|
expect(loadedDistOllama.loadedSsrFInternal).toBe(true);
|
|
const distBrowserLoader = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions(distBrowserAliases),
|
|
tryNative: true,
|
|
});
|
|
const loadedDistBrowser = distBrowserLoader(distBrowserEntry) as {
|
|
loadedSsrFInternal?: unknown;
|
|
};
|
|
expect(loadedDistBrowser.loadedSsrFInternal).toBe(true);
|
|
|
|
const distRuntimeLoader = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions(distRuntimeAliases),
|
|
tryNative: true,
|
|
});
|
|
const loadedDistRuntimeOllama = distRuntimeLoader(distRuntimeOllamaEntry) as {
|
|
loadedSsrFInternal?: unknown;
|
|
};
|
|
expect(loadedDistRuntimeOllama.loadedSsrFInternal).toBe(true);
|
|
const distRuntimeBrowserLoader = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions(distRuntimeBrowserAliases),
|
|
tryNative: true,
|
|
});
|
|
const loadedDistRuntimeBrowser = distRuntimeBrowserLoader(distRuntimeBrowserEntry) as {
|
|
loadedSsrFInternal?: unknown;
|
|
};
|
|
expect(loadedDistRuntimeBrowser.loadedSsrFInternal).toBe(true);
|
|
|
|
const otherLoader = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions(privateQaOtherAliases),
|
|
tryNative: false,
|
|
});
|
|
let otherLoadError: unknown;
|
|
try {
|
|
otherLoader(sourceOtherPluginEntry);
|
|
} catch (error) {
|
|
otherLoadError = error;
|
|
}
|
|
expect(otherLoadError).toBeInstanceOf(Error);
|
|
expect((otherLoadError as Error).message).toContain("ssrf-runtime-internal");
|
|
});
|
|
|
|
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("aliases workspace packages to source when dist artifacts are missing", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
const gatewayClient = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "gateway-client",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const gatewayClientTimeouts = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "gateway-client",
|
|
srcFile: "timeouts.ts",
|
|
distFile: "timeouts.mjs",
|
|
});
|
|
const gatewayProtocol = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "gateway-protocol",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const gatewayProtocolSchema = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "gateway-protocol",
|
|
srcFile: "schema.ts",
|
|
distFile: "schema.mjs",
|
|
});
|
|
const netPolicy = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "net-policy",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const mediaGenerationCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "media-generation-core",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const mediaCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "media-core",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const mediaCoreMime = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "media-core",
|
|
srcFile: "mime.ts",
|
|
distFile: "mime.mjs",
|
|
});
|
|
const acpCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "acp-core",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const acpCoreRuntimeTypes = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "acp-core",
|
|
srcFile: path.join("runtime", "types.ts"),
|
|
distFile: path.join("runtime", "types.mjs"),
|
|
});
|
|
const normalizationCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "normalization-core",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const normalizationStringCoerce = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "normalization-core",
|
|
srcFile: "string-coerce.ts",
|
|
distFile: "string-coerce.mjs",
|
|
});
|
|
const markdownCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "markdown-core",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const markdownCoreTables = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "markdown-core",
|
|
srcFile: "tables.ts",
|
|
distFile: "tables.mjs",
|
|
});
|
|
const mediaGenerationModelRef = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "media-generation-core",
|
|
srcFile: "model-ref.ts",
|
|
distFile: "model-ref.mjs",
|
|
});
|
|
const terminalCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "terminal-core",
|
|
srcFile: "index.ts",
|
|
distFile: "index.mjs",
|
|
});
|
|
const terminalCoreTheme = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "terminal-core",
|
|
srcFile: "theme.ts",
|
|
distFile: "theme.mjs",
|
|
});
|
|
const netPolicyIp = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "net-policy",
|
|
srcFile: "ip.ts",
|
|
distFile: "ip.mjs",
|
|
});
|
|
const modelCatalogProviderId = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "model-catalog-core",
|
|
srcFile: "provider-id.ts",
|
|
distFile: "provider-id.mjs",
|
|
});
|
|
fs.rmSync(gatewayClient.distFile);
|
|
fs.rmSync(gatewayClientTimeouts.distFile);
|
|
fs.rmSync(gatewayProtocol.distFile);
|
|
fs.rmSync(gatewayProtocolSchema.distFile);
|
|
fs.rmSync(markdownCore.distFile);
|
|
fs.rmSync(markdownCoreTables.distFile);
|
|
fs.rmSync(mediaGenerationCore.distFile);
|
|
fs.rmSync(mediaGenerationModelRef.distFile);
|
|
fs.rmSync(mediaCore.distFile);
|
|
fs.rmSync(mediaCoreMime.distFile);
|
|
fs.rmSync(acpCore.distFile);
|
|
fs.rmSync(acpCoreRuntimeTypes.distFile);
|
|
fs.rmSync(normalizationCore.distFile);
|
|
fs.rmSync(normalizationStringCoerce.distFile);
|
|
fs.rmSync(terminalCore.distFile);
|
|
fs.rmSync(terminalCoreTheme.distFile);
|
|
fs.rmSync(netPolicy.distFile);
|
|
fs.rmSync(netPolicyIp.distFile);
|
|
fs.rmSync(modelCatalogProviderId.distFile);
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
|
|
const aliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["@openclaw/gateway-client"] ?? "")).toBe(
|
|
fs.realpathSync(gatewayClient.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/gateway-client/timeouts"] ?? "")).toBe(
|
|
fs.realpathSync(gatewayClientTimeouts.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/gateway-protocol"] ?? "")).toBe(
|
|
fs.realpathSync(gatewayProtocol.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/gateway-protocol/schema"] ?? "")).toBe(
|
|
fs.realpathSync(gatewayProtocolSchema.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/markdown-core"] ?? "")).toBe(
|
|
fs.realpathSync(markdownCore.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/markdown-core/tables"] ?? "")).toBe(
|
|
fs.realpathSync(markdownCoreTables.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/media-generation-core"] ?? "")).toBe(
|
|
fs.realpathSync(mediaGenerationCore.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/media-generation-core/model-ref"] ?? "")).toBe(
|
|
fs.realpathSync(mediaGenerationModelRef.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/media-core"] ?? "")).toBe(
|
|
fs.realpathSync(mediaCore.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/media-core/mime"] ?? "")).toBe(
|
|
fs.realpathSync(mediaCoreMime.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/acp-core"] ?? "")).toBe(
|
|
fs.realpathSync(acpCore.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/acp-core/runtime/types"] ?? "")).toBe(
|
|
fs.realpathSync(acpCoreRuntimeTypes.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/normalization-core"] ?? "")).toBe(
|
|
fs.realpathSync(normalizationCore.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/normalization-core/string-coerce"] ?? "")).toBe(
|
|
fs.realpathSync(normalizationStringCoerce.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/terminal-core"] ?? "")).toBe(
|
|
fs.realpathSync(terminalCore.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/terminal-core/theme"] ?? "")).toBe(
|
|
fs.realpathSync(terminalCoreTheme.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/net-policy"] ?? "")).toBe(
|
|
fs.realpathSync(netPolicy.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/net-policy/ip"] ?? "")).toBe(
|
|
fs.realpathSync(netPolicyIp.srcFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/model-catalog-core/provider-id"] ?? "")).toBe(
|
|
fs.realpathSync(modelCatalogProviderId.srcFile),
|
|
);
|
|
});
|
|
|
|
it("aliases workspace package subpaths to dist when available", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
const gatewayClient = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "gateway-client",
|
|
srcFile: "readiness.ts",
|
|
distFile: "readiness.mjs",
|
|
});
|
|
const gatewayProtocol = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "gateway-protocol",
|
|
srcFile: "connect-error-details.ts",
|
|
distFile: "connect-error-details.mjs",
|
|
});
|
|
const mediaGenerationCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "media-generation-core",
|
|
srcFile: "catalog.ts",
|
|
distFile: "catalog.mjs",
|
|
});
|
|
writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "media-core",
|
|
srcFile: "read-response-with-limit.ts",
|
|
distFile: "read-response-with-limit.mjs",
|
|
});
|
|
const mediaCoreRootDistFile = path.join(
|
|
fixture.root,
|
|
"dist",
|
|
"media-core",
|
|
"read-response-with-limit.js",
|
|
);
|
|
mkdirSafeDir(path.dirname(mediaCoreRootDistFile));
|
|
fs.writeFileSync(mediaCoreRootDistFile, "export {};\n", "utf-8");
|
|
writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "acp-core",
|
|
srcFile: "normalize-text.ts",
|
|
distFile: "normalize-text.mjs",
|
|
});
|
|
const acpCoreRootDistFile = path.join(fixture.root, "dist", "acp-core", "normalize-text.js");
|
|
mkdirSafeDir(path.dirname(acpCoreRootDistFile));
|
|
fs.writeFileSync(acpCoreRootDistFile, "export {};\n", "utf-8");
|
|
writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "normalization-core",
|
|
srcFile: "record-coerce.ts",
|
|
distFile: "record-coerce.mjs",
|
|
});
|
|
const normalizationCoreRootDistFile = path.join(
|
|
fixture.root,
|
|
"dist",
|
|
"normalization-core",
|
|
"record-coerce.js",
|
|
);
|
|
mkdirSafeDir(path.dirname(normalizationCoreRootDistFile));
|
|
fs.writeFileSync(normalizationCoreRootDistFile, "export {};\n", "utf-8");
|
|
const markdownCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "markdown-core",
|
|
srcFile: "render.ts",
|
|
distFile: "render.mjs",
|
|
});
|
|
const terminalCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "terminal-core",
|
|
srcFile: "links.ts",
|
|
distFile: "links.mjs",
|
|
});
|
|
const terminalCoreRootDistFile = path.join(fixture.root, "dist", "terminal-core", "links.js");
|
|
mkdirSafeDir(path.dirname(terminalCoreRootDistFile));
|
|
fs.writeFileSync(terminalCoreRootDistFile, "export {};\n", "utf-8");
|
|
const netPolicy = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "net-policy",
|
|
srcFile: "redact-sensitive-url.ts",
|
|
distFile: "redact-sensitive-url.mjs",
|
|
});
|
|
const modelCatalogCore = writeWorkspacePackageEntry({
|
|
root: fixture.root,
|
|
packageDir: "model-catalog-core",
|
|
srcFile: "provider-model-id-normalize.ts",
|
|
distFile: "provider-model-id-normalize.mjs",
|
|
});
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
|
|
const aliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["@openclaw/gateway-client/readiness"] ?? "")).toBe(
|
|
fs.realpathSync(gatewayClient.distFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/gateway-protocol/connect-error-details"] ?? "")).toBe(
|
|
fs.realpathSync(gatewayProtocol.distFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/markdown-core/render"] ?? "")).toBe(
|
|
fs.realpathSync(markdownCore.distFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/media-generation-core/catalog"] ?? "")).toBe(
|
|
fs.realpathSync(mediaGenerationCore.distFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/media-core/read-response-with-limit"] ?? "")).toBe(
|
|
fs.realpathSync(mediaCoreRootDistFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/acp-core/normalize-text"] ?? "")).toBe(
|
|
fs.realpathSync(acpCoreRootDistFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/normalization-core/record-coerce"] ?? "")).toBe(
|
|
fs.realpathSync(normalizationCoreRootDistFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/terminal-core/links"] ?? "")).toBe(
|
|
fs.realpathSync(terminalCoreRootDistFile),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/net-policy/redact-sensitive-url"] ?? "")).toBe(
|
|
fs.realpathSync(netPolicy.distFile),
|
|
);
|
|
expect(
|
|
fs.realpathSync(aliases["@openclaw/model-catalog-core/provider-model-id-normalize"] ?? ""),
|
|
).toBe(fs.realpathSync(modelCatalogCore.distFile));
|
|
});
|
|
|
|
it("derives acp-core aliases from packaged root dist when package metadata is absent", () => {
|
|
const fixture = createPluginSdkAliasFixture();
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("demo", "src/index.ts"),
|
|
);
|
|
const acpRuntimeErrors = path.join(fixture.root, "dist", "acp-core", "runtime", "errors.js");
|
|
mkdirSafeDir(path.dirname(acpRuntimeErrors));
|
|
fs.writeFileSync(acpRuntimeErrors, "export {};\n", "utf-8");
|
|
const cwdWithoutOpenClawPackage = makeTempDir();
|
|
|
|
const aliases = withCwd(cwdWithoutOpenClawPackage, () =>
|
|
withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
|
|
),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["@openclaw/acp-core/runtime/errors"] ?? "")).toBe(
|
|
fs.realpathSync(acpRuntimeErrors),
|
|
);
|
|
});
|
|
|
|
it("aliases bundled plugin package public surfaces for source plugin transforms", () => {
|
|
const { fixture, sourceApiPath, sourceRuntimeApiPath } =
|
|
createBundledPluginPackagePublicSurfaceAliasFixture();
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"),
|
|
);
|
|
|
|
const aliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["@openclaw/slack/api.js"] ?? "")).toBe(
|
|
fs.realpathSync(sourceApiPath),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/slack/runtime-api.js"] ?? "")).toBe(
|
|
fs.realpathSync(sourceRuntimeApiPath),
|
|
);
|
|
expect(aliases["@openclaw/slack/test-api.js"]).toBeUndefined();
|
|
expect(aliases["@openclaw/slack/internal.js"]).toBeUndefined();
|
|
});
|
|
|
|
it("aliases bundled plugin package test surfaces only in private QA mode", () => {
|
|
const { fixture, sourceTestApiPath } = createBundledPluginPackagePublicSurfaceAliasFixture();
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"),
|
|
);
|
|
|
|
const aliases = withEnv({ OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1", NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["@openclaw/slack/test-api.js"] ?? "")).toBe(
|
|
fs.realpathSync(sourceTestApiPath),
|
|
);
|
|
});
|
|
|
|
it("aliases bundled plugin package public surfaces to dist when dist resolution is requested", () => {
|
|
const { fixture, distApiPath, distRuntimeApiPath } =
|
|
createBundledPluginPackagePublicSurfaceAliasFixture();
|
|
const sourcePluginEntry = writePluginEntry(
|
|
fixture.root,
|
|
bundledPluginFile("qa-lab", "src/live-transports/slack/slack-live.runtime.ts"),
|
|
);
|
|
|
|
const aliases = withEnv({ NODE_ENV: undefined }, () =>
|
|
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
|
|
);
|
|
|
|
expect(fs.realpathSync(aliases["@openclaw/slack/api.js"] ?? "")).toBe(
|
|
fs.realpathSync(distApiPath),
|
|
);
|
|
expect(fs.realpathSync(aliases["@openclaw/slack/runtime-api.js"] ?? "")).toBe(
|
|
fs.realpathSync(distRuntimeApiPath),
|
|
);
|
|
});
|
|
|
|
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/channel-outbound";
|
|
|
|
export const syntheticRuntimeMarker = {
|
|
resolveOutboundSendDep,
|
|
};
|
|
`,
|
|
"utf-8",
|
|
);
|
|
const copiedChannelRuntimeShim = path.join(copiedPluginSdkDir, "channel-outbound.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,
|
|
});
|
|
let loadError: unknown;
|
|
try {
|
|
withoutAlias(copiedChannelRuntime);
|
|
} catch (error) {
|
|
loadError = error;
|
|
}
|
|
expect(loadError).toBeInstanceOf(Error);
|
|
expect((loadError as Error).message).toContain("channel-outbound");
|
|
|
|
const withAlias = createJiti(sourceLoaderBaseUrl, {
|
|
...buildPluginLoaderJitiOptions({
|
|
"openclaw/plugin-sdk/channel-outbound": copiedChannelRuntimeShim,
|
|
"@openclaw/plugin-sdk/channel-outbound": copiedChannelRuntimeShim,
|
|
}),
|
|
tryNative: false,
|
|
});
|
|
const loadedRuntime = withAlias(copiedChannelRuntime) as {
|
|
syntheticRuntimeMarker?: { resolveOutboundSendDep?: unknown };
|
|
};
|
|
expect(typeof loadedRuntime.syntheticRuntimeMarker?.resolveOutboundSendDep).toBe("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);
|
|
});
|
|
|
|
it("resolves plugin runtime modules from an explicit dev source root", () => {
|
|
const stableFixture = createPluginRuntimeAliasFixture({
|
|
distBody: "export const stableRuntime = true;\n",
|
|
});
|
|
const devFixture = createPluginRuntimeAliasFixture({
|
|
distBody: "export const devRuntime = true;\n",
|
|
});
|
|
mkdirSafeDir(path.join(devFixture.root, "extensions"));
|
|
const entry = path.join(stableFixture.root, "dist", "plugins", "loader.js");
|
|
mkdirSafeDir(path.dirname(entry));
|
|
fs.writeFileSync(entry, "export {};\n", "utf-8");
|
|
|
|
const resolved = resolvePluginRuntimeModule({
|
|
modulePath: entry,
|
|
pluginSdkResolution: "dist",
|
|
devSourceRoot: devFixture.root,
|
|
});
|
|
|
|
expect(fs.realpathSync(resolved ?? "")).toBe(fs.realpathSync(devFixture.distFile));
|
|
});
|
|
|
|
it("falls back to ancestor runtime candidates when package-root markers are unavailable", () => {
|
|
const root = makeTempDir();
|
|
const distFile = path.join(root, "dist", "plugins", "runtime", "index.js");
|
|
const loaderCachePath = path.join(root, ".cache", "tsx", "openclaw-loader.js");
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
mkdirSafeDir(path.dirname(loaderCachePath));
|
|
fs.writeFileSync(distFile, "export const createPluginRuntime = () => ({});\n", "utf-8");
|
|
fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8");
|
|
|
|
expect(
|
|
resolvePluginRuntimeModulePath({
|
|
modulePath: loaderCachePath,
|
|
argv1: path.join(root, "bin", "openclaw"),
|
|
pluginSdkResolution: "dist",
|
|
}),
|
|
).toBe(distFile);
|
|
});
|
|
|
|
it("uses the default startup argv hint for runtime fallback when argv1 is omitted", () => {
|
|
const root = makeTempDir();
|
|
const distFile = path.join(root, "dist", "plugins", "runtime", "index.js");
|
|
const loaderCacheRoot = makeTempDir();
|
|
const loaderCachePath = path.join(loaderCacheRoot, "tsx", "openclaw-loader.js");
|
|
const originalArgv1 = process.argv[1];
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
mkdirSafeDir(path.dirname(loaderCachePath));
|
|
mkdirSafeDir(path.join(root, "bin"));
|
|
fs.writeFileSync(distFile, "export const createPluginRuntime = () => ({});\n", "utf-8");
|
|
fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8");
|
|
|
|
process.argv[1] = path.join(root, "bin", "openclaw");
|
|
try {
|
|
expect(
|
|
resolvePluginRuntimeModulePath({
|
|
modulePath: loaderCachePath,
|
|
pluginSdkResolution: "dist",
|
|
}),
|
|
).toBe(distFile);
|
|
} finally {
|
|
process.argv[1] = originalArgv1;
|
|
}
|
|
});
|
|
|
|
it("prefers startup argv runtime candidates over cache ancestor fallbacks", () => {
|
|
const root = makeTempDir();
|
|
const distFile = path.join(root, "dist", "plugins", "runtime", "index.js");
|
|
const loaderCacheRoot = makeTempDir();
|
|
const cacheDistFile = path.join(loaderCacheRoot, "dist", "plugins", "runtime", "index.js");
|
|
const loaderCachePath = path.join(loaderCacheRoot, "tsx", "openclaw-loader.js");
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
mkdirSafeDir(path.dirname(cacheDistFile));
|
|
mkdirSafeDir(path.dirname(loaderCachePath));
|
|
mkdirSafeDir(path.join(root, "bin"));
|
|
fs.writeFileSync(distFile, "export const runtime = 'startup';\n", "utf-8");
|
|
fs.writeFileSync(cacheDistFile, "export const runtime = 'cache';\n", "utf-8");
|
|
fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8");
|
|
|
|
expect(
|
|
resolvePluginRuntimeModulePath({
|
|
modulePath: loaderCachePath,
|
|
argv1: path.join(root, "bin", "openclaw"),
|
|
pluginSdkResolution: "dist",
|
|
}),
|
|
).toBe(distFile);
|
|
});
|
|
|
|
it("resolves runtime fallback through symlinked startup argv", () => {
|
|
const root = makeTempDir();
|
|
const distFile = path.join(root, "dist", "plugins", "runtime", "index.js");
|
|
const binFile = path.join(root, "bin", "openclaw");
|
|
const shimRoot = makeTempDir();
|
|
const shimFile = path.join(shimRoot, "bin", "openclaw");
|
|
const loaderCachePath = path.join(makeTempDir(), "tsx", "openclaw-loader.js");
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
mkdirSafeDir(path.dirname(binFile));
|
|
mkdirSafeDir(path.dirname(shimFile));
|
|
mkdirSafeDir(path.dirname(loaderCachePath));
|
|
fs.writeFileSync(distFile, "export const runtime = 'startup';\n", "utf-8");
|
|
fs.writeFileSync(binFile, "#!/usr/bin/env node\n", "utf-8");
|
|
fs.symlinkSync(binFile, shimFile);
|
|
fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8");
|
|
|
|
expect(
|
|
resolvePluginRuntimeModulePath({
|
|
modulePath: loaderCachePath,
|
|
argv1: shimFile,
|
|
pluginSdkResolution: "dist",
|
|
}),
|
|
).toBe(fs.realpathSync(distFile));
|
|
});
|
|
|
|
it("resolves runtime fallback through npm .bin startup argv", () => {
|
|
const root = makeTempDir();
|
|
const packageRoot = path.join(root, "node_modules", "openclaw");
|
|
const distFile = path.join(packageRoot, "dist", "plugins", "runtime", "index.js");
|
|
const projectDistFile = path.join(root, "dist", "plugins", "runtime", "index.js");
|
|
const binFile = path.join(root, "node_modules", ".bin", "openclaw");
|
|
const loaderCachePath = path.join(makeTempDir(), "tsx", "openclaw-loader.js");
|
|
mkdirSafeDir(path.dirname(distFile));
|
|
mkdirSafeDir(path.dirname(projectDistFile));
|
|
mkdirSafeDir(path.dirname(binFile));
|
|
mkdirSafeDir(path.dirname(loaderCachePath));
|
|
fs.writeFileSync(distFile, "export const runtime = 'startup';\n", "utf-8");
|
|
fs.writeFileSync(projectDistFile, "export const runtime = 'project';\n", "utf-8");
|
|
fs.writeFileSync(binFile, "#!/usr/bin/env node\n", "utf-8");
|
|
fs.writeFileSync(loaderCachePath, "export {};\n", "utf-8");
|
|
|
|
expect(
|
|
resolvePluginRuntimeModulePath({
|
|
modulePath: loaderCachePath,
|
|
argv1: binFile,
|
|
pluginSdkResolution: "dist",
|
|
}),
|
|
).toBe(distFile);
|
|
});
|
|
|
|
it("reports loader, package root, and candidate paths when runtime resolution fails", () => {
|
|
const root = makeTempDir();
|
|
const modulePath = path.join(root, "dist", "plugins", "loader.js");
|
|
fs.writeFileSync(
|
|
path.join(root, "package.json"),
|
|
JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
|
|
"utf-8",
|
|
);
|
|
mkdirSafeDir(path.dirname(modulePath));
|
|
fs.writeFileSync(modulePath, "export {};\n", "utf-8");
|
|
|
|
const resolution = resolvePluginRuntimeModulePathWithDiagnostics({
|
|
modulePath,
|
|
pluginSdkResolution: "dist",
|
|
});
|
|
|
|
expect(resolution.resolvedPath).toBeNull();
|
|
expect(resolution.modulePath).toBe(modulePath);
|
|
expect(resolution.packageRoot).toBe(root);
|
|
expect(resolution.candidates).toContain(
|
|
path.join(root, "dist", "plugins", "runtime", "index.js"),
|
|
);
|
|
expect(resolution.candidates).toContain(
|
|
path.join(root, "src", "plugins", "runtime", "index.ts"),
|
|
);
|
|
});
|
|
});
|
|
|
|
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("returns different references when an explicit dev source root differs", () => {
|
|
const stableFixture = createPluginSdkAliasFixture();
|
|
const devFixture = createPluginSdkAliasFixture();
|
|
const stableRootAlias = path.join(stableFixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
|
const devRootAlias = path.join(devFixture.root, "dist", "plugin-sdk", "root-alias.cjs");
|
|
mkdirSafeDir(path.join(devFixture.root, "extensions"));
|
|
fs.writeFileSync(stableRootAlias, "module.exports = { stable: true };\n", "utf-8");
|
|
fs.writeFileSync(devRootAlias, "module.exports = { dev: true };\n", "utf-8");
|
|
const entry = writePluginEntry(
|
|
stableFixture.root,
|
|
bundledPluginFile("dev-env", "src/index.ts"),
|
|
);
|
|
|
|
const stableAliases = buildPluginLoaderAliasMap(entry, undefined, undefined, "dist", null);
|
|
const devAliases = buildPluginLoaderAliasMap(
|
|
entry,
|
|
undefined,
|
|
undefined,
|
|
"dist",
|
|
devFixture.root,
|
|
);
|
|
|
|
expect(devAliases).not.toBe(stableAliases);
|
|
expect(fs.realpathSync(stableAliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(stableRootAlias),
|
|
);
|
|
expect(fs.realpathSync(devAliases["openclaw/plugin-sdk"] ?? "")).toBe(
|
|
fs.realpathSync(devRootAlias),
|
|
);
|
|
});
|
|
|
|
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("scopes jiti fs cache by OpenClaw package version and install metadata", () => {
|
|
const root = createTrustedOpenClawPackageFixture("1.2.3-beta.4");
|
|
const tmpDir = path.join(root, "tmp");
|
|
|
|
const fsCache = withEnv({ TMPDIR: tmpDir }, () =>
|
|
resolvePluginLoaderJitiFsCacheDir({
|
|
modulePath: path.join(root, "dist", "plugins", "loader.js"),
|
|
}),
|
|
);
|
|
|
|
expect(fsCache).toContain(path.join(tmpDir, "jiti", "openclaw", "1.2.3-beta.4") + path.sep);
|
|
expect(path.basename(fsCache)).toMatch(/^\d+-\d+$/u);
|
|
});
|
|
|
|
it("preserves jiti's tmpdir guard when TMPDIR resolves to cwd", () => {
|
|
const root = createTrustedOpenClawPackageFixture("1.2.3-beta.4");
|
|
|
|
const guardedFsCache = withEnv({ TMPDIR: root, JITI_RESPECT_TMPDIR_ENV: undefined }, () =>
|
|
withCwd(root, () =>
|
|
resolvePluginLoaderJitiFsCacheDir({
|
|
modulePath: path.join(root, "dist", "plugins", "loader.js"),
|
|
}),
|
|
),
|
|
);
|
|
const respectedFsCache = withEnv({ TMPDIR: root, JITI_RESPECT_TMPDIR_ENV: "1" }, () =>
|
|
withCwd(root, () =>
|
|
resolvePluginLoaderJitiFsCacheDir({
|
|
modulePath: path.join(root, "dist", "plugins", "loader.js"),
|
|
}),
|
|
),
|
|
);
|
|
|
|
expect(guardedFsCache).toContain(path.join("jiti", "openclaw", "1.2.3-beta.4") + path.sep);
|
|
expect(guardedFsCache.startsWith(path.join(root, "jiti") + path.sep)).toBe(false);
|
|
expect(respectedFsCache).toContain(
|
|
path.join(root, "jiti", "openclaw", "1.2.3-beta.4") + path.sep,
|
|
);
|
|
});
|
|
|
|
it("adds the versioned fs cache directory to plugin loader jiti options", () => {
|
|
const root = createTrustedOpenClawPackageFixture("2.0.0");
|
|
const tmpDir = path.join(root, "tmp");
|
|
|
|
const options = withEnv({ TMPDIR: tmpDir }, () =>
|
|
buildPluginLoaderJitiOptions(
|
|
{ "openclaw/plugin-sdk": path.join(root, "dist", "plugin-sdk", "root-alias.cjs") },
|
|
{ modulePath: path.join(root, "dist", "plugins", "loader.js") },
|
|
),
|
|
);
|
|
|
|
expect(options.fsCache).toContain(path.join(tmpDir, "jiti", "openclaw", "2.0.0"));
|
|
});
|
|
|
|
it("preserves jiti's fs cache environment opt-out", () => {
|
|
const root = createTrustedOpenClawPackageFixture("2.0.0");
|
|
|
|
const explicitOptOut = withEnv({ JITI_FS_CACHE: "false" }, () =>
|
|
resolvePluginLoaderJitiFsCacheOption({
|
|
modulePath: path.join(root, "dist", "plugins", "loader.js"),
|
|
}),
|
|
);
|
|
const legacyOptOut = withEnv({ JITI_CACHE: "false", JITI_FS_CACHE: undefined }, () =>
|
|
buildPluginLoaderJitiOptions(
|
|
{ "openclaw/plugin-sdk": path.join(root, "dist", "plugin-sdk", "root-alias.cjs") },
|
|
{ modulePath: path.join(root, "dist", "plugins", "loader.js") },
|
|
),
|
|
);
|
|
|
|
expect(explicitOptOut).toBe(false);
|
|
expect(legacyOptOut.fsCache).toBe(false);
|
|
});
|
|
|
|
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("follows chained source-transform alias targets", () => {
|
|
const aliasMap = {
|
|
alpha: "/repo/alpha",
|
|
gamma: "beta/gamma",
|
|
beta: "alpha/beta",
|
|
};
|
|
|
|
const alias = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
|
|
|
|
expect(alias.gamma).toBe("/repo/alpha/beta/gamma");
|
|
});
|
|
|
|
it("does not rewrite concrete Windows drive alias targets", () => {
|
|
const aliasMap = {
|
|
"C:": "/wrong",
|
|
beta: "C:/repo/beta",
|
|
};
|
|
|
|
const alias = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
|
|
|
|
expect(alias.beta).toBe("C:/repo/beta");
|
|
});
|
|
|
|
it("stops chained source-transform alias rewrites after reaching a Windows drive target", () => {
|
|
const aliasMap = {
|
|
beta: "C:/repo/beta",
|
|
"C:": "/wrong",
|
|
alpha: "beta/alpha",
|
|
};
|
|
|
|
const alias = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
|
|
|
|
expect(alias.alpha).toBe("C:/repo/beta/alpha");
|
|
});
|
|
|
|
it("bounds cyclic source-transform alias targets", () => {
|
|
const aliasMap = {
|
|
alpha: "beta/a",
|
|
beta: "alpha/b",
|
|
gamma: "alpha/g",
|
|
};
|
|
|
|
const alias = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
|
|
|
|
expect(alias.gamma.length).toBeLessThan(32);
|
|
});
|
|
|
|
it("does not attach an empty alias map", () => {
|
|
expect(buildPluginLoaderJitiOptions({})).not.toHaveProperty("alias");
|
|
});
|
|
});
|