Files
openclaw/src/plugin-sdk/facade-runtime.test.ts
Eva cb38535875 [plugin sdk] Project session extension slots (#75609)
Merged via squash.

Prepared head SHA: d9b670a867
Co-authored-by: 100yenadmin <239388517+100yenadmin@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
2026-05-04 08:04:27 -07:00

518 lines
16 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js";
import { createPluginActivationSource, normalizePluginsConfig } from "../plugins/config-state.js";
import {
evaluateBundledPluginPublicSurfaceAccess,
resolveBundledPluginPublicSurfaceAccess as resolveActivationCheckBundledPluginPublicSurfaceAccess,
throwForBundledPluginPublicSurfaceAccess,
} from "./facade-activation-check.runtime.js";
import {
__testing,
listImportedBundledPluginFacadeIds,
loadBundledPluginPublicSurfaceModuleSync,
resetFacadeRuntimeStateForTest,
} from "./facade-runtime.js";
import { createPluginSdkTestHarness } from "./test-helpers.js";
const { createTempDirSync } = createPluginSdkTestHarness();
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalDisableBundledPlugins = process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
const trustedBundledFixturesRoot = path.resolve("dist-runtime", "extensions");
const trustedBundledFixtureDirs: string[] = [];
function writeJsonFile(filePath: string, value: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function createTrustedBundledFixtureRoot(prefix: string): string {
fs.mkdirSync(trustedBundledFixturesRoot, { recursive: true });
const rootDir = fs.mkdtempSync(path.join(trustedBundledFixturesRoot, `.${prefix}`));
trustedBundledFixtureDirs.push(rootDir);
return rootDir;
}
function writePluginPackageJson(
pluginDir: string,
name = "demo",
type: "commonjs" | "module" = "module",
): void {
writeJsonFile(path.join(pluginDir, "package.json"), {
name: `@openclaw/plugin-${name}`,
version: "0.0.0",
type,
});
}
function createBundledPluginDir(prefix: string, marker: string): string {
const rootDir = createTrustedBundledFixtureRoot(prefix);
const pluginDir = path.join(rootDir, "demo");
fs.mkdirSync(pluginDir, { recursive: true });
writePluginPackageJson(pluginDir);
fs.writeFileSync(
path.join(pluginDir, "api.js"),
`export const marker = ${JSON.stringify(marker)};\n`,
"utf8",
);
return rootDir;
}
function useBundledPluginDirOverrideForTest(dir: string): void {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
setBundledPluginsDirOverrideForTest(dir);
}
function createThrowingPluginDir(prefix: string): string {
const rootDir = createTrustedBundledFixtureRoot(prefix);
const pluginDir = path.join(rootDir, "bad");
fs.mkdirSync(pluginDir, { recursive: true });
writePluginPackageJson(pluginDir, "bad", "commonjs");
fs.writeFileSync(
path.join(pluginDir, "api.js"),
`throw new Error("plugin load failure");\n`,
"utf8",
);
return rootDir;
}
beforeEach(() => {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
delete process.env.OPENCLAW_STATE_DIR;
});
afterEach(() => {
vi.restoreAllMocks();
for (const dir of trustedBundledFixtureDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
clearRuntimeConfigSnapshot();
resetFacadeRuntimeStateForTest();
setBundledPluginsDirOverrideForTest(undefined);
vi.doUnmock("../plugins/manifest-registry.js");
if (originalBundledPluginsDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
}
if (originalDisableBundledPlugins === undefined) {
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
} else {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = originalDisableBundledPlugins;
}
if (originalStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = originalStateDir;
}
});
describe("plugin-sdk facade runtime", () => {
it("honors trusted bundled plugin dir overrides", () => {
const overrideA = createBundledPluginDir("openclaw-facade-runtime-a-", "override-a");
const overrideB = createBundledPluginDir("openclaw-facade-runtime-b-", "override-b");
useBundledPluginDirOverrideForTest(overrideA);
const fromA = __testing.resolveFacadeModuleLocation({
dirName: "demo",
artifactBasename: "api.js",
});
expect(fromA).toEqual({
modulePath: path.join(overrideA, "demo", "api.js"),
boundaryRoot: overrideA,
});
useBundledPluginDirOverrideForTest(overrideB);
const fromB = __testing.resolveFacadeModuleLocation({
dirName: "demo",
artifactBasename: "api.js",
});
expect(fromB).toEqual({
modulePath: path.join(overrideB, "demo", "api.js"),
boundaryRoot: overrideB,
});
});
it("falls back to package source surfaces when an override dir is partial", () => {
const overrideDir = createTrustedBundledFixtureRoot("openclaw-facade-runtime-empty-");
useBundledPluginDirOverrideForTest(overrideDir);
const resolved = __testing.resolveFacadeModuleLocation({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
});
expect(resolved?.boundaryRoot).not.toBe(overrideDir);
expect(resolved?.modulePath).toMatch(
/(?:^|\/)(?:extensions|dist-runtime\/extensions)\/browser\/browser-maintenance\.(?:ts|js)$/u,
);
});
it("does not fall back to package source surfaces when bundled plugins are disabled", () => {
process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS = "1";
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
expect(
__testing.resolveFacadeModuleLocation({
dirName: "browser",
artifactBasename: "browser-maintenance.js",
}),
).toBeNull();
});
it("returns the same object identity on repeated calls (sentinel consistency)", () => {
const dir = createBundledPluginDir("openclaw-facade-identity-", "identity-check");
useBundledPluginDirOverrideForTest(dir);
const location = {
modulePath: path.join(dir, "demo", "api.js"),
boundaryRoot: dir,
};
const loader = vi.fn(() => ({ marker: "identity-check" }));
const first = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
location,
trackedPluginId: "demo",
loadModule: loader,
});
const second = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
location,
trackedPluginId: "demo",
loadModule: loader,
});
expect(first).toBe(second);
expect(first.marker).toBe("identity-check");
expect(listImportedBundledPluginFacadeIds()).toEqual(["demo"]);
expect(loader).toHaveBeenCalledTimes(1);
});
it("breaks circular facade re-entry during module evaluation", () => {
const dir = createBundledPluginDir("openclaw-facade-circular-", "circular-ok");
const location = {
modulePath: path.join(dir, "demo", "api.js"),
boundaryRoot: dir,
};
let reentered: { marker?: string } | undefined;
const loader = vi.fn(() => {
reentered = __testing.loadFacadeModuleAtLocationSync<{ marker?: string }>({
location,
trackedPluginId: "demo",
loadModule: loader,
});
return { marker: "circular-ok" };
});
const loaded = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
location,
trackedPluginId: "demo",
loadModule: loader,
});
expect(loaded.marker).toBe("circular-ok");
expect(reentered).toBe(loaded);
expect(reentered?.marker).toBe("circular-ok");
expect(loader).toHaveBeenCalledTimes(1);
});
it("back-fills the sentinel before post-load facade tracking re-enters", () => {
const dir = createBundledPluginDir("openclaw-facade-post-load-", "post-load-ok");
const location = {
modulePath: path.join(dir, "demo", "api.js"),
boundaryRoot: dir,
};
const reentryMarkers: Array<string | undefined> = [];
const loader = vi.fn(() => ({ marker: "post-load-ok" }));
const loaded = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
location,
trackedPluginId: () => {
const reentered = __testing.loadFacadeModuleAtLocationSync<{ marker?: string }>({
location,
trackedPluginId: "demo",
loadModule: loader,
});
reentryMarkers.push(reentered.marker);
return "demo";
},
loadModule: loader,
});
expect(loaded.marker).toBe("post-load-ok");
expect(reentryMarkers.length).toBeGreaterThan(0);
expect(reentryMarkers.every((marker) => marker === "post-load-ok")).toBe(true);
expect(listImportedBundledPluginFacadeIds()).toEqual(["demo"]);
expect(loader).toHaveBeenCalledTimes(1);
});
it("clears the cache on load failure so retries re-execute", () => {
const dir = createThrowingPluginDir("openclaw-facade-throw-");
useBundledPluginDirOverrideForTest(dir);
expect(() =>
loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
dirName: "bad",
artifactBasename: "api.js",
}),
).toThrow("plugin load failure");
expect(listImportedBundledPluginFacadeIds()).toEqual([]);
// A second call must also throw (not return a stale empty sentinel).
expect(() =>
loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
dirName: "bad",
artifactBasename: "api.js",
}),
).toThrow("plugin load failure");
});
it("blocks runtime-api facade loads for bundled plugins that are not activated", () => {
const access = evaluateBundledPluginPublicSurfaceAccess({
params: {
dirName: "discord",
artifactBasename: "runtime-api.js",
},
manifestRecord: {
id: "discord",
origin: "bundled",
enabledByDefault: false,
rootDir: "/tmp/discord",
channels: ["discord"],
},
config: {},
normalizedPluginsConfig: normalizePluginsConfig(),
activationSource: createPluginActivationSource({ config: {} }),
autoEnabledReasons: {},
});
expect(access.allowed).toBe(false);
expect(access.pluginId).toBe("discord");
expect(access.reason).toBeTruthy();
expect(() =>
throwForBundledPluginPublicSurfaceAccess({
access,
request: {
dirName: "discord",
artifactBasename: "runtime-api.js",
},
}),
).toThrow(/Bundled plugin public surface access blocked/);
expect(access.allowed).toBe(false);
});
it("allows runtime-api facade loads when the bundled plugin is explicitly enabled", () => {
const dir = createTempDirSync("openclaw-facade-runtime-enabled-");
fs.mkdirSync(path.join(dir, "discord"), { recursive: true });
fs.writeFileSync(
path.join(dir, "discord", "runtime-api.js"),
'export const marker = "runtime-api-enabled";\n',
"utf8",
);
const config = {
plugins: {
entries: {
discord: {
enabled: true,
},
},
},
} as const;
const access = evaluateBundledPluginPublicSurfaceAccess({
params: {
dirName: "discord",
artifactBasename: "runtime-api.js",
},
manifestRecord: {
id: "discord",
origin: "bundled",
enabledByDefault: false,
rootDir: "/tmp/discord",
channels: ["discord"],
},
config,
normalizedPluginsConfig: normalizePluginsConfig(config.plugins),
activationSource: createPluginActivationSource({ config }),
autoEnabledReasons: {},
});
const loader = vi.fn(() => ({ marker: "runtime-api-enabled" }));
const location = {
modulePath: path.join(dir, "discord", "runtime-api.js"),
boundaryRoot: dir,
};
expect(access.allowed).toBe(true);
const loaded = __testing.loadFacadeModuleAtLocationSync<{ marker: string }>({
location,
trackedPluginId: "discord",
loadModule: loader,
});
expect(loaded.marker).toBe("runtime-api-enabled");
expect(loader).toHaveBeenCalledTimes(1);
});
it("resolves a globally-installed plugin whose rootDir basename matches the dirName", () => {
const lineDir = createTempDirSync("openclaw-facade-global-line-");
fs.mkdirSync(lineDir, { recursive: true });
fs.writeFileSync(
path.join(lineDir, "runtime-api.js"),
'export const marker = "global-line";\n',
"utf8",
);
fs.writeFileSync(
path.join(lineDir, "package.json"),
JSON.stringify({
name: "@openclaw/line",
version: "0.0.0",
openclaw: {
extensions: ["./runtime-api.js"],
channel: { id: "line" },
},
}),
"utf8",
);
fs.writeFileSync(
path.join(lineDir, "openclaw.plugin.json"),
JSON.stringify({
id: "line",
channels: ["line"],
configSchema: { type: "object", additionalProperties: false, properties: {} },
}),
"utf8",
);
expect(
__testing.resolveRegistryPluginModuleLocationFromRegistry({
registry: [
{
id: "line",
rootDir: lineDir,
channels: ["line"],
},
],
dirName: "line",
artifactBasename: "runtime-api.js",
}),
).toEqual({
modulePath: path.join(lineDir, "runtime-api.js"),
boundaryRoot: lineDir,
});
});
it("resolves a globally-installed plugin with an encoded scoped rootDir basename", () => {
const encodedDir = createTempDirSync("openclaw-facade-encoded-line-");
fs.mkdirSync(encodedDir, { recursive: true });
fs.writeFileSync(
path.join(encodedDir, "runtime-api.js"),
'export const marker = "encoded-global-line";\n',
"utf8",
);
fs.writeFileSync(
path.join(encodedDir, "package.json"),
JSON.stringify({
name: "@openclaw/line",
version: "0.0.0",
openclaw: {
extensions: ["./runtime-api.js"],
channel: { id: "line" },
},
}),
"utf8",
);
fs.writeFileSync(
path.join(encodedDir, "openclaw.plugin.json"),
JSON.stringify({
id: "line",
channels: ["line"],
configSchema: { type: "object", additionalProperties: false, properties: {} },
}),
"utf8",
);
expect(
__testing.resolveRegistryPluginModuleLocationFromRegistry({
registry: [
{
id: "line",
rootDir: encodedDir,
channels: ["line"],
},
],
dirName: "line",
artifactBasename: "runtime-api.js",
}),
).toEqual({
modulePath: path.join(encodedDir, "runtime-api.js"),
boundaryRoot: encodedDir,
});
});
it("keeps shared runtime-core facades available without plugin activation", () => {
setRuntimeConfigSnapshot({});
for (const dirName of ["speech-core", "image-generation-core", "media-understanding-core"]) {
expect(
resolveActivationCheckBundledPluginPublicSurfaceAccess({
dirName,
artifactBasename: "runtime-api.js",
location: null,
sourceExtensionsRoot: "",
resolutionKey: `runtime-core:${dirName}`,
}),
).toEqual({
allowed: true,
pluginId: dirName,
});
}
});
it("prefers the source runtime snapshot for facade activation checks", () => {
const dir = createTempDirSync("openclaw-facade-source-snapshot-");
fs.mkdirSync(path.join(dir, "demo"), { recursive: true });
fs.writeFileSync(
path.join(dir, "demo", "runtime-api.js"),
'export const marker = "source-snapshot";\n',
"utf8",
);
fs.writeFileSync(
path.join(dir, "demo", "openclaw.plugin.json"),
JSON.stringify({
id: "demo",
}),
"utf8",
);
useBundledPluginDirOverrideForTest(dir);
setRuntimeConfigSnapshot(
{
plugins: {},
},
{
plugins: {
entries: {
demo: {
enabled: true,
},
},
},
},
);
expect(
resolveActivationCheckBundledPluginPublicSurfaceAccess({
dirName: "demo",
artifactBasename: "runtime-api.js",
location: {
modulePath: path.join(dir, "demo", "runtime-api.js"),
boundaryRoot: dir,
},
sourceExtensionsRoot: dir,
resolutionKey: "source-snapshot-demo",
}),
).toEqual({
allowed: true,
pluginId: "demo",
});
});
});