Unify TUI runtime commands with gateway and harden gateway restart
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user