@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.
- package/README.md +325 -0
- package/dist/__tests__/NgrokTestSuite.d.ts +6 -0
- package/dist/__tests__/NgrokTestSuite.d.ts.map +1 -0
- package/dist/__tests__/NgrokTestSuite.js +92 -0
- package/dist/__tests__/NgrokTestSuite.js.map +1 -0
- package/dist/actions/get-tunnel-status.d.ts +4 -0
- package/dist/actions/get-tunnel-status.d.ts.map +1 -0
- package/dist/actions/get-tunnel-status.js +186 -0
- package/dist/actions/get-tunnel-status.js.map +1 -0
- package/dist/actions/start-tunnel.d.ts +4 -0
- package/dist/actions/start-tunnel.d.ts.map +1 -0
- package/dist/actions/start-tunnel.js +221 -0
- package/dist/actions/start-tunnel.js.map +1 -0
- package/dist/actions/stop-tunnel.d.ts +4 -0
- package/dist/actions/stop-tunnel.d.ts.map +1 -0
- package/dist/actions/stop-tunnel.js +174 -0
- package/dist/actions/stop-tunnel.js.map +1 -0
- package/dist/environment.d.ts +12 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +68 -0
- package/dist/environment.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/services/NgrokService.d.ts +30 -0
- package/dist/services/NgrokService.d.ts.map +1 -0
- package/dist/services/NgrokService.js +333 -0
- package/dist/services/NgrokService.js.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/NgrokTestSuite.ts +110 -0
- package/src/__tests__/debug-mock.test.ts +15 -0
- package/src/__tests__/e2e/real-ngrok.test.ts +543 -0
- package/src/__tests__/integration/webhook-scenarios.test.ts +463 -0
- package/src/__tests__/mocks/NgrokServiceMock.ts +76 -0
- package/src/__tests__/ngrok-integration.test.ts +521 -0
- package/src/__tests__/test-config.ts +83 -0
- package/src/__tests__/test-helpers.ts +43 -0
- package/src/__tests__/test-setup.ts +174 -0
- package/src/__tests__/test-utils.ts +155 -0
- package/src/__tests__/unit/actions.test.ts +402 -0
- package/src/__tests__/unit/environment.test.ts +352 -0
- package/src/actions/get-tunnel-status.ts +218 -0
- package/src/actions/start-tunnel.ts +255 -0
- package/src/actions/stop-tunnel.ts +203 -0
- package/src/environment.ts +75 -0
- package/src/index.ts +33 -0
- 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
|
+
}
|