Files
openclaw/src/plugins/plugin-graceful-init-failure.test.ts
2026-04-03 23:24:02 +09:00

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");
});
});