Spaces:
Runtime error
Runtime error
File size: 5,797 Bytes
46252cd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | import { ConfigService } from '@nestjs/config';
import { ModuleRef } from '@nestjs/core';
import { PluginLoaderService } from './plugin-loader.service';
import { PluginStorageService } from './plugin-storage.service';
import { HookManager, HookHandler } from '../hooks';
import { PluginContext, PluginInstance, PluginManifest, PluginStatus, PluginType } from './plugin.interfaces';
function makePlugin(opts: { sessionScoped?: boolean; activeSessions?: string[] }): PluginInstance {
const manifest: PluginManifest = {
id: 'act-ext',
name: 'Activation Ext',
version: '1.0.0',
type: PluginType.EXTENSION,
main: 'index.js',
sessionScoped: opts.sessionScoped,
};
return { manifest, status: PluginStatus.ENABLED, config: {}, instance: null, activeSessions: opts.activeSessions };
}
describe('PluginLoaderService — per-session activation gate (hook delivery)', () => {
let loader: PluginLoaderService;
let hookManager: HookManager;
let setSessions: jest.Mock;
beforeEach(() => {
hookManager = new HookManager();
const configService = { get: jest.fn().mockReturnValue(undefined) } as unknown as ConfigService;
setSessions = jest.fn();
const pluginStorage = {
createPluginStorage: jest.fn().mockReturnValue({}),
setPluginSessions: setSessions,
} as unknown as PluginStorageService;
loader = new PluginLoaderService(configService, hookManager, pluginStorage, {
get: jest.fn(),
} as unknown as ModuleRef);
});
const seed = (plugin: PluginInstance): void => {
(loader as unknown as { plugins: Map<string, PluginInstance> }).plugins.set(plugin.manifest.id, plugin);
};
function register(plugin: PluginInstance, handler: HookHandler): void {
const ctx = (
loader as unknown as { createPluginContext: (p: PluginInstance) => PluginContext }
).createPluginContext(plugin);
ctx.registerHook('message:received', handler);
}
const fire = (sessionId: string): Promise<unknown> =>
hookManager.execute('message:received', {}, { sessionId, source: 'Engine' });
it('delivers a hook only for the sessions a session-scoped plugin is activated for', async () => {
const handler = jest.fn().mockResolvedValue({ continue: true });
register(makePlugin({ activeSessions: ['sess-1'] }), handler);
await fire('sess-1');
expect(handler).toHaveBeenCalledTimes(1);
await fire('sess-2');
expect(handler).toHaveBeenCalledTimes(1); // not delivered for the inactive session
});
it("delivers for every session when activeSessions is ['*']", async () => {
const handler = jest.fn().mockResolvedValue({ continue: true });
register(makePlugin({ activeSessions: ['*'] }), handler);
await fire('a');
await fire('b');
expect(handler).toHaveBeenCalledTimes(2);
});
it('defaults to all sessions when activeSessions is unset', async () => {
const handler = jest.fn().mockResolvedValue({ continue: true });
register(makePlugin({}), handler); // no activeSessions
await fire('anything');
expect(handler).toHaveBeenCalledTimes(1);
});
it('a global plugin (sessionScoped:false) always receives the hook, even with no active sessions', async () => {
const handler = jest.fn().mockResolvedValue({ continue: true });
register(makePlugin({ sessionScoped: false, activeSessions: [] }), handler);
await fire('x');
expect(handler).toHaveBeenCalledTimes(1);
});
describe('setPluginSessions', () => {
it('updates a session-scoped plugin and persists the new active set', () => {
const plugin = makePlugin({ activeSessions: ['*'] });
seed(plugin);
loader.setPluginSessions('act-ext', ['sess-1', 'sess-2']);
expect(plugin.activeSessions).toEqual(['sess-1', 'sess-2']);
expect(setSessions).toHaveBeenCalledWith('act-ext', ['sess-1', 'sess-2']);
});
it('rejects activating a global (non-session-scoped) plugin per session', () => {
seed(makePlugin({ sessionScoped: false }));
expect(() => loader.setPluginSessions('act-ext', ['sess-1'])).toThrow(/global/i);
expect(setSessions).not.toHaveBeenCalled();
});
});
});
// A built-in/bundled plugin must read its persisted per-session activation + config back into the
// runtime on boot, exactly like a directory plugin (loadPlugin) — otherwise the gate falls back to
// all-sessions/base-config after every restart, silently widening delivery for a plugin the operator
// had restricted.
describe('PluginLoaderService — registerBuiltInPlugin restart read-back', () => {
it('seeds activeSessions and sessionConfig from persisted storage', () => {
const hookManager = new HookManager();
const configService = { get: jest.fn().mockReturnValue(undefined) } as unknown as ConfigService;
const pluginStorage = {
getPluginConfig: jest.fn().mockReturnValue({}),
getPluginSessions: jest.fn().mockReturnValue(['sess-1']),
getPluginSessionConfig: jest.fn().mockReturnValue({ 'sess-1': { lang: 'id' } }),
getPluginEntry: jest.fn().mockReturnValue(undefined),
setPluginEntry: jest.fn(),
} as unknown as PluginStorageService;
const loader = new PluginLoaderService(configService, hookManager, pluginStorage, {
get: jest.fn(),
} as unknown as ModuleRef);
const manifest: PluginManifest = {
id: 'builtin-ext',
name: 'Built-in Ext',
version: '1.0.0',
type: PluginType.EXTENSION,
main: 'index.js',
sessionScoped: true,
};
loader.registerBuiltInPlugin(manifest, {});
const plugin = (loader as unknown as { plugins: Map<string, PluginInstance> }).plugins.get('builtin-ext');
expect(plugin?.activeSessions).toEqual(['sess-1']);
expect(plugin?.sessionConfig).toEqual({ 'sess-1': { lang: 'id' } });
});
});
|