mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 14:51:08 +00:00
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterAll, describe, expect, it } from "vitest";
|
|
|
|
function mkdtempSafe(prefix: string) {
|
|
const dir = fs.mkdtempSync(prefix);
|
|
try {
|
|
fs.chmodSync(dir, 0o755);
|
|
} catch {
|
|
// Best-effort
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
const fixtureRoot = mkdtempSafe(path.join(os.tmpdir(), "openclaw-plugin-graceful-"));
|
|
let tempDirIndex = 0;
|
|
|
|
afterAll(() => {
|
|
try {
|
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
|
} catch {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
function makeTempDir() {
|
|
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function writePlugin(params: { id: string; body: string; dir?: string }): {
|
|
id: string;
|
|
file: string;
|
|
dir: string;
|
|
} {
|
|
const dir = params.dir ?? makeTempDir();
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const filename = `${params.id}.cjs`;
|
|
const file = path.join(dir, filename);
|
|
fs.writeFileSync(file, params.body, "utf-8");
|
|
fs.writeFileSync(
|
|
path.join(dir, "openclaw.plugin.json"),
|
|
JSON.stringify({
|
|
id: params.id,
|
|
name: params.id,
|
|
version: "1.0.0",
|
|
main: filename,
|
|
configSchema: { type: "object" },
|
|
}),
|
|
"utf-8",
|
|
);
|
|
return { id: params.id, file, dir };
|
|
}
|
|
|
|
function readPluginId(pluginPath: string): string {
|
|
const manifestPath = path.join(path.dirname(pluginPath), "openclaw.plugin.json");
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as { id: string };
|
|
return manifest.id;
|
|
}
|
|
|
|
async function loadPlugins(pluginPaths: string[], warnings?: string[]) {
|
|
const { loadOpenClawPlugins, clearPluginLoaderCache } = await import("./loader.js");
|
|
clearPluginLoaderCache();
|
|
const allow = pluginPaths.map((pluginPath) => readPluginId(pluginPath));
|
|
return loadOpenClawPlugins({
|
|
cache: false,
|
|
config: {
|
|
plugins: {
|
|
enabled: true,
|
|
load: { paths: pluginPaths },
|
|
allow,
|
|
},
|
|
},
|
|
logger: {
|
|
info: () => {},
|
|
debug: () => {},
|
|
error: () => {},
|
|
warn: (message: string) => warnings?.push(message),
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("graceful plugin initialization failure", () => {
|
|
it("does not crash when register throws", async () => {
|
|
const plugin = writePlugin({
|
|
id: "throws-on-register",
|
|
body: `module.exports = { id: "throws-on-register", register() { throw new Error("config schema mismatch"); } };`,
|
|
});
|
|
|
|
await expect(loadPlugins([plugin.file])).resolves.toBeDefined();
|
|
});
|
|
|
|
it("keeps loading other plugins after one register failure", async () => {
|
|
const failing = writePlugin({
|
|
id: "plugin-fail",
|
|
body: `module.exports = { id: "plugin-fail", register() { throw new Error("boom"); } };`,
|
|
});
|
|
const working = writePlugin({
|
|
id: "plugin-ok",
|
|
body: `module.exports = { id: "plugin-ok", register() {} };`,
|
|
});
|
|
|
|
const registry = await loadPlugins([failing.file, working.file]);
|
|
|
|
expect(registry.plugins.find((plugin) => plugin.id === "plugin-ok")?.status).toBe("loaded");
|
|
});
|
|
|
|
it("records failed register metadata", async () => {
|
|
const plugin = writePlugin({
|
|
id: "register-error",
|
|
body: `module.exports = { id: "register-error", register() { throw new Error("brutal config fail"); } };`,
|
|
});
|
|
|
|
const before = new Date();
|
|
const registry = await loadPlugins([plugin.file]);
|
|
const after = new Date();
|
|
|
|
const failed = registry.plugins.find((entry) => entry.id === "register-error");
|
|
expect(failed).toBeDefined();
|
|
expect(failed?.status).toBe("error");
|
|
expect(failed?.failurePhase).toBe("register");
|
|
expect(failed?.error).toContain("brutal config fail");
|
|
expect(failed?.failedAt).toBeInstanceOf(Date);
|
|
expect(failed?.failedAt?.getTime()).toBeGreaterThanOrEqual(before.getTime());
|
|
expect(failed?.failedAt?.getTime()).toBeLessThanOrEqual(after.getTime());
|
|
});
|
|
|
|
it("records validation failures before register", async () => {
|
|
const plugin = writePlugin({
|
|
id: "missing-register",
|
|
body: `module.exports = { id: "missing-register" };`,
|
|
});
|
|
|
|
const registry = await loadPlugins([plugin.file]);
|
|
const failed = registry.plugins.find((entry) => entry.id === "missing-register");
|
|
|
|
expect(failed?.status).toBe("error");
|
|
expect(failed?.failurePhase).toBe("validation");
|
|
expect(failed?.error).toBe("plugin export missing register/activate");
|
|
});
|
|
|
|
it("logs a startup summary grouped by failure phase", async () => {
|
|
const registerFailure = writePlugin({
|
|
id: "warn-register",
|
|
body: `module.exports = { id: "warn-register", register() { throw new Error("bad config"); } };`,
|
|
});
|
|
const validationFailure = writePlugin({
|
|
id: "warn-validation",
|
|
body: `module.exports = { id: "warn-validation" };`,
|
|
});
|
|
|
|
const warnings: string[] = [];
|
|
await loadPlugins([registerFailure.file, validationFailure.file], warnings);
|
|
|
|
const summary = warnings.find((warning) => warning.includes("failed to initialize"));
|
|
expect(summary).toBeDefined();
|
|
expect(summary).toContain("register: warn-register");
|
|
expect(summary).toContain("validation: warn-validation");
|
|
expect(summary).toContain("openclaw plugins list");
|
|
});
|
|
});
|