audit follow-up: reduce warning hotspots in automation and gateway tests
This commit is contained in:
+16
-12
@@ -3,6 +3,10 @@ import { CronScheduler } from './cron.js';
|
||||
import type { CronJobConfig } from '../config/schema.js';
|
||||
import type { InboundMessage } from '../channels/types.js';
|
||||
|
||||
function asCronChannelRegistry(value: unknown): ConstructorParameters<typeof CronScheduler>[1] {
|
||||
return value as ConstructorParameters<typeof CronScheduler>[1];
|
||||
}
|
||||
|
||||
function makeCronJob(overrides?: Partial<CronJobConfig>): CronJobConfig {
|
||||
return {
|
||||
name: 'test-job',
|
||||
@@ -31,19 +35,19 @@ describe('CronScheduler', () => {
|
||||
});
|
||||
|
||||
it('implements ChannelAdapter interface', () => {
|
||||
scheduler = new CronScheduler([], mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler([], asCronChannelRegistry(mockChannelRegistry));
|
||||
expect(scheduler.name).toBe('cron');
|
||||
expect(scheduler.status).toBe('disconnected');
|
||||
});
|
||||
|
||||
it('status changes to connected after connect()', async () => {
|
||||
scheduler = new CronScheduler([], mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler([], asCronChannelRegistry(mockChannelRegistry));
|
||||
await scheduler.connect();
|
||||
expect(scheduler.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('status changes to disconnected after disconnect()', async () => {
|
||||
scheduler = new CronScheduler([], mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler([], asCronChannelRegistry(mockChannelRegistry));
|
||||
await scheduler.connect();
|
||||
await scheduler.disconnect();
|
||||
expect(scheduler.status).toBe('disconnected');
|
||||
@@ -51,7 +55,7 @@ describe('CronScheduler', () => {
|
||||
|
||||
it('skips disabled jobs', async () => {
|
||||
const jobs = [makeCronJob({ enabled: false })];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
scheduler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -63,7 +67,7 @@ describe('CronScheduler', () => {
|
||||
|
||||
it('fires a message when triggerJob is called', async () => {
|
||||
const jobs = [makeCronJob()];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
scheduler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -80,7 +84,7 @@ describe('CronScheduler', () => {
|
||||
|
||||
it('uses isolated sender IDs when delivery mode is isolated_job', async () => {
|
||||
const jobs = [makeCronJob()];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any, 'isolated_job');
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry), 'isolated_job');
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
scheduler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -100,7 +104,7 @@ describe('CronScheduler', () => {
|
||||
mockChannelRegistry.get.mockReturnValue(mockOutputAdapter);
|
||||
|
||||
const jobs = [makeCronJob()];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
await scheduler.connect();
|
||||
|
||||
await scheduler.send('test-job', { text: 'Agent response' });
|
||||
@@ -114,7 +118,7 @@ describe('CronScheduler', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const jobs = [makeCronJob()];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
await scheduler.connect();
|
||||
|
||||
await scheduler.send('test-job', { text: 'Agent response' });
|
||||
@@ -127,7 +131,7 @@ describe('CronScheduler', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const jobs = [makeCronJob()];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
await scheduler.connect();
|
||||
|
||||
await scheduler.send('nonexistent-job', { text: 'response' });
|
||||
@@ -138,7 +142,7 @@ describe('CronScheduler', () => {
|
||||
|
||||
it('triggerJob includes model_tier in metadata when configured', () => {
|
||||
const jobs = [makeCronJob({ model_tier: 'fast' })];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
scheduler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -151,7 +155,7 @@ describe('CronScheduler', () => {
|
||||
|
||||
it('triggerJob metadata.modelTier is undefined when not configured', () => {
|
||||
const jobs = [makeCronJob()];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
scheduler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -167,7 +171,7 @@ describe('CronScheduler', () => {
|
||||
makeCronJob({ name: 'job-a' }),
|
||||
makeCronJob({ name: 'job-b', enabled: false }),
|
||||
];
|
||||
scheduler = new CronScheduler(jobs, mockChannelRegistry as any);
|
||||
scheduler = new CronScheduler(jobs, asCronChannelRegistry(mockChannelRegistry));
|
||||
|
||||
const names = scheduler.getJobNames();
|
||||
expect(names).toEqual(['job-a', 'job-b']);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { HeartbeatMonitor, parseInterval } from './heartbeat.js';
|
||||
import type { HeartbeatDeps } from './heartbeat.js';
|
||||
import type { HeartbeatConfig } from '../config/schema.js';
|
||||
import type { ChannelAdapter } from '../channels/types.js';
|
||||
|
||||
function makeConfig(overrides?: Partial<HeartbeatConfig>): HeartbeatConfig {
|
||||
return {
|
||||
@@ -21,8 +22,8 @@ function makeDeps(overrides?: Partial<HeartbeatDeps>): HeartbeatDeps {
|
||||
modelRouter: { getTier: () => 'default' },
|
||||
channelLister: {
|
||||
list: () => [
|
||||
{ name: 'telegram', status: 'connected' } as any,
|
||||
{ name: 'webchat', status: 'connected' } as any,
|
||||
makeChannelAdapter('telegram', 'connected'),
|
||||
makeChannelAdapter('webchat', 'connected'),
|
||||
],
|
||||
},
|
||||
memoryDir: '/tmp/flynn-test-memory',
|
||||
@@ -32,6 +33,17 @@ function makeDeps(overrides?: Partial<HeartbeatDeps>): HeartbeatDeps {
|
||||
};
|
||||
}
|
||||
|
||||
function makeChannelAdapter(name: string, status: ChannelAdapter['status']): ChannelAdapter {
|
||||
return {
|
||||
name,
|
||||
status,
|
||||
connect: async () => {},
|
||||
disconnect: async () => {},
|
||||
send: async () => {},
|
||||
onMessage: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
describe('parseInterval', () => {
|
||||
it('parses seconds', () => {
|
||||
expect(parseInterval('60s')).toBe(60000);
|
||||
@@ -150,8 +162,8 @@ describe('HeartbeatMonitor', () => {
|
||||
|
||||
const lastResult = monitor.getLastResult();
|
||||
expect(lastResult).toBeDefined();
|
||||
expect(lastResult!.checks).toHaveLength(1);
|
||||
expect(lastResult!.timestamp).toBeGreaterThan(0);
|
||||
expect(lastResult?.checks).toHaveLength(1);
|
||||
expect(lastResult?.timestamp ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('notification sent after failure_threshold consecutive failures', async () => {
|
||||
@@ -294,7 +306,8 @@ describe('HeartbeatMonitor', () => {
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'model')!;
|
||||
const check = result.checks.find((c) => c.name === 'model');
|
||||
if (!check) {throw new Error('Expected model check result');}
|
||||
expect(check.healthy).toBe(true);
|
||||
expect(check.message).toContain('fast');
|
||||
});
|
||||
@@ -307,7 +320,8 @@ describe('HeartbeatMonitor', () => {
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'model')!;
|
||||
const check = result.checks.find((c) => c.name === 'model');
|
||||
if (!check) {throw new Error('Expected model check result');}
|
||||
expect(check.healthy).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -318,15 +332,16 @@ describe('HeartbeatMonitor', () => {
|
||||
config: makeConfig({ checks: ['channels'] }),
|
||||
channelLister: {
|
||||
list: () => [
|
||||
{ name: 'telegram', status: 'connected' } as any,
|
||||
{ name: 'webchat', status: 'disconnected' } as any,
|
||||
makeChannelAdapter('telegram', 'connected'),
|
||||
makeChannelAdapter('webchat', 'disconnected'),
|
||||
],
|
||||
},
|
||||
});
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'channels')!;
|
||||
const check = result.checks.find((c) => c.name === 'channels');
|
||||
if (!check) {throw new Error('Expected channels check result');}
|
||||
expect(check.healthy).toBe(true);
|
||||
expect(check.message).toContain('1/2 connected');
|
||||
expect(check.message).toContain('webchat');
|
||||
@@ -337,14 +352,15 @@ describe('HeartbeatMonitor', () => {
|
||||
config: makeConfig({ checks: ['channels'] }),
|
||||
channelLister: {
|
||||
list: () => [
|
||||
{ name: 'telegram', status: 'disconnected' } as any,
|
||||
makeChannelAdapter('telegram', 'disconnected'),
|
||||
],
|
||||
},
|
||||
});
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'channels')!;
|
||||
const check = result.checks.find((c) => c.name === 'channels');
|
||||
if (!check) {throw new Error('Expected channels check result');}
|
||||
expect(check.healthy).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -358,7 +374,8 @@ describe('HeartbeatMonitor', () => {
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'memory')!;
|
||||
const check = result.checks.find((c) => c.name === 'memory');
|
||||
if (!check) {throw new Error('Expected memory check result');}
|
||||
expect(check.healthy).toBe(true);
|
||||
expect(check.message).toContain('disabled');
|
||||
});
|
||||
@@ -371,7 +388,8 @@ describe('HeartbeatMonitor', () => {
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'memory')!;
|
||||
const check = result.checks.find((c) => c.name === 'memory');
|
||||
if (!check) {throw new Error('Expected memory check result');}
|
||||
expect(check.healthy).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -385,7 +403,8 @@ describe('HeartbeatMonitor', () => {
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'disk')!;
|
||||
const check = result.checks.find((c) => c.name === 'disk');
|
||||
if (!check) {throw new Error('Expected disk check result');}
|
||||
expect(check.healthy).toBe(true);
|
||||
expect(check.message).toContain('MB available');
|
||||
});
|
||||
@@ -398,7 +417,8 @@ describe('HeartbeatMonitor', () => {
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'disk')!;
|
||||
const check = result.checks.find((c) => c.name === 'disk');
|
||||
if (!check) {throw new Error('Expected disk check result');}
|
||||
expect(check.healthy).toBe(false);
|
||||
expect(check.message).toContain('Low disk space');
|
||||
});
|
||||
@@ -411,7 +431,8 @@ describe('HeartbeatMonitor', () => {
|
||||
monitor = new HeartbeatMonitor(deps);
|
||||
|
||||
const result = await monitor.runChecks();
|
||||
const check = result.checks.find((c) => c.name === 'disk')!;
|
||||
const check = result.checks.find((c) => c.name === 'disk');
|
||||
if (!check) {throw new Error('Expected disk check result');}
|
||||
expect(check.healthy).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,10 @@ import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { createHmac } from 'crypto';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
function asWebhookChannelLookup(value: unknown): ConstructorParameters<typeof WebhookHandler>[1] {
|
||||
return value as ConstructorParameters<typeof WebhookHandler>[1];
|
||||
}
|
||||
|
||||
function makeWebhook(overrides?: Partial<WebhookConfig>): WebhookConfig {
|
||||
return {
|
||||
name: 'test-hook',
|
||||
@@ -18,8 +22,8 @@ function makeWebhook(overrides?: Partial<WebhookConfig>): WebhookConfig {
|
||||
|
||||
/** Create a mock IncomingMessage that emits the given body. */
|
||||
function mockRequest(body: string, headers: Record<string, string> = {}): IncomingMessage {
|
||||
const emitter = new EventEmitter();
|
||||
(emitter as any).headers = headers;
|
||||
const emitter = new EventEmitter() as EventEmitter & Partial<IncomingMessage>;
|
||||
emitter.headers = headers;
|
||||
// Simulate data arriving next tick
|
||||
process.nextTick(() => {
|
||||
emitter.emit('data', Buffer.from(body));
|
||||
@@ -30,7 +34,7 @@ function mockRequest(body: string, headers: Record<string, string> = {}): Incomi
|
||||
|
||||
/** Create a mock ServerResponse that captures writeHead and end calls. */
|
||||
function mockResponse(): ServerResponse & { statusCode_: number; body_: string; headers_: Record<string, string> } {
|
||||
const res: any = {
|
||||
const res = {
|
||||
statusCode_: 0,
|
||||
body_: '',
|
||||
headers_: {},
|
||||
@@ -44,7 +48,7 @@ function mockResponse(): ServerResponse & { statusCode_: number; body_: string;
|
||||
return res;
|
||||
},
|
||||
};
|
||||
return res;
|
||||
return res as ServerResponse & { statusCode_: number; body_: string; headers_: Record<string, string> };
|
||||
}
|
||||
|
||||
describe('WebhookHandler', () => {
|
||||
@@ -64,19 +68,19 @@ describe('WebhookHandler', () => {
|
||||
});
|
||||
|
||||
it('implements ChannelAdapter interface', () => {
|
||||
handler = new WebhookHandler([], mockChannelRegistry as any);
|
||||
handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry));
|
||||
expect(handler.name).toBe('webhook');
|
||||
expect(handler.status).toBe('disconnected');
|
||||
});
|
||||
|
||||
it('status changes to connected after connect()', async () => {
|
||||
handler = new WebhookHandler([], mockChannelRegistry as any);
|
||||
handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry));
|
||||
await handler.connect();
|
||||
expect(handler.status).toBe('connected');
|
||||
});
|
||||
|
||||
it('status changes to disconnected after disconnect()', async () => {
|
||||
handler = new WebhookHandler([], mockChannelRegistry as any);
|
||||
handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry));
|
||||
await handler.connect();
|
||||
await handler.disconnect();
|
||||
expect(handler.status).toBe('disconnected');
|
||||
@@ -87,7 +91,7 @@ describe('WebhookHandler', () => {
|
||||
makeWebhook({ name: 'hook-a' }),
|
||||
makeWebhook({ name: 'hook-b', enabled: false }),
|
||||
];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
|
||||
const names = handler.getWebhookNames();
|
||||
expect(names).toEqual(['hook-a', 'hook-b']);
|
||||
@@ -95,7 +99,7 @@ describe('WebhookHandler', () => {
|
||||
|
||||
it('handleRequest produces correct InboundMessage', async () => {
|
||||
const webhooks = [makeWebhook()];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
handler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -116,7 +120,7 @@ describe('WebhookHandler', () => {
|
||||
|
||||
it('handleRequest uses isolated sender IDs when delivery mode is isolated_job', async () => {
|
||||
const webhooks = [makeWebhook()];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any, 'isolated_job');
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry), 'isolated_job');
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
handler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -134,7 +138,7 @@ describe('WebhookHandler', () => {
|
||||
});
|
||||
|
||||
it('returns false for unknown webhook', async () => {
|
||||
handler = new WebhookHandler([], mockChannelRegistry as any);
|
||||
handler = new WebhookHandler([], asWebhookChannelLookup(mockChannelRegistry));
|
||||
await handler.connect();
|
||||
|
||||
const req = mockRequest('test');
|
||||
@@ -148,7 +152,7 @@ describe('WebhookHandler', () => {
|
||||
|
||||
it('returns false for disabled webhook', async () => {
|
||||
const webhooks = [makeWebhook({ enabled: false })];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
await handler.connect();
|
||||
|
||||
const req = mockRequest('test');
|
||||
@@ -163,7 +167,7 @@ describe('WebhookHandler', () => {
|
||||
it('verifies valid HMAC signature', async () => {
|
||||
const secret = 'my-secret-key';
|
||||
const webhooks = [makeWebhook({ secret })];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
handler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -185,7 +189,7 @@ describe('WebhookHandler', () => {
|
||||
it('rejects invalid HMAC signature', async () => {
|
||||
const secret = 'my-secret-key';
|
||||
const webhooks = [makeWebhook({ secret })];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
handler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -204,7 +208,7 @@ describe('WebhookHandler', () => {
|
||||
it('rejects missing HMAC signature when secret is configured', async () => {
|
||||
const secret = 'my-secret-key';
|
||||
const webhooks = [makeWebhook({ secret })];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
handler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -222,7 +226,7 @@ describe('WebhookHandler', () => {
|
||||
|
||||
it('rejects oversized payloads with 413', async () => {
|
||||
const webhooks = [makeWebhook()];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any, 'shared_session', 16);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry), 'shared_session', 16);
|
||||
|
||||
const messages: InboundMessage[] = [];
|
||||
handler.onMessage((msg: InboundMessage) => messages.push(msg));
|
||||
@@ -245,7 +249,7 @@ describe('WebhookHandler', () => {
|
||||
mockChannelRegistry.get.mockReturnValue(mockOutputAdapter);
|
||||
|
||||
const webhooks = [makeWebhook()];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
await handler.connect();
|
||||
|
||||
await handler.send('test-hook', { text: 'Agent response' });
|
||||
@@ -259,7 +263,7 @@ describe('WebhookHandler', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const webhooks = [makeWebhook()];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
await handler.connect();
|
||||
|
||||
await handler.send('test-hook', { text: 'Agent response' });
|
||||
@@ -272,7 +276,7 @@ describe('WebhookHandler', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const webhooks = [makeWebhook()];
|
||||
handler = new WebhookHandler(webhooks, mockChannelRegistry as any);
|
||||
handler = new WebhookHandler(webhooks, asWebhookChannelLookup(mockChannelRegistry));
|
||||
await handler.connect();
|
||||
|
||||
await handler.send('nonexistent-hook', { text: 'response' });
|
||||
|
||||
Reference in New Issue
Block a user