@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,174 @@
1
+ import { afterAll, beforeAll, beforeEach } from 'bun:test';
2
+ import { spawn } from 'node:child_process';
3
+ import * as path from 'node:path';
4
+ import * as dotenv from 'dotenv';
5
+
6
+ // Load environment variables
7
+ dotenv.config({ path: path.resolve(process.cwd(), '.env') });
8
+
9
+ // Store the original NGROK_DOMAIN to restore later
10
+ const originalNgrokDomain = process.env.NGROK_DOMAIN;
11
+ const originalNgrokAuthToken = process.env.NGROK_AUTH_TOKEN;
12
+
13
+ // Global test state
14
+ const activeNgrokProcesses: Set<number> = new Set();
15
+
16
+ // Kill all ngrok processes before tests start
17
+ async function killAllNgrokProcesses(): Promise<void> {
18
+ return new Promise((resolve) => {
19
+ // Use more specific pattern to only kill actual ngrok processes
20
+ const killProcess = spawn('pkill', ['-x', 'ngrok']);
21
+ killProcess.on('exit', () => {
22
+ // Give it a moment to fully terminate
23
+ setTimeout(resolve, 1000);
24
+ });
25
+ killProcess.on('error', () => {
26
+ // pkill might not exist on all systems, that's OK
27
+ resolve();
28
+ });
29
+ });
30
+ }
31
+
32
+ // Check if any ngrok processes are running
33
+ async function checkNgrokProcesses(): Promise<boolean> {
34
+ return new Promise((resolve) => {
35
+ // Use more specific pattern to only find actual ngrok processes
36
+ const checkProcess = spawn('pgrep', ['-x', 'ngrok']);
37
+ let hasProcesses = false;
38
+
39
+ checkProcess.stdout.on('data', (data) => {
40
+ const pids = data.toString().trim().split('\n').filter(Boolean);
41
+ if (pids.length > 0) {
42
+ hasProcesses = true;
43
+ pids.forEach((pid: string) => {
44
+ const pidNum = parseInt(pid, 10);
45
+ if (!Number.isNaN(pidNum)) {
46
+ activeNgrokProcesses.add(pidNum);
47
+ }
48
+ });
49
+ }
50
+ });
51
+
52
+ checkProcess.on('exit', () => {
53
+ resolve(hasProcesses);
54
+ });
55
+
56
+ checkProcess.on('error', () => {
57
+ resolve(false);
58
+ });
59
+ });
60
+ }
61
+
62
+ // Check if this is a pay-as-you-go account by trying to start without domain
63
+ async function checkIfPayAsYouGo(): Promise<boolean> {
64
+ if (!originalNgrokAuthToken) {
65
+ return false;
66
+ }
67
+
68
+ return new Promise((resolve) => {
69
+ const envWithoutDomain: NodeJS.ProcessEnv = { ...process.env };
70
+ delete envWithoutDomain.NGROK_DOMAIN;
71
+
72
+ const checkProcess = spawn('ngrok', ['http', '8080'], {
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ env: envWithoutDomain,
75
+ });
76
+
77
+ let isPayAsYouGo = false;
78
+
79
+ checkProcess.stderr?.on('data', (data: Buffer) => {
80
+ const message = data.toString();
81
+ if (message.includes('ERR_NGROK_15002') || message.includes('Pay-as-you-go')) {
82
+ isPayAsYouGo = true;
83
+ checkProcess.kill();
84
+ }
85
+ });
86
+
87
+ checkProcess.on('exit', () => {
88
+ resolve(isPayAsYouGo);
89
+ });
90
+
91
+ // Give it a few seconds then kill it
92
+ setTimeout(() => {
93
+ if (!checkProcess.killed) {
94
+ checkProcess.kill();
95
+ }
96
+ }, 3000);
97
+ });
98
+ }
99
+
100
+ beforeAll(async () => {
101
+ // Only run setup for test files, not for the main process
102
+ const isTestFile = process.argv.some((arg) => arg.includes('.test.') || arg.includes('.spec.'));
103
+ if (!isTestFile) {
104
+ return;
105
+ }
106
+
107
+ console.log('\n🧹 Cleaning up any existing ngrok processes...');
108
+ await killAllNgrokProcesses();
109
+
110
+ // Check if this is a pay-as-you-go account
111
+ const isPayAsYouGo = await checkIfPayAsYouGo();
112
+
113
+ if (isPayAsYouGo && originalNgrokDomain) {
114
+ // For pay-as-you-go accounts, we MUST use the domain
115
+ console.log('📌 Pay-as-you-go account detected, using domain:', originalNgrokDomain);
116
+ process.env.NGROK_DOMAIN = originalNgrokDomain;
117
+ } else {
118
+ // For free accounts, we should NOT use a fixed domain to avoid conflicts
119
+ console.log('🆓 Free account detected, using random URLs for tests');
120
+ delete process.env.NGROK_DOMAIN;
121
+ }
122
+
123
+ console.log('✅ Test environment ready\n');
124
+ });
125
+
126
+ beforeEach(async () => {
127
+ // Only run for test files
128
+ const isTestFile = process.argv.some((arg) => arg.includes('.test.') || arg.includes('.spec.'));
129
+ if (!isTestFile) {
130
+ return;
131
+ }
132
+
133
+ // Check for lingering ngrok processes between tests
134
+ const hasProcesses = await checkNgrokProcesses();
135
+ if (hasProcesses) {
136
+ console.log('⚠️ Found lingering ngrok processes, cleaning up...');
137
+ await killAllNgrokProcesses();
138
+ }
139
+
140
+ // Clear the active processes set
141
+ activeNgrokProcesses.clear();
142
+ });
143
+
144
+ afterAll(async () => {
145
+ // Only run for test files
146
+ const isTestFile = process.argv.some((arg) => arg.includes('.test.') || arg.includes('.spec.'));
147
+ if (!isTestFile) {
148
+ return;
149
+ }
150
+
151
+ console.log('\n🧹 Final cleanup...');
152
+
153
+ // Kill any remaining ngrok processes
154
+ await killAllNgrokProcesses();
155
+
156
+ // Restore original environment
157
+ if (originalNgrokDomain) {
158
+ process.env.NGROK_DOMAIN = originalNgrokDomain;
159
+ }
160
+
161
+ console.log('✅ Cleanup complete\n');
162
+ });
163
+
164
+ // Export helper to track ngrok processes
165
+ export function trackNgrokProcess(pid: number): void {
166
+ activeNgrokProcesses.add(pid);
167
+ }
168
+
169
+ export function untrackNgrokProcess(pid: number): void {
170
+ activeNgrokProcesses.delete(pid);
171
+ }
172
+
173
+ // Export the original domain for tests that need it
174
+ export const ORIGINAL_NGROK_DOMAIN = originalNgrokDomain;
@@ -0,0 +1,155 @@
1
+ import type { IAgentRuntime, Memory, Service, ServiceTypeName, State } from '@elizaos/core';
2
+ import { MockNgrokService } from './mocks/NgrokServiceMock';
3
+
4
+ type MockRuntimeOverrides = Partial<IAgentRuntime> & {
5
+ settings?: Record<string, string>;
6
+ services?: Record<string, Service | null | undefined>;
7
+ };
8
+
9
+ // Local mock implementations until core test-utils build issue is resolved
10
+ const createCoreMockRuntime = (overrides: MockRuntimeOverrides = {}) => ({
11
+ agentId: 'test-agent-id',
12
+ character: { name: 'TestAgent', bio: ['Test agent'], ...overrides.character },
13
+ getSetting: overrides.getSetting || (() => null),
14
+ getService: overrides.getService || (() => null),
15
+ useModel: overrides.useModel || (() => Promise.resolve('{}')),
16
+ composeState: overrides.composeState || (() => Promise.resolve({})),
17
+ getMemories: overrides.getMemories || (() => Promise.resolve([])),
18
+ createMemory: overrides.createMemory || (() => Promise.resolve()),
19
+ ...overrides,
20
+ });
21
+
22
+ const createCoreMockMemory = (overrides: Partial<Memory> = {}) => ({
23
+ id: 'test-memory-id',
24
+ entityId: 'test-entity-id',
25
+ roomId: 'test-room-id',
26
+ content: { text: 'test message' },
27
+ createdAt: Date.now(),
28
+ ...overrides,
29
+ });
30
+
31
+ const createCoreMockState = (overrides: Partial<State> = {}) => ({
32
+ text: 'test state',
33
+ ...overrides,
34
+ });
35
+
36
+ /**
37
+ * Creates a mock runtime for testing ngrok plugin
38
+ * Uses the unified mock runtime from core with ngrok-specific overrides
39
+ *
40
+ * @param overrides - Optional overrides for the default mock methods and properties
41
+ * @returns A mock runtime for testing
42
+ */
43
+ export function createMockRuntime(overrides: MockRuntimeOverrides = {}): IAgentRuntime {
44
+ const {
45
+ settings: extraSettings = {},
46
+ services: extraServices = {},
47
+ ...runtimeOverrides
48
+ } = overrides;
49
+
50
+ // Use the unified mock runtime from core with ngrok-specific overrides
51
+ return createCoreMockRuntime({
52
+ character: {
53
+ name: 'NgrokTestAgent',
54
+ bio: ['Test agent for ngrok tunnel functionality'],
55
+ system: 'You are a test agent for ngrok tunnel management',
56
+ messageExamples: [],
57
+ postExamples: [],
58
+ topics: ['ngrok', 'tunneling', 'webhooks'],
59
+ knowledge: [],
60
+ plugins: ['@elizaos/plugin-ngrok'],
61
+ ...runtimeOverrides.character,
62
+ },
63
+
64
+ // Ngrok-specific settings
65
+ getSetting: (key: string): string | boolean | number | null => {
66
+ const settings: Record<string, string> = {
67
+ NGROK_AUTH_TOKEN: process.env.NGROK_AUTH_TOKEN || 'test-ngrok-auth-token',
68
+ NGROK_DOMAIN: process.env.NGROK_DOMAIN || '',
69
+ NGROK_REGION: 'us',
70
+ LOG_LEVEL: 'info',
71
+ ...extraSettings,
72
+ };
73
+ const direct = settings[key];
74
+ if (direct !== undefined) return direct;
75
+ const envVal = process.env[key];
76
+ return envVal === undefined ? null : envVal;
77
+ },
78
+
79
+ // Ngrok-specific services
80
+ getService: <T extends Service>(name: ServiceTypeName | string): T | null => {
81
+ const placeholderRuntime = { agentId: 'mock' } as IAgentRuntime;
82
+ const defaults: Record<string, Service> = {
83
+ 'ngrok-tunnel': new MockNgrokService(placeholderRuntime),
84
+ };
85
+ const merged: Record<string, Service | null | undefined> = {
86
+ ...defaults,
87
+ ...extraServices,
88
+ };
89
+ const resolved = merged[String(name)];
90
+ return (resolved ?? null) as T | null;
91
+ },
92
+
93
+ ...runtimeOverrides,
94
+ }) as IAgentRuntime;
95
+ }
96
+
97
+ /**
98
+ * Creates a mock Memory object for testing
99
+ * Uses the unified mock with ngrok-specific defaults
100
+ */
101
+ export function createMockMemory(overrides: Partial<Memory> = {}): Memory {
102
+ return createCoreMockMemory({
103
+ content: {
104
+ text: 'Ngrok tunnel test message',
105
+ source: 'ngrok-test',
106
+ },
107
+ ...overrides,
108
+ }) as Memory;
109
+ }
110
+
111
+ /**
112
+ * Creates a mock State object for testing
113
+ * Uses the unified mock with ngrok-specific defaults
114
+ */
115
+ export function createMockState(overrides: Partial<State> = {}): State {
116
+ return createCoreMockState({
117
+ values: {
118
+ ngrokTunnelActive: false,
119
+ ngrokUrl: null,
120
+ },
121
+ text: 'Ngrok tunnel context',
122
+ ...overrides,
123
+ }) as State;
124
+ }
125
+
126
+ /**
127
+ * Sets up logger spies for common usage in tests
128
+ */
129
+ type ConsoleMockFactory = (
130
+ implementation: (...args: unknown[]) => void
131
+ ) => (...args: unknown[]) => void;
132
+
133
+ export function setupLoggerSpies(mockFn?: ConsoleMockFactory) {
134
+ const originalConsole = {
135
+ info: console.info,
136
+ error: console.error,
137
+ warn: console.warn,
138
+ debug: console.debug,
139
+ };
140
+
141
+ if (mockFn) {
142
+ console.info = mockFn(() => {});
143
+ console.error = mockFn(() => {});
144
+ console.warn = mockFn(() => {});
145
+ console.debug = mockFn(() => {});
146
+ }
147
+
148
+ // Allow tests to restore originals
149
+ return () => {
150
+ console.info = originalConsole.info;
151
+ console.error = originalConsole.error;
152
+ console.warn = originalConsole.warn;
153
+ console.debug = originalConsole.debug;
154
+ };
155
+ }