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