@elizaos/plugin-ngrok 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +325 -0
  2. package/dist/__tests__/NgrokTestSuite.d.ts +6 -0
  3. package/dist/__tests__/NgrokTestSuite.d.ts.map +1 -0
  4. package/dist/__tests__/NgrokTestSuite.js +92 -0
  5. package/dist/__tests__/NgrokTestSuite.js.map +1 -0
  6. package/dist/actions/get-tunnel-status.d.ts +4 -0
  7. package/dist/actions/get-tunnel-status.d.ts.map +1 -0
  8. package/dist/actions/get-tunnel-status.js +186 -0
  9. package/dist/actions/get-tunnel-status.js.map +1 -0
  10. package/dist/actions/start-tunnel.d.ts +4 -0
  11. package/dist/actions/start-tunnel.d.ts.map +1 -0
  12. package/dist/actions/start-tunnel.js +221 -0
  13. package/dist/actions/start-tunnel.js.map +1 -0
  14. package/dist/actions/stop-tunnel.d.ts +4 -0
  15. package/dist/actions/stop-tunnel.d.ts.map +1 -0
  16. package/dist/actions/stop-tunnel.js +174 -0
  17. package/dist/actions/stop-tunnel.js.map +1 -0
  18. package/dist/environment.d.ts +12 -0
  19. package/dist/environment.d.ts.map +1 -0
  20. package/dist/environment.js +68 -0
  21. package/dist/environment.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +29 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/services/NgrokService.d.ts +30 -0
  27. package/dist/services/NgrokService.d.ts.map +1 -0
  28. package/dist/services/NgrokService.js +333 -0
  29. package/dist/services/NgrokService.js.map +1 -0
  30. package/package.json +63 -0
  31. package/src/__tests__/NgrokTestSuite.ts +110 -0
  32. package/src/__tests__/debug-mock.test.ts +15 -0
  33. package/src/__tests__/e2e/real-ngrok.test.ts +543 -0
  34. package/src/__tests__/integration/webhook-scenarios.test.ts +463 -0
  35. package/src/__tests__/mocks/NgrokServiceMock.ts +76 -0
  36. package/src/__tests__/ngrok-integration.test.ts +521 -0
  37. package/src/__tests__/test-config.ts +83 -0
  38. package/src/__tests__/test-helpers.ts +43 -0
  39. package/src/__tests__/test-setup.ts +174 -0
  40. package/src/__tests__/test-utils.ts +155 -0
  41. package/src/__tests__/unit/actions.test.ts +402 -0
  42. package/src/__tests__/unit/environment.test.ts +352 -0
  43. package/src/actions/get-tunnel-status.ts +218 -0
  44. package/src/actions/start-tunnel.ts +255 -0
  45. package/src/actions/stop-tunnel.ts +203 -0
  46. package/src/environment.ts +75 -0
  47. package/src/index.ts +33 -0
  48. package/src/services/NgrokService.ts +401 -0
@@ -0,0 +1,402 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
+ import type { HandlerCallback, IAgentRuntime, Memory, State } from '@elizaos/core';
3
+
4
+ type SimpleMock<TArgs extends readonly unknown[] = unknown[], TReturn = unknown> = ((
5
+ ...args: TArgs
6
+ ) => TReturn) & {
7
+ calls: TArgs[];
8
+ _returnValue: TReturn | undefined;
9
+ _implementation: ((...args: unknown[]) => TReturn) | null;
10
+ mockReturnValue: (value: TReturn) => SimpleMock<TArgs, TReturn>;
11
+ mockResolvedValue: (value: Awaited<TReturn>) => SimpleMock<TArgs, TReturn>;
12
+ mockRejectedValue: (reason: unknown) => SimpleMock<TArgs, TReturn>;
13
+ mockImplementation: (impl: (...args: unknown[]) => TReturn) => SimpleMock<TArgs, TReturn>;
14
+ mock: { calls: TArgs[]; results: unknown[] };
15
+ };
16
+
17
+ // Use local mock implementation until core test-utils build issue is resolved
18
+ const mock = <const TArgs extends readonly unknown[] = unknown[], TReturn = unknown>(): SimpleMock<
19
+ TArgs,
20
+ TReturn
21
+ > => {
22
+ const calls: TArgs[] = [];
23
+ const fn = ((...args: TArgs) => {
24
+ calls.push(args);
25
+ if (typeof fn._implementation === 'function') {
26
+ return fn._implementation(...args);
27
+ }
28
+ return fn._returnValue as TReturn;
29
+ }) as SimpleMock<TArgs, TReturn>;
30
+ fn.calls = calls;
31
+ fn._returnValue = undefined;
32
+ fn._implementation = null;
33
+ fn.mockReturnValue = (value: TReturn) => {
34
+ fn._returnValue = value;
35
+ fn._implementation = null;
36
+ return fn;
37
+ };
38
+ fn.mockResolvedValue = (value: Awaited<TReturn>) => {
39
+ fn._returnValue = Promise.resolve(value) as TReturn;
40
+ fn._implementation = null;
41
+ return fn;
42
+ };
43
+ fn.mockRejectedValue = (reason: unknown) => {
44
+ fn._returnValue = Promise.reject(reason) as TReturn;
45
+ fn._implementation = null;
46
+ return fn;
47
+ };
48
+ fn.mockImplementation = (impl: (...args: unknown[]) => TReturn) => {
49
+ fn._implementation = impl;
50
+ fn._returnValue = undefined;
51
+ return fn;
52
+ };
53
+ fn.mock = { calls, results: [] };
54
+ return fn;
55
+ };
56
+
57
+ import { getTunnelStatusAction } from '../../actions/get-tunnel-status';
58
+ import { startTunnelAction } from '../../actions/start-tunnel';
59
+ import { stopTunnelAction } from '../../actions/stop-tunnel';
60
+ import { MockNgrokService } from '../mocks/NgrokServiceMock';
61
+ import { createMockMemory, createMockRuntime, createMockState } from '../test-utils';
62
+
63
+ describe('Ngrok Actions - Validation and Error Handling', () => {
64
+ let mockRuntime: IAgentRuntime;
65
+ let mockTunnelService: MockNgrokService;
66
+ let mockCallback: HandlerCallback;
67
+ let mockMemory: Memory;
68
+ let mockState: State;
69
+ let mockUseModel: ReturnType<typeof mock>;
70
+
71
+ beforeEach(() => {
72
+ mockTunnelService = new MockNgrokService({} as IAgentRuntime);
73
+
74
+ // Reset all mocks to default behavior
75
+ mockTunnelService.startTunnel.mockResolvedValue('https://test.ngrok.io');
76
+ mockTunnelService.stopTunnel.mockResolvedValue(undefined);
77
+ mockTunnelService.isActive.mockReturnValue(false);
78
+ mockTunnelService.getStatus.mockReturnValue({
79
+ active: false,
80
+ url: null,
81
+ port: null,
82
+ startedAt: null,
83
+ provider: 'ngrok',
84
+ });
85
+
86
+ mockUseModel = mock().mockResolvedValue('{"port": 8080}');
87
+ mockRuntime = createMockRuntime({
88
+ getService: mock().mockImplementation((name: string) => {
89
+ if (name === 'tunnel' || name === 'ngrok-tunnel') {
90
+ return mockTunnelService;
91
+ }
92
+ return null;
93
+ }),
94
+ useModel: mockUseModel,
95
+ });
96
+
97
+ mockCallback = mock();
98
+ mockMemory = createMockMemory();
99
+ mockState = createMockState();
100
+ });
101
+
102
+ afterEach(() => {
103
+ // Clear any mock state between tests
104
+ });
105
+
106
+ describe('startTunnelAction - Validation', () => {
107
+ it('should validate that tunnel service exists', async () => {
108
+ const runtimeWithoutService = createMockRuntime({
109
+ getService: mock().mockReturnValue(null),
110
+ });
111
+
112
+ const isValid = await startTunnelAction.validate(runtimeWithoutService, mockMemory);
113
+
114
+ expect(isValid).toBe(false);
115
+ });
116
+
117
+ it('should validate when tunnel service is available', async () => {
118
+ const isValid = await startTunnelAction.validate(mockRuntime, mockMemory);
119
+
120
+ expect(isValid).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe('startTunnelAction - Error Handling', () => {
125
+ it('should handle service not available', async () => {
126
+ const runtimeWithoutService = createMockRuntime({
127
+ getService: mock().mockReturnValue(null),
128
+ });
129
+ const memory = createMockMemory({
130
+ content: { text: 'start tunnel on port 8080' },
131
+ });
132
+
133
+ const result = await startTunnelAction.handler(
134
+ runtimeWithoutService,
135
+ memory,
136
+ mockState,
137
+ {},
138
+ mockCallback
139
+ );
140
+
141
+ expect(result.values?.success).toBe(false);
142
+ expect(result.values?.error).toBe('service_unavailable');
143
+ expect(mockCallback.calls.length).toBeGreaterThan(0);
144
+ });
145
+
146
+ it('should handle invalid port numbers gracefully', async () => {
147
+ const runtimeWithInvalidPort = createMockRuntime({
148
+ getService: mock().mockImplementation((name: string) => {
149
+ if (name === 'tunnel' || name === 'ngrok-tunnel') {
150
+ return mockTunnelService;
151
+ }
152
+ return null;
153
+ }),
154
+ useModel: mock().mockResolvedValue('{"port": -1}'),
155
+ });
156
+ const memory = createMockMemory({
157
+ content: { text: 'start tunnel on port -1' },
158
+ });
159
+ mockTunnelService.startTunnel.mockResolvedValue('https://test.ngrok.io');
160
+
161
+ const result = await startTunnelAction.handler(
162
+ runtimeWithInvalidPort,
163
+ memory,
164
+ mockState,
165
+ {},
166
+ mockCallback
167
+ );
168
+
169
+ expect(result.values?.success).toBe(true);
170
+ expect(result.values?.tunnelUrl).toContain('ngrok.io');
171
+ expect(mockTunnelService.startTunnel.calls.length).toBeGreaterThan(0);
172
+ });
173
+
174
+ it('should handle port extraction failure', async () => {
175
+ mockMemory.content = { text: 'start tunnel' };
176
+ mockUseModel.mockResolvedValue('invalid json');
177
+ mockTunnelService.startTunnel.mockResolvedValue('https://test.ngrok.io');
178
+
179
+ const result = await startTunnelAction.handler(
180
+ mockRuntime,
181
+ mockMemory,
182
+ mockState,
183
+ {},
184
+ mockCallback
185
+ );
186
+
187
+ expect(result.values?.success).toBe(true);
188
+ expect(result.values?.tunnelUrl).toContain('ngrok.io');
189
+ expect(mockTunnelService.startTunnel.calls.length).toBeGreaterThan(0);
190
+ expect(mockTunnelService.startTunnel.calls[0][0]).toBe(3000); // Should use default
191
+ });
192
+
193
+ it('should handle tunnel start failure', async () => {
194
+ // Create a fresh mock service for this test that will fail
195
+ const failingTunnelService = new MockNgrokService({} as IAgentRuntime);
196
+ failingTunnelService.startTunnel.mockRejectedValue(new Error('Ngrok auth failed'));
197
+ failingTunnelService.isActive.mockReturnValue(false);
198
+
199
+ const failingRuntime = createMockRuntime({
200
+ getService: mock().mockImplementation((name: string) => {
201
+ if (name === 'tunnel' || name === 'ngrok-tunnel') {
202
+ return failingTunnelService;
203
+ }
204
+ return null;
205
+ }),
206
+ useModel: mock().mockResolvedValue('{"port": 8080}'),
207
+ });
208
+
209
+ mockMemory.content = { text: 'start tunnel on port 8080' };
210
+
211
+ const result = await startTunnelAction.handler(
212
+ failingRuntime,
213
+ mockMemory,
214
+ mockState,
215
+ {},
216
+ mockCallback
217
+ );
218
+
219
+ expect(result.values?.success).toBe(false);
220
+ expect(result.values?.error).toBe('Ngrok auth failed');
221
+ expect(mockCallback.calls.length).toBeGreaterThan(0);
222
+ const call = mockCallback.calls[0][0];
223
+ expect(call.text).toContain('Failed to start ngrok tunnel');
224
+ expect(call.metadata.error).toBe('Ngrok auth failed');
225
+ });
226
+
227
+ it('should handle port already in use', async () => {
228
+ // Create a fresh mock service for this test that reports as active
229
+ const activeTunnelService = new MockNgrokService({} as IAgentRuntime);
230
+ activeTunnelService.isActive.mockReturnValue(true);
231
+ activeTunnelService.getStatus.mockReturnValue({
232
+ active: true,
233
+ url: 'https://existing.ngrok.io',
234
+ port: 8080,
235
+ startedAt: new Date(),
236
+ provider: 'ngrok',
237
+ });
238
+
239
+ const activeRuntime = createMockRuntime({
240
+ getService: mock().mockImplementation((name: string) => {
241
+ if (name === 'tunnel' || name === 'ngrok-tunnel') {
242
+ return activeTunnelService;
243
+ }
244
+ return null;
245
+ }),
246
+ useModel: mock().mockResolvedValue('{"port": 8080}'),
247
+ });
248
+
249
+ mockMemory.content = { text: 'start tunnel on port 8080' };
250
+
251
+ const result = await startTunnelAction.handler(
252
+ activeRuntime,
253
+ mockMemory,
254
+ mockState,
255
+ {},
256
+ mockCallback
257
+ );
258
+
259
+ expect(result.values?.success).toBe(false);
260
+ expect(result.values?.error).toBe('tunnel_already_active');
261
+ expect(mockCallback.calls.length).toBeGreaterThan(0);
262
+ expect(mockCallback.calls[0][0].text).toContain('Tunnel is already active');
263
+ });
264
+ });
265
+
266
+ describe('stopTunnelAction - Validation', () => {
267
+ it('should validate that tunnel service exists', async () => {
268
+ const runtimeWithoutService = createMockRuntime({
269
+ getService: mock().mockReturnValue(null),
270
+ });
271
+
272
+ const isValid = await stopTunnelAction.validate(runtimeWithoutService, mockMemory);
273
+
274
+ expect(isValid).toBe(false);
275
+ });
276
+ });
277
+
278
+ describe('stopTunnelAction - Error Handling', () => {
279
+ it('should handle service not available', async () => {
280
+ const runtimeWithoutService = createMockRuntime({
281
+ getService: mock().mockReturnValue(null),
282
+ });
283
+
284
+ const result = await stopTunnelAction.handler(
285
+ runtimeWithoutService,
286
+ mockMemory,
287
+ mockState,
288
+ {},
289
+ mockCallback
290
+ );
291
+
292
+ expect(result.values?.success).toBe(false);
293
+ expect(result.values?.error).toBe('service_unavailable');
294
+ expect(mockCallback.calls.length).toBeGreaterThan(0);
295
+ expect(mockCallback.calls[0][0].text).toContain('Tunnel service is not available');
296
+ });
297
+
298
+ it('should handle stopping when no tunnel is active', async () => {
299
+ mockTunnelService.isActive.mockReturnValue(false);
300
+
301
+ const result = await stopTunnelAction.handler(
302
+ mockRuntime,
303
+ mockMemory,
304
+ mockState,
305
+ {},
306
+ mockCallback
307
+ );
308
+
309
+ expect(result.values?.success).toBe(true);
310
+ expect(result.values?.wasActive).toBe(false);
311
+ expect(mockCallback.calls.length).toBeGreaterThan(0);
312
+ expect(mockCallback.calls[0][0].text).toContain('No tunnel is currently running');
313
+ expect(mockTunnelService.stopTunnel.calls.length).toBe(0);
314
+ });
315
+
316
+ it('should handle stop failure gracefully', async () => {
317
+ // Create a fresh mock service for this test that will fail to stop
318
+ const failingStopService = new MockNgrokService({} as IAgentRuntime);
319
+ failingStopService.isActive.mockReturnValue(true);
320
+ failingStopService.getStatus.mockReturnValue({
321
+ active: true,
322
+ url: 'https://test.ngrok.io',
323
+ port: 8080,
324
+ startedAt: new Date(),
325
+ provider: 'ngrok',
326
+ });
327
+ failingStopService.stopTunnel.mockRejectedValue(new Error('Stop failed'));
328
+
329
+ const failingStopRuntime = createMockRuntime({
330
+ getService: mock().mockImplementation((name: string) => {
331
+ if (name === 'tunnel' || name === 'ngrok-tunnel') {
332
+ return failingStopService;
333
+ }
334
+ return null;
335
+ }),
336
+ });
337
+
338
+ const result = await stopTunnelAction.handler(
339
+ failingStopRuntime,
340
+ mockMemory,
341
+ mockState,
342
+ {},
343
+ mockCallback
344
+ );
345
+
346
+ expect(result.values?.success).toBe(false);
347
+ expect(result.values?.error).toBe('Stop failed');
348
+ expect(mockCallback.calls.length).toBeGreaterThan(0);
349
+ const call = mockCallback.calls[0][0];
350
+ expect(call.text).toContain('Failed to stop ngrok tunnel');
351
+ expect(call.metadata.error).toBe('Stop failed');
352
+ });
353
+ });
354
+
355
+ describe('getTunnelStatusAction - Edge Cases', () => {
356
+ it('should handle service not available', async () => {
357
+ const runtimeWithoutService = createMockRuntime({
358
+ getService: mock().mockReturnValue(null),
359
+ });
360
+
361
+ const result = await getTunnelStatusAction.handler(
362
+ runtimeWithoutService,
363
+ mockMemory,
364
+ mockState,
365
+ {},
366
+ mockCallback
367
+ );
368
+
369
+ expect(result.values?.success).toBe(false);
370
+ expect(result.values?.error).toBe('Tunnel service not found');
371
+ });
372
+
373
+ it('should format uptime correctly', async () => {
374
+ const startTime = new Date();
375
+ startTime.setHours(startTime.getHours() - 2); // 2 hours ago
376
+
377
+ // Create a fresh mock service for this test with active tunnel
378
+ const activeTunnelService = new MockNgrokService({} as IAgentRuntime);
379
+ activeTunnelService.getStatus.mockReturnValue({
380
+ active: true,
381
+ url: 'https://test.ngrok.io',
382
+ port: 8080,
383
+ startedAt: startTime,
384
+ provider: 'ngrok',
385
+ });
386
+
387
+ const activeRuntime = createMockRuntime({
388
+ getService: mock().mockImplementation((name: string) => {
389
+ if (name === 'tunnel' || name === 'ngrok-tunnel') {
390
+ return activeTunnelService;
391
+ }
392
+ return null;
393
+ }),
394
+ });
395
+
396
+ await getTunnelStatusAction.handler(activeRuntime, mockMemory, mockState, {}, mockCallback);
397
+
398
+ expect(mockCallback.calls.length).toBeGreaterThan(0);
399
+ expect(mockCallback.calls[0][0].text).toContain('2 hours');
400
+ });
401
+ });
402
+ });