fix(plugin-sdk): add bundled entry error context

(cherry picked from commit 6099405ba1e8b98caa92cce4487808d212dc3544)
This commit is contained in:
huntharo
2026-04-06 12:16:26 -04:00
parent d25491aa6d
commit f2cd2c00b0
2 changed files with 88 additions and 1 deletions

View File

@@ -0,0 +1,45 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { loadBundledEntryExportSync } from "./channel-entry-contract.js";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
describe("loadBundledEntryExportSync", () => {
it("includes importer and resolved path context when a bundled sidecar is missing", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-entry-contract-"));
tempDirs.push(tempRoot);
const pluginRoot = path.join(tempRoot, "dist", "extensions", "telegram");
fs.mkdirSync(pluginRoot, { recursive: true });
const importerPath = path.join(pluginRoot, "index.js");
fs.writeFileSync(importerPath, "export default {};\n", "utf8");
let thrown: unknown;
try {
loadBundledEntryExportSync(pathToFileURL(importerPath).href, {
specifier: "./src/secret-contract.js",
});
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(Error);
const message = (thrown as Error).message;
expect(message).toContain('bundled plugin entry "./src/secret-contract.js" failed to open');
expect(message).toContain(`from "${importerPath}"`);
expect(message).toContain(`resolved "${path.join(pluginRoot, "src", "secret-contract.js")}"`);
expect(message).toContain(`plugin root "${pluginRoot}"`);
expect(message).toContain('reason "path"');
expect(message).toContain("ENOENT");
});
});

View File

@@ -85,6 +85,40 @@ function resolveEntryBoundaryRoot(importMetaUrl: string): string {
return path.dirname(fileURLToPath(importMetaUrl));
}
function formatBundledEntryUnknownError(error: unknown): string {
if (typeof error === "string") {
return error;
}
if (error === undefined) {
return "boundary validation failed";
}
try {
return JSON.stringify(error);
} catch {
return "non-serializable error";
}
}
function formatBundledEntryModuleOpenFailure(params: {
importMetaUrl: string;
specifier: string;
resolvedPath: string;
boundaryRoot: string;
failure: Extract<ReturnType<typeof openBoundaryFileSync>, { ok: false }>;
}): string {
const importerPath = fileURLToPath(params.importMetaUrl);
const errorDetail =
params.failure.error instanceof Error
? params.failure.error.message
: formatBundledEntryUnknownError(params.failure.error);
return [
`bundled plugin entry "${params.specifier}" failed to open`,
`from "${importerPath}"`,
`(resolved "${params.resolvedPath}", plugin root "${params.boundaryRoot}",`,
`reason "${params.failure.reason}"): ${errorDetail}`,
].join(" ");
}
function resolveBundledEntryModulePath(importMetaUrl: string, specifier: string): string {
const importerPath = fileURLToPath(importMetaUrl);
const resolved = path.resolve(path.dirname(importerPath), specifier);
@@ -99,7 +133,15 @@ function resolveBundledEntryModulePath(importMetaUrl: string, specifier: string)
skipLexicalRootCheck: true,
});
if (!opened.ok) {
throw new Error(`plugin entry path escapes plugin root: ${specifier}`);
throw new Error(
formatBundledEntryModuleOpenFailure({
importMetaUrl,
specifier,
resolvedPath: candidate,
boundaryRoot,
failure: opened,
}),
);
}
fs.closeSync(opened.fd);
return opened.path;