Files
openclaw/src/plugins/public-surface-loader.test.ts
Peter Steinberger 250376f885 fix: simplify bundled runtime dependency repair (#75183)
Summary:
- Merged fix: simplify bundled runtime dependency repair after ClawSweeper review.

ClawSweeper fixups:
- Included follow-up commit: fix: verify cached bundled runtime roots
- Included follow-up commit: refactor: simplify plugin runtime startup paths
- Included follow-up commit: refactor: trim plugin startup policy helpers
- Included follow-up commit: refactor: trust package manager runtime deps materialization
- Included follow-up commit: fix: narrow channel runtime deps skip policy
- Included follow-up commit: refactor: defer startup plugin runtime deps
- Ran the ClawSweeper repair loop before final review.

Validation:
- ClawSweeper review passed for head 04dc566534.
- Required merge gates passed before the squash merge.

Prepared head SHA: 04dc566534
Review: https://github.com/openclaw/openclaw/pull/75183#issuecomment-4358383786

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: clawsweeper-repair <clawsweeper-repair@users.noreply.github.com>
2026-05-01 07:49:02 +00:00

282 lines
10 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveBundledRuntimeDependencyInstallRoot } from "./bundled-runtime-deps-roots.js";
import { clearBundledRuntimeDependencyNodePaths } from "./bundled-runtime-deps.js";
const tempDirs: string[] = [];
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalPluginStageDir = process.env.OPENCLAW_PLUGIN_STAGE_DIR;
function createTempDir(): string {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-public-surface-loader-"));
tempDirs.push(tempDir);
return tempDir;
}
function createPackagedPublicArtifactWithStagedRuntimeDep(): {
bundledPluginsDir: string;
pluginRoot: string;
stageRoot: string;
} {
const packageRoot = createTempDir();
const pluginRoot = path.join(packageRoot, "dist", "extensions", "demo");
const stageRoot = path.join(packageRoot, "stage");
fs.mkdirSync(pluginRoot, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "0.0.0", type: "module" }, null, 2),
"utf8",
);
fs.writeFileSync(
path.join(pluginRoot, "package.json"),
JSON.stringify(
{
name: "@openclaw/plugin-demo",
version: "0.0.0",
type: "module",
dependencies: {
"public-artifact-runtime-dep": "1.0.0",
},
},
null,
2,
),
"utf8",
);
fs.writeFileSync(
path.join(pluginRoot, "provider-policy-api.js"),
[
'import { marker as depMarker } from "public-artifact-runtime-dep";',
"export const marker = `artifact:${depMarker}`;",
"",
].join("\n"),
"utf8",
);
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, {
env: {
...process.env,
OPENCLAW_PLUGIN_STAGE_DIR: stageRoot,
},
});
const depRoot = path.join(installRoot, "node_modules", "public-artifact-runtime-dep");
fs.mkdirSync(depRoot, { recursive: true });
fs.writeFileSync(
path.join(depRoot, "package.json"),
JSON.stringify(
{
name: "public-artifact-runtime-dep",
version: "1.0.0",
type: "module",
exports: "./index.js",
},
null,
2,
),
"utf8",
);
fs.writeFileSync(path.join(depRoot, "index.js"), 'export const marker = "staged";\n', "utf8");
return {
bundledPluginsDir: path.join(packageRoot, "dist", "extensions"),
pluginRoot,
stageRoot,
};
}
afterEach(() => {
for (const tempDir of tempDirs.splice(0)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
vi.restoreAllMocks();
vi.resetModules();
vi.doUnmock("jiti");
vi.doUnmock("node:module");
clearBundledRuntimeDependencyNodePaths();
if (originalBundledPluginsDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
}
if (originalPluginStageDir === undefined) {
delete process.env.OPENCLAW_PLUGIN_STAGE_DIR;
} else {
process.env.OPENCLAW_PLUGIN_STAGE_DIR = originalPluginStageDir;
}
});
describe("bundled plugin public surface loader", () => {
it("uses native Jiti import for Windows dist public artifact loads", async () => {
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "windows-dist-ok" })));
vi.doMock("jiti", () => ({
createJiti,
}));
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
vi.resetModules();
try {
const publicSurfaceLoader = await importFreshModule<
typeof import("./public-surface-loader.js")
>(import.meta.url, "./public-surface-loader.js?scope=windows-dist-jiti");
const tempRoot = createTempDir();
const bundledPluginsDir = path.join(tempRoot, "dist");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;
const modulePath = path.join(bundledPluginsDir, "demo", "provider-policy-api.js");
fs.mkdirSync(path.dirname(modulePath), { recursive: true });
fs.writeFileSync(modulePath, 'export const marker = "windows-dist-ok";\n', "utf8");
expect(
publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{ marker: string }>({
dirName: "demo",
artifactBasename: "provider-policy-api.js",
}).marker,
).toBe("windows-dist-ok");
expect(createJiti).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
tryNative: true,
}),
);
} finally {
platformSpy.mockRestore();
}
});
it("prefers source require for bundled source public artifacts when a ts require hook exists", async () => {
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "jiti-should-not-run" })));
vi.doMock("jiti", () => ({
createJiti,
}));
const requireLoader = Object.assign(
vi.fn(() => ({ marker: "source-require-ok" })),
{
extensions: {
".ts": vi.fn(),
},
},
);
vi.doMock("node:module", async () => {
const actual = await vi.importActual<typeof import("node:module")>("node:module");
return Object.assign({}, actual, {
createRequire: vi.fn(() => requireLoader),
});
});
vi.resetModules();
const publicSurfaceLoader = await importFreshModule<
typeof import("./public-surface-loader.js")
>(import.meta.url, "./public-surface-loader.js?scope=source-require-fast-path");
const tempRoot = createTempDir();
const bundledPluginsDir = path.join(tempRoot, "extensions");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;
const modulePath = path.join(bundledPluginsDir, "demo", "secret-contract-api.ts");
fs.mkdirSync(path.dirname(modulePath), { recursive: true });
fs.writeFileSync(modulePath, 'export const marker = "source-require-ok";\n', "utf8");
expect(
publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{ marker: string }>({
dirName: "demo",
artifactBasename: "secret-contract-api.js",
}).marker,
).toBe("source-require-ok");
expect(requireLoader).toHaveBeenCalledWith(fs.realpathSync(modulePath));
expect(createJiti).not.toHaveBeenCalled();
});
it("reuses one bundled dist jiti loader across public artifacts with the same native mode", async () => {
const createJiti = vi.fn(() => vi.fn((modulePath: string) => ({ modulePath })));
vi.doMock("jiti", () => ({
createJiti,
}));
vi.resetModules();
const publicSurfaceLoader = await importFreshModule<
typeof import("./public-surface-loader.js")
>(import.meta.url, "./public-surface-loader.js?scope=shared-bundled-jiti");
const tempRoot = createTempDir();
const bundledPluginsDir = path.join(tempRoot, "dist");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;
const firstPath = path.join(bundledPluginsDir, "demo-a", "api.js");
const secondPath = path.join(bundledPluginsDir, "demo-b", "api.js");
fs.mkdirSync(path.dirname(firstPath), { recursive: true });
fs.mkdirSync(path.dirname(secondPath), { recursive: true });
fs.writeFileSync(firstPath, 'export const marker = "demo-a";\n', "utf8");
fs.writeFileSync(secondPath, 'export const marker = "demo-b";\n', "utf8");
publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{ modulePath: string }>({
dirName: "demo-a",
artifactBasename: "api.js",
});
publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{ modulePath: string }>({
dirName: "demo-b",
artifactBasename: "api.js",
});
expect(createJiti).toHaveBeenCalledTimes(1);
});
it("loads built public artifacts through staged runtime deps", async () => {
const publicSurfaceLoader = await importFreshModule<
typeof import("./public-surface-loader.js")
>(import.meta.url, "./public-surface-loader.js?scope=runtime-deps");
const fixture = createPackagedPublicArtifactWithStagedRuntimeDep();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = fixture.bundledPluginsDir;
process.env.OPENCLAW_PLUGIN_STAGE_DIR = fixture.stageRoot;
const loaded = publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{
marker: string;
}>({
dirName: "demo",
artifactBasename: "provider-policy-api.js",
});
expect(loaded.marker).toBe("artifact:staged");
expect(fs.existsSync(path.join(fixture.pluginRoot, "node_modules"))).toBe(false);
});
it("rejects public artifacts that change after boundary validation", async () => {
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "should-not-load" })));
vi.doMock("jiti", () => ({
createJiti,
}));
vi.resetModules();
const publicSurfaceLoader = await importFreshModule<
typeof import("./public-surface-loader.js")
>(import.meta.url, "./public-surface-loader.js?scope=post-validation-identity");
const tempRoot = createTempDir();
const bundledPluginsDir = path.join(tempRoot, "dist");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir;
const modulePath = path.join(bundledPluginsDir, "demo", "api.js");
fs.mkdirSync(path.dirname(modulePath), { recursive: true });
fs.writeFileSync(modulePath, 'export const marker = "demo";\n', "utf8");
const realStatSync = fs.statSync.bind(fs);
const moduleRealPath = fs.realpathSync(modulePath);
vi.spyOn(fs, "statSync").mockImplementation((target, options) => {
const stat = realStatSync(target, options);
if (fs.realpathSync(target) !== moduleRealPath) {
return stat;
}
return Object.assign(Object.create(Object.getPrototypeOf(stat)), stat, {
ino: Number(stat.ino) + 1,
});
});
expect(() =>
publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{ marker: string }>({
dirName: "demo",
artifactBasename: "api.js",
}),
).toThrow(/changed after validation/);
expect(createJiti).not.toHaveBeenCalled();
});
});