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' });
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ManagedSession } from '../../session/index.js';
|
||||
import type { ModelClient } from '../../models/types.js';
|
||||
import type { ModelRouter } from '../../models/router.js';
|
||||
|
||||
const {
|
||||
mockLoadStoredAnthropicAuth,
|
||||
@@ -33,6 +36,30 @@ vi.mock('node:readline', () => ({
|
||||
emitKeypressEvents: vi.fn(),
|
||||
}));
|
||||
|
||||
function asSession(value: unknown): ManagedSession {
|
||||
return value as ManagedSession;
|
||||
}
|
||||
|
||||
function asModelClient(value: unknown): ModelClient {
|
||||
return value as ModelClient;
|
||||
}
|
||||
|
||||
function asModelRouter(value: unknown): ModelRouter {
|
||||
return value as ModelRouter;
|
||||
}
|
||||
|
||||
function minimalTuiPrivates(value: unknown): {
|
||||
rl: { pause: () => void; resume: () => void };
|
||||
prompt: (text: string) => Promise<string>;
|
||||
handleLoginCommand: (provider: string) => Promise<void>;
|
||||
} {
|
||||
return value as {
|
||||
rl: { pause: () => void; resume: () => void };
|
||||
prompt: (text: string) => Promise<string>;
|
||||
handleLoginCommand: (provider: string) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
describe('MinimalTui login re-auth confirmation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -57,20 +84,20 @@ describe('MinimalTui login re-auth confirmation', () => {
|
||||
};
|
||||
|
||||
const tui = new MinimalTui({
|
||||
session: mockSession as any,
|
||||
modelClient: {} as any,
|
||||
modelRouter: {} as any,
|
||||
session: asSession(mockSession),
|
||||
modelClient: asModelClient({}),
|
||||
modelRouter: asModelRouter({}),
|
||||
systemPrompt: 'test',
|
||||
});
|
||||
|
||||
(tui as any).rl = { pause: vi.fn(), resume: vi.fn() };
|
||||
const promptMock = vi.spyOn(tui as any, 'prompt')
|
||||
minimalTuiPrivates(tui).rl = { pause: vi.fn(), resume: vi.fn() };
|
||||
const promptMock = vi.spyOn(minimalTuiPrivates(tui), 'prompt')
|
||||
.mockResolvedValueOnce('') // default -> API key path
|
||||
.mockResolvedValueOnce('n'); // confirmation
|
||||
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await (tui as any).handleLoginCommand('anthropic');
|
||||
await minimalTuiPrivates(tui).handleLoginCommand('anthropic');
|
||||
|
||||
expect(promptMock).toHaveBeenCalled();
|
||||
expect(mockStoreAnthropicAuth).not.toHaveBeenCalled();
|
||||
@@ -98,23 +125,23 @@ describe('MinimalTui login re-auth confirmation', () => {
|
||||
};
|
||||
|
||||
const tui = new MinimalTui({
|
||||
session: mockSession as any,
|
||||
modelClient: {} as any,
|
||||
modelRouter: {} as any,
|
||||
session: asSession(mockSession),
|
||||
modelClient: asModelClient({}),
|
||||
modelRouter: asModelRouter({}),
|
||||
systemPrompt: 'test',
|
||||
});
|
||||
|
||||
const pause = vi.fn();
|
||||
const resume = vi.fn();
|
||||
(tui as any).rl = { pause, resume };
|
||||
vi.spyOn(tui as any, 'prompt')
|
||||
minimalTuiPrivates(tui).rl = { pause, resume };
|
||||
vi.spyOn(minimalTuiPrivates(tui), 'prompt')
|
||||
.mockResolvedValueOnce('') // default -> API key path
|
||||
.mockResolvedValueOnce('y'); // confirmation
|
||||
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
await (tui as any).handleLoginCommand('anthropic');
|
||||
await minimalTuiPrivates(tui).handleLoginCommand('anthropic');
|
||||
|
||||
expect(mockStoreAnthropicAuth).toHaveBeenCalledWith('new-anthropic-key');
|
||||
expect(pause).toHaveBeenCalled();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
|
||||
import { execFile } from 'child_process';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
// Mock child_process before importing module
|
||||
vi.mock('child_process', () => ({
|
||||
@@ -7,6 +8,11 @@ vi.mock('child_process', () => ({
|
||||
}));
|
||||
|
||||
const mockExecFile = vi.mocked(execFile);
|
||||
type ExecFileCallback = (error: Error | null, stdout: string, stderr: string) => void;
|
||||
|
||||
function mockChildProcess(): ChildProcess {
|
||||
return {} as ChildProcess;
|
||||
}
|
||||
|
||||
describe('tailscale', () => {
|
||||
// Import after mocking
|
||||
@@ -28,13 +34,13 @@ describe('tailscale', () => {
|
||||
describe('isTailscaleAvailable', () => {
|
||||
it('returns available when tailscale CLI works', async () => {
|
||||
mockExecFile
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(null, '1.62.0', '');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
})
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(null, '{}', '');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
});
|
||||
|
||||
const result = await isTailscaleAvailable();
|
||||
@@ -43,9 +49,9 @@ describe('tailscale', () => {
|
||||
});
|
||||
|
||||
it('returns unavailable when tailscale CLI fails', async () => {
|
||||
mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(new Error('command not found'), '', 'command not found');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
});
|
||||
|
||||
const result = await isTailscaleAvailable();
|
||||
@@ -58,14 +64,14 @@ describe('tailscale', () => {
|
||||
it('calls tailscale serve with correct args', async () => {
|
||||
mockExecFile
|
||||
// serve command
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(null, '', '');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
})
|
||||
// status for hostname
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), '');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
});
|
||||
|
||||
const url = await startTailscaleServe({ localPort: 18800 });
|
||||
@@ -78,13 +84,13 @@ describe('tailscale', () => {
|
||||
|
||||
it('uses custom serve port', async () => {
|
||||
mockExecFile
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(null, '', '');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
})
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(null, JSON.stringify({ Self: { DNSName: 'myhost.tailnet.ts.net.' } }), '');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
});
|
||||
|
||||
const url = await startTailscaleServe({ localPort: 18800, servePort: 8443 });
|
||||
@@ -97,9 +103,9 @@ describe('tailscale', () => {
|
||||
|
||||
describe('stopTailscaleServe', () => {
|
||||
it('calls tailscale serve off', async () => {
|
||||
mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(null, '', '');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
});
|
||||
|
||||
await stopTailscaleServe({ localPort: 18800 });
|
||||
@@ -111,9 +117,9 @@ describe('tailscale', () => {
|
||||
});
|
||||
|
||||
it('does not throw on failure', async () => {
|
||||
mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: any) => {
|
||||
mockExecFile.mockImplementationOnce((_cmd, _args, _opts, callback: ExecFileCallback) => {
|
||||
callback(new Error('failed'), '', 'failed');
|
||||
return {} as any;
|
||||
return mockChildProcess();
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
|
||||
Reference in New Issue
Block a user