140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { configSchema } from '../config/schema.js';
|
|
import { Lifecycle } from './lifecycle.js';
|
|
import { startServices } from './services.js';
|
|
|
|
vi.mock('../automation/index.js', () => {
|
|
return {
|
|
HeartbeatMonitor: class {
|
|
start(): void {}
|
|
stop(): void {}
|
|
},
|
|
MinioSyncScheduler: class {
|
|
start(): void {}
|
|
stop(): void {}
|
|
},
|
|
};
|
|
});
|
|
|
|
function makeConfig(overrides: Record<string, unknown> = {}) {
|
|
return configSchema.parse({
|
|
telegram: { bot_token: 'test-token', allowed_chat_ids: [1] },
|
|
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe('startServices startup ordering', () => {
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('fails after bounded retries on persistent gateway bind collision before starting channels', async () => {
|
|
const lifecycle = new Lifecycle();
|
|
const config = makeConfig({ server: { localhost: true, port: 18800 } });
|
|
const startError = new Error('listen EADDRINUSE: address already in use 127.0.0.1:18800') as Error & { code?: string };
|
|
startError.code = 'EADDRINUSE';
|
|
|
|
const channelRegistry = {
|
|
startAll: vi.fn(async () => {}),
|
|
stopAll: vi.fn(async () => {}),
|
|
};
|
|
const gateway = {
|
|
start: vi.fn(async () => { throw startError; }),
|
|
stop: vi.fn(async () => {}),
|
|
getMetrics: vi.fn(() => ({ getModelMetrics: () => [] })),
|
|
};
|
|
|
|
await expect(startServices({
|
|
config,
|
|
lifecycle,
|
|
channelRegistry: channelRegistry as never,
|
|
gateway: gateway as never,
|
|
modelRouter: {} as never,
|
|
memoryDir: '/tmp',
|
|
dataDir: '/tmp',
|
|
gatewayStartRetry: {
|
|
maxAttempts: 3,
|
|
retryDelayMs: 0,
|
|
sleep: async () => {},
|
|
},
|
|
})).rejects.toThrow('Gateway bind failed');
|
|
|
|
expect(channelRegistry.startAll).not.toHaveBeenCalled();
|
|
expect(channelRegistry.stopAll).not.toHaveBeenCalled();
|
|
expect(gateway.start).toHaveBeenCalledTimes(3);
|
|
expect(gateway.stop).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('retries gateway bind collisions and then starts channels on success', async () => {
|
|
const lifecycle = new Lifecycle();
|
|
const config = makeConfig({ server: { localhost: true, port: 18800 } });
|
|
const startError = new Error('listen EADDRINUSE: address already in use 127.0.0.1:18800') as Error & { code?: string };
|
|
startError.code = 'EADDRINUSE';
|
|
const order: string[] = [];
|
|
|
|
const channelRegistry = {
|
|
startAll: vi.fn(async () => { order.push('channels.start'); }),
|
|
stopAll: vi.fn(async () => {}),
|
|
};
|
|
const gateway = {
|
|
start: vi.fn(async () => {
|
|
order.push('gateway.start');
|
|
if (gateway.start.mock.calls.length === 1) {
|
|
throw startError;
|
|
}
|
|
}),
|
|
stop: vi.fn(async () => { order.push('gateway.stop'); }),
|
|
getMetrics: vi.fn(() => ({ getModelMetrics: () => [] })),
|
|
};
|
|
|
|
await startServices({
|
|
config,
|
|
lifecycle,
|
|
channelRegistry: channelRegistry as never,
|
|
gateway: gateway as never,
|
|
modelRouter: {} as never,
|
|
memoryDir: '/tmp',
|
|
dataDir: '/tmp',
|
|
gatewayStartRetry: {
|
|
maxAttempts: 3,
|
|
retryDelayMs: 0,
|
|
sleep: async () => {},
|
|
},
|
|
});
|
|
|
|
expect(order).toEqual(['gateway.start', 'gateway.stop', 'gateway.start', 'channels.start']);
|
|
expect(channelRegistry.startAll).toHaveBeenCalledOnce();
|
|
await lifecycle.shutdown();
|
|
});
|
|
|
|
it('starts gateway before channels when startup succeeds', async () => {
|
|
const lifecycle = new Lifecycle();
|
|
const config = makeConfig({ server: { localhost: true, port: 18800 } });
|
|
const order: string[] = [];
|
|
|
|
const channelRegistry = {
|
|
startAll: vi.fn(async () => { order.push('channels.start'); }),
|
|
stopAll: vi.fn(async () => {}),
|
|
};
|
|
const gateway = {
|
|
start: vi.fn(async () => { order.push('gateway.start'); }),
|
|
stop: vi.fn(async () => {}),
|
|
getMetrics: vi.fn(() => ({ getModelMetrics: () => [] })),
|
|
};
|
|
|
|
await startServices({
|
|
config,
|
|
lifecycle,
|
|
channelRegistry: channelRegistry as never,
|
|
gateway: gateway as never,
|
|
modelRouter: {} as never,
|
|
memoryDir: '/tmp',
|
|
dataDir: '/tmp',
|
|
});
|
|
|
|
expect(order).toEqual(['gateway.start', 'channels.start']);
|
|
await lifecycle.shutdown();
|
|
});
|
|
});
|