autotel-subscribers 4.0.0

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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +669 -0
  3. package/dist/amplitude.cjs +2486 -0
  4. package/dist/amplitude.cjs.map +1 -0
  5. package/dist/amplitude.d.cts +49 -0
  6. package/dist/amplitude.d.ts +49 -0
  7. package/dist/amplitude.js +2463 -0
  8. package/dist/amplitude.js.map +1 -0
  9. package/dist/event-subscriber-base-CnF3V56W.d.cts +182 -0
  10. package/dist/event-subscriber-base-CnF3V56W.d.ts +182 -0
  11. package/dist/factories.cjs +16660 -0
  12. package/dist/factories.cjs.map +1 -0
  13. package/dist/factories.d.cts +304 -0
  14. package/dist/factories.d.ts +304 -0
  15. package/dist/factories.js +16624 -0
  16. package/dist/factories.js.map +1 -0
  17. package/dist/index.cjs +16575 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +179 -0
  20. package/dist/index.d.ts +179 -0
  21. package/dist/index.js +16539 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/middleware.cjs +220 -0
  24. package/dist/middleware.cjs.map +1 -0
  25. package/dist/middleware.d.cts +227 -0
  26. package/dist/middleware.d.ts +227 -0
  27. package/dist/middleware.js +208 -0
  28. package/dist/middleware.js.map +1 -0
  29. package/dist/mixpanel.cjs +2940 -0
  30. package/dist/mixpanel.cjs.map +1 -0
  31. package/dist/mixpanel.d.cts +47 -0
  32. package/dist/mixpanel.d.ts +47 -0
  33. package/dist/mixpanel.js +2932 -0
  34. package/dist/mixpanel.js.map +1 -0
  35. package/dist/posthog.cjs +4115 -0
  36. package/dist/posthog.cjs.map +1 -0
  37. package/dist/posthog.d.cts +299 -0
  38. package/dist/posthog.d.ts +299 -0
  39. package/dist/posthog.js +4113 -0
  40. package/dist/posthog.js.map +1 -0
  41. package/dist/segment.cjs +6822 -0
  42. package/dist/segment.cjs.map +1 -0
  43. package/dist/segment.d.cts +49 -0
  44. package/dist/segment.d.ts +49 -0
  45. package/dist/segment.js +6794 -0
  46. package/dist/segment.js.map +1 -0
  47. package/dist/slack.cjs +368 -0
  48. package/dist/slack.cjs.map +1 -0
  49. package/dist/slack.d.cts +126 -0
  50. package/dist/slack.d.ts +126 -0
  51. package/dist/slack.js +366 -0
  52. package/dist/slack.js.map +1 -0
  53. package/dist/webhook.cjs +100 -0
  54. package/dist/webhook.cjs.map +1 -0
  55. package/dist/webhook.d.cts +53 -0
  56. package/dist/webhook.d.ts +53 -0
  57. package/dist/webhook.js +98 -0
  58. package/dist/webhook.js.map +1 -0
  59. package/examples/quickstart-custom-subscriber.ts +144 -0
  60. package/examples/subscriber-bigquery.ts +219 -0
  61. package/examples/subscriber-databricks.ts +280 -0
  62. package/examples/subscriber-kafka.ts +326 -0
  63. package/examples/subscriber-kinesis.ts +307 -0
  64. package/examples/subscriber-posthog.ts +421 -0
  65. package/examples/subscriber-pubsub.ts +336 -0
  66. package/examples/subscriber-snowflake.ts +232 -0
  67. package/package.json +141 -0
  68. package/src/amplitude.test.ts +231 -0
  69. package/src/amplitude.ts +148 -0
  70. package/src/event-subscriber-base.ts +325 -0
  71. package/src/factories.ts +197 -0
  72. package/src/index.ts +50 -0
  73. package/src/middleware.ts +489 -0
  74. package/src/mixpanel.test.ts +194 -0
  75. package/src/mixpanel.ts +134 -0
  76. package/src/mock-event-subscriber.ts +333 -0
  77. package/src/posthog.test.ts +629 -0
  78. package/src/posthog.ts +530 -0
  79. package/src/segment.test.ts +228 -0
  80. package/src/segment.ts +148 -0
  81. package/src/slack.ts +383 -0
  82. package/src/streaming-event-subscriber.ts +323 -0
  83. package/src/testing/index.ts +37 -0
  84. package/src/testing/mock-webhook-server.ts +242 -0
  85. package/src/testing/subscriber-test-harness.ts +365 -0
  86. package/src/webhook.test.ts +264 -0
  87. package/src/webhook.ts +158 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Testing utilities for subscriber authors.
3
+ *
4
+ * Use these to validate your custom subscribers work correctly.
5
+ *
6
+ * @example Test your subscriber
7
+ * ```typescript
8
+ * import { SubscriberTestHarness } from 'autotel-subscribers/testing';
9
+ *
10
+ * const harness = new SubscriberTestHarness(new MySubscriber());
11
+ * const results = await harness.runAll();
12
+ * SubscriberTestHarness.printResults(results);
13
+ * ```
14
+ *
15
+ * @example Test webhook subscriber
16
+ * ```typescript
17
+ * import { MockWebhookServer } from 'autotel-subscribers/testing';
18
+ *
19
+ * const server = new MockWebhookServer();
20
+ * const url = await server.start();
21
+ * const subscriber = new WebhookSubscriber({ url });
22
+ *
23
+ * await subscriber.trackEvent('test', {});
24
+ * expect(server.getRequestCount()).toBe(1);
25
+ *
26
+ * await server.stop();
27
+ * ```
28
+ */
29
+
30
+ export { SubscriberTestHarness } from './subscriber-test-harness';
31
+ export type { TestResult, TestSuiteResult } from './subscriber-test-harness';
32
+
33
+ export { MockWebhookServer } from './mock-webhook-server';
34
+ export type { RecordedRequest, MockServerOptions } from './mock-webhook-server';
35
+
36
+ // Re-export MockEventSubscriber for convenience
37
+ export { MockEventSubscriber } from '../mock-event-subscriber';
@@ -0,0 +1,242 @@
1
+ /**
2
+ * MockWebhookServer - In-Memory HTTP Server for Testing
3
+ *
4
+ * Perfect for testing webhook subscribers without real HTTP calls.
5
+ * Records all requests for easy assertions in tests.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { MockWebhookServer } from 'autotel-subscribers/testing';
10
+ *
11
+ * const server = new MockWebhookServer();
12
+ * const url = await server.start();
13
+ *
14
+ * // Test your webhook subscriber
15
+ * const subscriber = new WebhookSubscriber({ url });
16
+ * await subscriber.trackEvent('test.event', { foo: 'bar' });
17
+ *
18
+ * // Assert
19
+ * const requests = server.getRequests();
20
+ * expect(requests).toHaveLength(1);
21
+ * expect(requests[0].body.event).toBe('test.event');
22
+ *
23
+ * await server.stop();
24
+ * ```
25
+ */
26
+
27
+ import * as http from 'node:http';
28
+ import type { AddressInfo } from 'node:net';
29
+
30
+ export interface RecordedRequest {
31
+ method: string;
32
+ path: string;
33
+ headers: http.IncomingHttpHeaders;
34
+ body: any;
35
+ timestamp: number;
36
+ }
37
+
38
+ export interface MockServerOptions {
39
+ /** Port to listen on (0 = random port) */
40
+ port?: number;
41
+ /** Response status code (default: 200) */
42
+ responseStatus?: number;
43
+ /** Response delay in ms (default: 0) */
44
+ responseDelay?: number;
45
+ /** Response body (default: 'OK') */
46
+ responseBody?: string;
47
+ /** Log requests to console (default: false) */
48
+ logRequests?: boolean;
49
+ }
50
+
51
+ /**
52
+ * In-memory HTTP server for testing webhook subscribers.
53
+ *
54
+ * Records all incoming requests so you can assert on them in tests.
55
+ */
56
+ export class MockWebhookServer {
57
+ private server?: http.Server;
58
+ private requests: RecordedRequest[] = [];
59
+ private options: Required<MockServerOptions>;
60
+
61
+ constructor(options: MockServerOptions = {}) {
62
+ this.options = {
63
+ port: options.port ?? 0,
64
+ responseStatus: options.responseStatus ?? 200,
65
+ responseDelay: options.responseDelay ?? 0,
66
+ responseBody: options.responseBody ?? 'OK',
67
+ logRequests: options.logRequests ?? false,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Start the mock server and return its URL.
73
+ *
74
+ * @returns Promise resolving to the server URL (e.g., "http://localhost:3000")
75
+ */
76
+ async start(): Promise<string> {
77
+ if (this.server) {
78
+ throw new Error('Server already started');
79
+ }
80
+
81
+ this.server = http.createServer((req, res) => {
82
+ let body = '';
83
+
84
+ req.on('data', (chunk) => {
85
+ body += chunk.toString();
86
+ });
87
+
88
+ req.on('end', () => {
89
+ const parsedBody = this.parseBody(body, req.headers['content-type']);
90
+
91
+ const request: RecordedRequest = {
92
+ method: req.method || 'GET',
93
+ path: req.url || '/',
94
+ headers: req.headers,
95
+ body: parsedBody,
96
+ timestamp: Date.now(),
97
+ };
98
+
99
+ this.requests.push(request);
100
+
101
+ if (this.options.logRequests) {
102
+ console.log('[MockWebhookServer]', request.method, request.path, parsedBody);
103
+ }
104
+
105
+ // Simulate response delay
106
+ setTimeout(() => {
107
+ res.writeHead(this.options.responseStatus, {
108
+ 'Content-Type': 'text/plain',
109
+ });
110
+ res.end(this.options.responseBody);
111
+ }, this.options.responseDelay);
112
+ });
113
+ });
114
+
115
+ await new Promise<void>((resolve) => {
116
+ this.server!.listen(this.options.port, () => {
117
+ resolve();
118
+ });
119
+ });
120
+
121
+ const address = this.server.address() as AddressInfo;
122
+ return `http://localhost:${address.port}`;
123
+ }
124
+
125
+ /**
126
+ * Stop the mock server.
127
+ */
128
+ async stop(): Promise<void> {
129
+ if (!this.server) {
130
+ return;
131
+ }
132
+
133
+ await new Promise<void>((resolve, reject) => {
134
+ this.server!.close((err) => {
135
+ if (err) {
136
+ reject(err);
137
+ } else {
138
+ resolve();
139
+ }
140
+ });
141
+ });
142
+
143
+ this.server = undefined;
144
+ }
145
+
146
+ /**
147
+ * Get all recorded requests.
148
+ */
149
+ getRequests(): RecordedRequest[] {
150
+ return [...this.requests];
151
+ }
152
+
153
+ /**
154
+ * Get requests matching a filter.
155
+ */
156
+ getRequestsWhere(filter: Partial<RecordedRequest>): RecordedRequest[] {
157
+ return this.requests.filter((req) => {
158
+ return Object.entries(filter).every(([key, value]) => {
159
+ return (req as any)[key] === value;
160
+ });
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Get the last recorded request.
166
+ */
167
+ getLastRequest(): RecordedRequest | undefined {
168
+ return this.requests.at(-1);
169
+ }
170
+
171
+ /**
172
+ * Get the first recorded request.
173
+ */
174
+ getFirstRequest(): RecordedRequest | undefined {
175
+ return this.requests[0];
176
+ }
177
+
178
+ /**
179
+ * Clear all recorded requests.
180
+ */
181
+ reset(): void {
182
+ this.requests = [];
183
+ }
184
+
185
+ /**
186
+ * Get the number of recorded requests.
187
+ */
188
+ getRequestCount(): number {
189
+ return this.requests.length;
190
+ }
191
+
192
+ /**
193
+ * Wait for a specific number of requests.
194
+ *
195
+ * Useful when testing async subscribers.
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * await subscriber.trackEvent('event1', {});
200
+ * await subscriber.trackEvent('event2', {});
201
+ * await server.waitForRequests(2, 1000); // Wait max 1 second
202
+ * ```
203
+ */
204
+ async waitForRequests(count: number, timeoutMs: number = 5000): Promise<void> {
205
+ const startTime = Date.now();
206
+
207
+ while (this.requests.length < count) {
208
+ if (Date.now() - startTime > timeoutMs) {
209
+ throw new Error(
210
+ `Timeout waiting for ${count} requests (got ${this.requests.length})`
211
+ );
212
+ }
213
+
214
+ // Wait 10ms before checking again
215
+ await new Promise((resolve) => setTimeout(resolve, 10));
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Parse request body based on Content-Type.
221
+ */
222
+ private parseBody(body: string, contentType?: string): any {
223
+ if (!body) {
224
+ return null;
225
+ }
226
+
227
+ try {
228
+ if (contentType?.includes('application/json')) {
229
+ return JSON.parse(body);
230
+ }
231
+
232
+ if (contentType?.includes('application/x-www-form-urlencoded')) {
233
+ return Object.fromEntries(new URLSearchParams(body));
234
+ }
235
+
236
+ return body;
237
+ } catch {
238
+ // If parsing fails, return raw string
239
+ return body;
240
+ }
241
+ }
242
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * SubscriberTestHarness - Validate Your Custom Subscriber
3
+ *
4
+ * Use this to test that your custom subscriber implements the EventSubscriber
5
+ * interface correctly. It runs a suite of tests covering:
6
+ * - Basic tracking (all 4 methods)
7
+ * - Concurrent requests
8
+ * - Error handling
9
+ * - Graceful shutdown
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { SubscriberTestHarness } from 'autotel-subscribers/testing';
14
+ * import { MyCustomSubscriber } from './my-adapter';
15
+ *
16
+ * const harness = new SubscriberTestHarness(new MyCustomSubscriber());
17
+ * const results = await harness.runAll();
18
+ *
19
+ * if (results.passed) {
20
+ * console.log('āœ… All tests passed!');
21
+ * } else {
22
+ * console.error('āŒ Tests failed:', results.failures);
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ import type {
28
+ EventSubscriber,
29
+ FunnelStatus,
30
+ OutcomeStatus,
31
+ } from '../event-subscriber-base';
32
+
33
+ export interface TestResult {
34
+ name: string;
35
+ passed: boolean;
36
+ duration: number;
37
+ error?: Error;
38
+ details?: string;
39
+ }
40
+
41
+ export interface TestSuiteResult {
42
+ passed: boolean;
43
+ total: number;
44
+ passed_count: number;
45
+ failed_count: number;
46
+ duration: number;
47
+ results: TestResult[];
48
+ failures: TestResult[];
49
+ }
50
+
51
+ /**
52
+ * Test harness for validating custom subscribers.
53
+ *
54
+ * Runs a comprehensive suite of tests to ensure your subscriber:
55
+ * 1. Implements all required methods
56
+ * 2. Handles concurrent requests
57
+ * 3. Deals with errors gracefully
58
+ * 4. Shuts down cleanly
59
+ */
60
+ export class SubscriberTestHarness {
61
+ constructor(private subscriber: EventSubscriber) {}
62
+
63
+ /**
64
+ * Run all tests and return a comprehensive report.
65
+ */
66
+ async runAll(): Promise<TestSuiteResult> {
67
+ const startTime = performance.now();
68
+ const results: TestResult[] = [];
69
+
70
+ // Run all test methods
71
+ const testResults = await Promise.all([
72
+ this.testBasicTracking(),
73
+ this.testFunnelTracking(),
74
+ this.testOutcomeTracking(),
75
+ this.testValueTracking(),
76
+ this.testConcurrency(),
77
+ this.testErrorHandling(),
78
+ this.testShutdown(),
79
+ ]);
80
+ results.push(...testResults);
81
+
82
+ const duration = performance.now() - startTime;
83
+ const passed_count = results.filter((r) => r.passed).length;
84
+ const failed_count = results.filter((r) => !r.passed).length;
85
+ const failures = results.filter((r) => !r.passed);
86
+
87
+ return {
88
+ passed: failed_count === 0,
89
+ total: results.length,
90
+ passed_count,
91
+ failed_count,
92
+ duration,
93
+ results,
94
+ failures,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Test basic event tracking.
100
+ */
101
+ async testBasicTracking(): Promise<TestResult> {
102
+ const startTime = performance.now();
103
+
104
+ try {
105
+ await this.subscriber.trackEvent('test.event', {
106
+ userId: 'test-user',
107
+ testAttribute: 'test-value',
108
+ });
109
+
110
+ return {
111
+ name: 'Basic Event Tracking',
112
+ passed: true,
113
+ duration: performance.now() - startTime,
114
+ details: 'Successfully tracked event',
115
+ };
116
+ } catch (error) {
117
+ return {
118
+ name: 'Basic Event Tracking',
119
+ passed: false,
120
+ duration: performance.now() - startTime,
121
+ error: error as Error,
122
+ };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Test funnel step tracking.
128
+ */
129
+ async testFunnelTracking(): Promise<TestResult> {
130
+ const startTime = performance.now();
131
+
132
+ try {
133
+ await this.subscriber.trackFunnelStep('test_funnel', 'started' as FunnelStatus, {
134
+ cartValue: 99.99,
135
+ });
136
+
137
+ await this.subscriber.trackFunnelStep('test_funnel', 'completed' as FunnelStatus, {
138
+ cartValue: 99.99,
139
+ });
140
+
141
+ return {
142
+ name: 'Funnel Tracking',
143
+ passed: true,
144
+ duration: performance.now() - startTime,
145
+ details: 'Successfully tracked funnel steps',
146
+ };
147
+ } catch (error) {
148
+ return {
149
+ name: 'Funnel Tracking',
150
+ passed: false,
151
+ duration: performance.now() - startTime,
152
+ error: error as Error,
153
+ };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Test outcome tracking.
159
+ */
160
+ async testOutcomeTracking(): Promise<TestResult> {
161
+ const startTime = performance.now();
162
+
163
+ try {
164
+ await this.subscriber.trackOutcome('test_operation', 'success' as OutcomeStatus, {
165
+ duration: 100,
166
+ });
167
+
168
+ await this.subscriber.trackOutcome('test_operation', 'failure' as OutcomeStatus, {
169
+ error: 'Test error',
170
+ });
171
+
172
+ return {
173
+ name: 'Outcome Tracking',
174
+ passed: true,
175
+ duration: performance.now() - startTime,
176
+ details: 'Successfully tracked outcomes',
177
+ };
178
+ } catch (error) {
179
+ return {
180
+ name: 'Outcome Tracking',
181
+ passed: false,
182
+ duration: performance.now() - startTime,
183
+ error: error as Error,
184
+ };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Test value tracking.
190
+ */
191
+ async testValueTracking(): Promise<TestResult> {
192
+ const startTime = performance.now();
193
+
194
+ try {
195
+ await this.subscriber.trackValue('test_metric', 42, {
196
+ unit: 'ms',
197
+ });
198
+
199
+ await this.subscriber.trackValue('revenue', 99.99, {
200
+ currency: 'USD',
201
+ });
202
+
203
+ return {
204
+ name: 'Value Tracking',
205
+ passed: true,
206
+ duration: performance.now() - startTime,
207
+ details: 'Successfully tracked values',
208
+ };
209
+ } catch (error) {
210
+ return {
211
+ name: 'Value Tracking',
212
+ passed: false,
213
+ duration: performance.now() - startTime,
214
+ error: error as Error,
215
+ };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Test concurrent requests (sends 50 events simultaneously).
221
+ */
222
+ async testConcurrency(): Promise<TestResult> {
223
+ const startTime = performance.now();
224
+
225
+ try {
226
+ const promises = Array.from({ length: 50 }, (_, i) =>
227
+ this.subscriber.trackEvent(`concurrent_event_${i}`, {
228
+ index: i,
229
+ timestamp: Date.now(),
230
+ })
231
+ );
232
+
233
+ await Promise.all(promises);
234
+
235
+ return {
236
+ name: 'Concurrency',
237
+ passed: true,
238
+ duration: performance.now() - startTime,
239
+ details: 'Successfully handled 50 concurrent requests',
240
+ };
241
+ } catch (error) {
242
+ return {
243
+ name: 'Concurrency',
244
+ passed: false,
245
+ duration: performance.now() - startTime,
246
+ error: error as Error,
247
+ };
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Test error handling (passes invalid data).
253
+ */
254
+ async testErrorHandling(): Promise<TestResult> {
255
+ const startTime = performance.now();
256
+
257
+ try {
258
+ // Test with empty event name
259
+ await this.subscriber.trackEvent('', {});
260
+
261
+ // Test with undefined attributes
262
+ await this.subscriber.trackEvent('test');
263
+
264
+ // Test with null-ish values
265
+ await this.subscriber.trackValue('test', 0, {});
266
+
267
+ return {
268
+ name: 'Error Handling',
269
+ passed: true,
270
+ duration: performance.now() - startTime,
271
+ details: 'Handled edge cases gracefully',
272
+ };
273
+ } catch {
274
+ // Some subscribers might throw on invalid input - that's OK
275
+ return {
276
+ name: 'Error Handling',
277
+ passed: true,
278
+ duration: performance.now() - startTime,
279
+ details: 'Subscriber throws on invalid input (acceptable)',
280
+ };
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Test graceful shutdown.
286
+ */
287
+ async testShutdown(): Promise<TestResult> {
288
+ const startTime = performance.now();
289
+
290
+ try {
291
+ // Start some long-running operations
292
+ const promises = [
293
+ this.subscriber.trackEvent('shutdown_test_1', {}),
294
+ this.subscriber.trackEvent('shutdown_test_2', {}),
295
+ this.subscriber.trackEvent('shutdown_test_3', {}),
296
+ ];
297
+
298
+ // Call shutdown
299
+ await this.subscriber.shutdown?.();
300
+
301
+ // Wait for operations to complete
302
+ const results = await Promise.allSettled(promises);
303
+
304
+ const allSettled = results.every(
305
+ (r) => r.status === 'fulfilled' || r.status === 'rejected'
306
+ );
307
+
308
+ if (!allSettled) {
309
+ throw new Error('Some promises never settled');
310
+ }
311
+
312
+ return {
313
+ name: 'Graceful Shutdown',
314
+ passed: true,
315
+ duration: performance.now() - startTime,
316
+ details: 'Shutdown completed, all requests settled',
317
+ };
318
+ } catch (error) {
319
+ return {
320
+ name: 'Graceful Shutdown',
321
+ passed: false,
322
+ duration: performance.now() - startTime,
323
+ error: error as Error,
324
+ };
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Pretty-print test results to console.
330
+ */
331
+ static printResults(results: TestSuiteResult): void {
332
+ console.log('\n' + '='.repeat(60));
333
+ console.log('šŸ“Š Subscriber Test Results');
334
+ console.log('='.repeat(60));
335
+ console.log(`\nTotal Tests: ${results.total}`);
336
+ console.log(`āœ… Passed: ${results.passed_count}`);
337
+ console.log(`āŒ Failed: ${results.failed_count}`);
338
+ console.log(`ā±ļø Duration: ${results.duration.toFixed(2)}ms`);
339
+ console.log('\n' + '-'.repeat(60));
340
+
341
+ for (const result of results.results) {
342
+ const icon = result.passed ? 'āœ…' : 'āŒ';
343
+ const duration = result.duration.toFixed(2);
344
+ console.log(`${icon} ${result.name} (${duration}ms)`);
345
+
346
+ if (result.details) {
347
+ console.log(` ${result.details}`);
348
+ }
349
+
350
+ if (result.error) {
351
+ console.log(` Error: ${result.error.message}`);
352
+ }
353
+ }
354
+
355
+ console.log('='.repeat(60));
356
+
357
+ if (results.passed) {
358
+ console.log('\nšŸŽ‰ All tests passed! Your subscriber is ready to use.');
359
+ } else {
360
+ console.log('\nāš ļø Some tests failed. Review the errors above.');
361
+ }
362
+
363
+ console.log('\n');
364
+ }
365
+ }