fix(FN-3794): pass unload context and isolate WhatsApp sessions per project
- Extend PluginOnUnload to receive runtime context and wire ctx through plugin-loader unload hooks - Scope WhatsApp chat plugin connections by project root to avoid cross-project session leakage - Update plugin authoring docs and add a patch changeset for @runfusion/fusion - Align plugin test suites across WhatsApp and example/runtime plugins with the new onUnload context contract Fusion-Task-Id: FN-3794
This commit is contained in:
@@ -129,6 +129,8 @@ function droidPluginModulePath(): string {
|
||||
// Mock TaskStore for testing
|
||||
const mockTaskStore = {
|
||||
logActivity: vi.fn(),
|
||||
getRootDir: () => "/tmp/plugin-loader-test-root",
|
||||
getPluginStore: vi.fn(),
|
||||
} as any;
|
||||
|
||||
type MockStructuredLogger = {
|
||||
@@ -785,6 +787,42 @@ export default plugin;
|
||||
expect(loader.isPluginLoaded("remove-test")).toBe(false);
|
||||
});
|
||||
|
||||
it("passes plugin context to onUnload", async () => {
|
||||
await pluginStore.init();
|
||||
|
||||
const plugin = makePlugin(makeManifest({ id: "stop-context-test" }));
|
||||
const pluginDir = join(rootDir, "plugins");
|
||||
const pluginPath = await writePluginWithHooks(
|
||||
pluginDir,
|
||||
"stop-context.js",
|
||||
{
|
||||
onUnload:
|
||||
"(ctx => { globalThis.__pluginUnloadCtx = { pluginId: ctx.pluginId, taskStore: ctx.taskStore }; })",
|
||||
},
|
||||
plugin.manifest,
|
||||
);
|
||||
|
||||
await pluginStore.registerPlugin({
|
||||
manifest: plugin.manifest,
|
||||
path: pluginPath,
|
||||
});
|
||||
|
||||
const loader = new PluginLoader({
|
||||
pluginStore,
|
||||
taskStore: mockTaskStore,
|
||||
});
|
||||
|
||||
await loader.loadPlugin("stop-context-test");
|
||||
await loader.stopPlugin("stop-context-test");
|
||||
|
||||
const unloadCtx = (globalThis as { __pluginUnloadCtx?: { pluginId: string; taskStore: unknown } })
|
||||
.__pluginUnloadCtx;
|
||||
expect(unloadCtx).toBeDefined();
|
||||
expect(unloadCtx?.pluginId).toBe("stop-context-test");
|
||||
expect(unloadCtx?.taskStore).toBe(mockTaskStore);
|
||||
delete (globalThis as { __pluginUnloadCtx?: unknown }).__pluginUnloadCtx;
|
||||
});
|
||||
|
||||
it("no-ops for non-loaded plugin", async () => {
|
||||
await pluginStore.init();
|
||||
|
||||
|
||||
@@ -374,8 +374,9 @@ export class PluginLoader extends EventEmitter<{
|
||||
|
||||
// Call onUnload with timeout
|
||||
try {
|
||||
const ctx = await this.createContext(oldPlugin);
|
||||
await this.withTimeout(
|
||||
this.safeCallHook(oldPlugin, "onUnload", []),
|
||||
this.safeCallHook(oldPlugin, "onUnload", [ctx]),
|
||||
timeoutMs,
|
||||
`onUnload timeout for ${pluginId}`,
|
||||
);
|
||||
@@ -645,8 +646,9 @@ export class PluginLoader extends EventEmitter<{
|
||||
|
||||
try {
|
||||
// Call onUnload hook
|
||||
const ctx = await this.createContext(plugin);
|
||||
await this.withTimeout(
|
||||
this.safeCallHook(plugin, "onUnload", []),
|
||||
this.safeCallHook(plugin, "onUnload", [ctx]),
|
||||
5000,
|
||||
`onUnload timeout for ${pluginId}`,
|
||||
);
|
||||
|
||||
@@ -154,7 +154,7 @@ export interface PluginLogger {
|
||||
/** Lifecycle hook: called when plugin is loaded */
|
||||
export type PluginOnLoad = (ctx: PluginContext) => Promise<void> | void;
|
||||
/** Lifecycle hook: called when plugin is unloaded */
|
||||
export type PluginOnUnload = () => Promise<void> | void;
|
||||
export type PluginOnUnload = (ctx: PluginContext) => Promise<void> | void;
|
||||
/** Lifecycle hook: called during database schema initialization */
|
||||
export type PluginOnSchemaInit = (db: Database) => Promise<void> | void;
|
||||
/** Lifecycle hook: called when a task is created */
|
||||
|
||||
Reference in New Issue
Block a user