fix(plugin-sdk): resolve facade post-load re-entry (#61286)

This commit is contained in:
Vincent Koc
2026-04-05 11:25:36 +01:00
committed by GitHub
parent 4559ece355
commit fd0cc90427
3 changed files with 59 additions and 5 deletions

View File

@@ -75,6 +75,7 @@ afterEach(() => {
vi.restoreAllMocks();
clearRuntimeConfigSnapshot();
resetFacadeRuntimeStateForTest();
vi.doUnmock("../plugins/manifest-registry.js");
delete (globalThis as typeof globalThis & Record<string, unknown>)[FACADE_RUNTIME_GLOBAL];
if (originalBundledPluginsDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
@@ -137,6 +138,56 @@ describe("plugin-sdk facade runtime", () => {
expect(loaded.marker).toBe("circular-ok");
});
it("back-fills the sentinel before post-load facade tracking re-enters", async () => {
const dir = createBundledPluginDir("openclaw-facade-post-load-", "post-load-ok");
const reentryMarkers: Array<string | undefined> = [];
vi.resetModules();
vi.doMock("../plugins/manifest-registry.js", () => ({
loadPluginManifestRegistry: vi.fn(() => {
const load = (
globalThis as typeof globalThis & {
[FACADE_RUNTIME_GLOBAL]?: typeof loadBundledPluginPublicSurfaceModuleSync;
}
)[FACADE_RUNTIME_GLOBAL];
if (typeof load !== "function") {
throw new Error("missing facade runtime test loader");
}
const reentered = load<{ marker?: string }>({
dirName: "demo",
artifactBasename: "api.js",
});
reentryMarkers.push(reentered.marker);
return {
plugins: [
{
id: "demo",
rootDir: path.join(dir, "demo"),
origin: "bundled",
},
],
};
}),
}));
const facadeRuntime = await import("./facade-runtime.js");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
(globalThis as typeof globalThis & Record<string, unknown>)[FACADE_RUNTIME_GLOBAL] =
facadeRuntime.loadBundledPluginPublicSurfaceModuleSync;
const loaded = facadeRuntime.loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
dirName: "demo",
artifactBasename: "api.js",
});
expect(loaded.marker).toBe("post-load-ok");
expect(reentryMarkers).toEqual(["post-load-ok"]);
expect(facadeRuntime.listImportedBundledPluginFacadeIds()).toEqual(["demo"]);
facadeRuntime.resetFacadeRuntimeStateForTest();
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
});
it("clears the cache on load failure so retries re-execute", () => {
const dir = createThrowingPluginDir("openclaw-facade-throw-");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
@@ -148,7 +199,7 @@ describe("plugin-sdk facade runtime", () => {
}),
).toThrow("plugin load failure");
expect(listImportedBundledPluginFacadeIds()).toEqual(["bad"]);
expect(listImportedBundledPluginFacadeIds()).toEqual([]);
// A second call must also throw (not return a stale empty sentinel).
expect(() =>

View File

@@ -365,11 +365,14 @@ export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(param
let loaded: T;
try {
// Track the owning plugin once module evaluation begins. Facade top-level
// code may have already executed even if the module later throws.
loadedFacadePluginIds.add(resolveTrackedFacadePluginId(params));
loaded = getJiti(location.modulePath)(location.modulePath) as T;
// Back-fill the sentinel before resolving plugin ownership. That lookup can
// trigger config loading, plugin auto-enable, and other facade reads that
// re-enter this loader for the same module path.
Object.assign(sentinel, loaded);
// Track the owning plugin after the module exports are visible through the
// sentinel, so re-entrant callers never observe an empty facade object.
loadedFacadePluginIds.add(resolveTrackedFacadePluginId(params));
} catch (err) {
loadedFacadeModules.delete(location.modulePath);
throw err;