Files
openclaw/src/plugins/bundled-dir.test.ts
2026-04-08 10:29:37 +01:00

340 lines
11 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveBundledPluginsDir } from "./bundled-dir.js";
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
const tempDirs: string[] = [];
const originalBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
const originalVitest = process.env.VITEST;
const originalArgv1 = process.argv[1];
const originalExecArgv = [...process.execArgv];
function makeRepoRoot(prefix: string): string {
return makeTrackedTempDir(prefix, tempDirs);
}
function createOpenClawRoot(params: {
prefix: string;
hasExtensions?: boolean;
hasSrc?: boolean;
hasDistRuntimeExtensions?: boolean;
hasDistExtensions?: boolean;
hasGitCheckout?: boolean;
}) {
const repoRoot = makeRepoRoot(params.prefix);
if (params.hasExtensions) {
fs.mkdirSync(path.join(repoRoot, "extensions"), { recursive: true });
}
if (params.hasSrc) {
fs.mkdirSync(path.join(repoRoot, "src"), { recursive: true });
}
if (params.hasDistRuntimeExtensions) {
fs.mkdirSync(path.join(repoRoot, "dist-runtime", "extensions"), { recursive: true });
}
if (params.hasDistExtensions) {
fs.mkdirSync(path.join(repoRoot, "dist", "extensions"), { recursive: true });
}
if (params.hasGitCheckout) {
fs.writeFileSync(path.join(repoRoot, ".git"), "gitdir: /tmp/fake.git\n", "utf8");
}
fs.writeFileSync(
path.join(repoRoot, "package.json"),
`${JSON.stringify({ name: "openclaw" }, null, 2)}\n`,
"utf8",
);
return repoRoot;
}
function seedBundledPluginTree(rootDir: string, relativeDir: string, pluginId = "discord") {
const pluginDir = path.join(rootDir, relativeDir, pluginId);
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(pluginDir, "package.json"),
`${JSON.stringify({ name: `@openclaw/${pluginId}` }, null, 2)}\n`,
"utf8",
);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
`${JSON.stringify({ id: pluginId }, null, 2)}\n`,
"utf8",
);
}
function expectResolvedBundledDir(params: {
cwd: string;
expectedDir: string;
argv1?: string;
bundledDirOverride?: string;
disableBundledPlugins?: string;
vitest?: string;
execArgv?: readonly string[];
}) {
vi.spyOn(process, "cwd").mockReturnValue(params.cwd);
process.argv[1] = params.argv1 ?? "/usr/bin/env";
process.execArgv.length = 0;
process.execArgv.push(...(params.execArgv ?? []));
if (params.vitest === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = params.vitest;
}
if (params.bundledDirOverride === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = params.bundledDirOverride;
}
if (params.disableBundledPlugins === undefined) {
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
} else {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = params.disableBundledPlugins;
}
expect(fs.realpathSync(resolveBundledPluginsDir() ?? "")).toBe(
fs.realpathSync(params.expectedDir),
);
}
function expectResolvedBundledDirFromRoot(params: {
repoRoot: string;
expectedRelativeDir: string;
argv1?: string;
bundledDirOverride?: string;
vitest?: string;
cwd?: string;
execArgv?: readonly string[];
}) {
expectResolvedBundledDir({
cwd: params.cwd ?? params.repoRoot,
expectedDir: path.join(params.repoRoot, params.expectedRelativeDir),
...(params.argv1 ? { argv1: params.argv1 } : {}),
...(params.bundledDirOverride ? { bundledDirOverride: params.bundledDirOverride } : {}),
...(params.vitest !== undefined ? { vitest: params.vitest } : {}),
...(params.execArgv ? { execArgv: params.execArgv } : {}),
});
}
function expectInstalledBundledDirScenario(params: {
installedRoot: string;
cwd?: string;
argv1?: string;
bundledDirOverride?: string;
}) {
expectResolvedBundledDirFromRoot({
repoRoot: params.installedRoot,
cwd: params.cwd ?? process.cwd(),
...(params.argv1 ? { argv1: params.argv1 } : {}),
...(params.bundledDirOverride ? { bundledDirOverride: params.bundledDirOverride } : {}),
expectedRelativeDir: path.join("dist", "extensions"),
});
}
function expectInstalledBundledDirScenarioCase(
createScenario: () => {
installedRoot: string;
cwd?: string;
argv1?: string;
bundledDirOverride?: string;
},
) {
expectInstalledBundledDirScenario(createScenario());
}
afterEach(() => {
vi.restoreAllMocks();
if (originalBundledDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledDir;
}
if (originalDisableBundledPlugins === undefined) {
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
} else {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = originalDisableBundledPlugins;
}
if (originalVitest === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = originalVitest;
}
process.argv[1] = originalArgv1;
process.execArgv.length = 0;
process.execArgv.push(...originalExecArgv);
cleanupTrackedTempDirs(tempDirs);
});
describe("resolveBundledPluginsDir", () => {
it.each([
[
"prefers the staged runtime bundled plugin tree from the package root",
{
prefix: "openclaw-bundled-dir-runtime-",
hasDistRuntimeExtensions: true,
hasDistExtensions: true,
},
{
expectedRelativeDir: path.join("dist-runtime", "extensions"),
},
],
[
"falls back to built dist/extensions in installed package roots",
{
prefix: "openclaw-bundled-dir-dist-",
hasDistExtensions: true,
},
{
expectedRelativeDir: path.join("dist", "extensions"),
},
],
[
"prefers built dist/extensions in a git checkout outside vitest",
{
prefix: "openclaw-bundled-dir-git-built-",
hasExtensions: true,
hasSrc: true,
hasDistRuntimeExtensions: true,
hasDistExtensions: true,
hasGitCheckout: true,
},
{
expectedRelativeDir: path.join("dist-runtime", "extensions"),
},
],
[
"prefers source extensions under vitest to avoid stale staged plugins",
{
prefix: "openclaw-bundled-dir-vitest-",
hasExtensions: true,
hasDistRuntimeExtensions: true,
hasDistExtensions: true,
},
{
expectedRelativeDir: "extensions",
vitest: "true",
},
],
[
"prefers source extensions during tsx-driven source execution",
{
prefix: "openclaw-bundled-dir-tsx-",
hasExtensions: true,
hasSrc: true,
hasDistRuntimeExtensions: true,
hasDistExtensions: true,
hasGitCheckout: true,
},
{
expectedRelativeDir: "extensions",
execArgv: ["--import", "tsx"],
},
],
[
"falls back to source extensions in a git checkout when built trees are missing",
{
prefix: "openclaw-bundled-dir-git-",
hasExtensions: true,
hasSrc: true,
hasGitCheckout: true,
},
{
expectedRelativeDir: "extensions",
},
],
] as const)("%s", (_name, layout, expectation) => {
const repoRoot = createOpenClawRoot(layout);
if (expectation.expectedRelativeDir === path.join("dist-runtime", "extensions")) {
seedBundledPluginTree(repoRoot, path.join("dist", "extensions"));
seedBundledPluginTree(repoRoot, path.join("dist-runtime", "extensions"));
} else if (expectation.expectedRelativeDir === path.join("dist", "extensions")) {
seedBundledPluginTree(repoRoot, path.join("dist", "extensions"));
}
expectResolvedBundledDirFromRoot({
repoRoot,
expectedRelativeDir: expectation.expectedRelativeDir,
...("vitest" in expectation ? { vitest: expectation.vitest } : {}),
...("execArgv" in expectation ? { execArgv: [...expectation.execArgv] } : {}),
});
});
it("falls back to source extensions when dist trees exist but do not contain real plugin manifests", () => {
const repoRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-incomplete-built-",
hasExtensions: true,
hasSrc: true,
hasDistRuntimeExtensions: true,
hasDistExtensions: true,
hasGitCheckout: true,
});
fs.mkdirSync(path.join(repoRoot, "dist", "extensions", "discord"), { recursive: true });
fs.mkdirSync(path.join(repoRoot, "dist-runtime", "extensions", "discord"), {
recursive: true,
});
expectResolvedBundledDirFromRoot({
repoRoot,
expectedRelativeDir: "extensions",
});
});
it("returns a stable empty bundled plugin directory when bundled plugins are disabled", () => {
const repoRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-disabled-",
hasExtensions: true,
hasSrc: true,
hasGitCheckout: true,
});
vi.spyOn(process, "cwd").mockReturnValue(repoRoot);
process.argv[1] = "/usr/bin/env";
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const bundledDir = resolveBundledPluginsDir();
expect(bundledDir).toBeTruthy();
expect(fs.existsSync(bundledDir ?? "")).toBe(true);
expect(fs.readdirSync(bundledDir ?? "")).toEqual([]);
});
it.each([
{
name: "prefers the running CLI package root over an unrelated cwd checkout",
createScenario: () => {
const installedRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-installed-",
hasDistExtensions: true,
});
seedBundledPluginTree(installedRoot, path.join("dist", "extensions"));
const cwdRepoRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-cwd-",
hasExtensions: true,
hasSrc: true,
hasGitCheckout: true,
});
return {
installedRoot,
cwd: cwdRepoRoot,
argv1: path.join(installedRoot, "openclaw.mjs"),
};
},
},
{
name: "falls back to the running installed package when the override path is stale",
createScenario: () => {
const installedRoot = createOpenClawRoot({
prefix: "openclaw-bundled-dir-override-",
hasDistExtensions: true,
});
seedBundledPluginTree(installedRoot, path.join("dist", "extensions"));
return {
installedRoot,
argv1: path.join(installedRoot, "openclaw.mjs"),
bundledDirOverride: path.join(installedRoot, "missing-extensions"),
};
},
},
] as const)("$name", ({ createScenario }) => {
expectInstalledBundledDirScenarioCase(createScenario);
});
});