Add channel health diagnostics and gateway recovery fixes (#855)
This commit is contained in:
committed by
GitHub
Unverified
parent
6acd8acf5a
commit
1f39d1a8a7
165
tests/unit/gateway-manager-diagnostics.test.ts
Normal file
165
tests/unit/gateway-manager-diagnostics.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: () => '/tmp',
|
||||
isPackaged: false,
|
||||
},
|
||||
utilityProcess: {
|
||||
fork: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/logger', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('GatewayManager diagnostics', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-04-14T00:00:00.000Z'));
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
it('updates diagnostics on gateway message, rpc success/timeout, and socket close', async () => {
|
||||
const { GatewayManager } = await import('@electron/gateway/manager');
|
||||
const manager = new GatewayManager();
|
||||
|
||||
const ws = {
|
||||
readyState: 1,
|
||||
send: vi.fn(),
|
||||
ping: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
on: vi.fn(),
|
||||
};
|
||||
|
||||
(manager as unknown as { ws: typeof ws }).ws = ws;
|
||||
|
||||
(manager as unknown as { handleMessage: (message: unknown) => void }).handleMessage({
|
||||
type: 'event',
|
||||
event: 'gateway.ready',
|
||||
payload: {},
|
||||
});
|
||||
expect(manager.getDiagnostics().lastAliveAt).toBe(Date.now());
|
||||
|
||||
const successPromise = manager.rpc<{ ok: boolean }>('chat.history', {}, 1000);
|
||||
const successRequestId = Array.from(
|
||||
(manager as unknown as { pendingRequests: Map<string, unknown> }).pendingRequests.keys(),
|
||||
)[0];
|
||||
(manager as unknown as { handleMessage: (message: unknown) => void }).handleMessage({
|
||||
type: 'res',
|
||||
id: successRequestId,
|
||||
ok: true,
|
||||
payload: { ok: true },
|
||||
});
|
||||
await expect(successPromise).resolves.toEqual({ ok: true });
|
||||
expect(manager.getDiagnostics().lastRpcSuccessAt).toBe(Date.now());
|
||||
expect(manager.getDiagnostics().consecutiveRpcFailures).toBe(0);
|
||||
|
||||
const failurePromise = manager.rpc('chat.history', {}, 1000);
|
||||
vi.advanceTimersByTime(1001);
|
||||
await expect(failurePromise).rejects.toThrow('RPC timeout: chat.history');
|
||||
|
||||
const diagnostics = manager.getDiagnostics();
|
||||
expect(diagnostics.lastRpcFailureAt).toBe(Date.now());
|
||||
expect(diagnostics.lastRpcFailureMethod).toBe('chat.history');
|
||||
expect(diagnostics.consecutiveRpcFailures).toBe(1);
|
||||
|
||||
(manager as unknown as { recordSocketClose: (code: number) => void }).recordSocketClose(1006);
|
||||
expect(manager.getDiagnostics().lastSocketCloseAt).toBe(Date.now());
|
||||
expect(manager.getDiagnostics().lastSocketCloseCode).toBe(1006);
|
||||
});
|
||||
|
||||
it('does not count gateway-declared rpc errors as transport failures', async () => {
|
||||
const { GatewayManager } = await import('@electron/gateway/manager');
|
||||
const { buildGatewayHealthSummary } = await import('@electron/utils/gateway-health');
|
||||
const manager = new GatewayManager();
|
||||
|
||||
const ws = {
|
||||
readyState: 1,
|
||||
send: vi.fn(),
|
||||
ping: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
on: vi.fn(),
|
||||
};
|
||||
|
||||
(manager as unknown as { ws: typeof ws }).ws = ws;
|
||||
(manager as unknown as { status: { state: string; port: number } }).status = {
|
||||
state: 'running',
|
||||
port: 18789,
|
||||
};
|
||||
|
||||
const failurePromise = manager.rpc('channels.status', {}, 1000);
|
||||
const failureRequestId = Array.from(
|
||||
(manager as unknown as { pendingRequests: Map<string, unknown> }).pendingRequests.keys(),
|
||||
)[0];
|
||||
(manager as unknown as { handleMessage: (message: unknown) => void }).handleMessage({
|
||||
type: 'res',
|
||||
id: failureRequestId,
|
||||
ok: false,
|
||||
error: { message: 'channel unavailable' },
|
||||
});
|
||||
await expect(failurePromise).rejects.toThrow('channel unavailable');
|
||||
|
||||
expect(manager.getDiagnostics().consecutiveRpcFailures).toBe(0);
|
||||
|
||||
const health = buildGatewayHealthSummary({
|
||||
status: manager.getStatus(),
|
||||
diagnostics: manager.getDiagnostics(),
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(health.reasons).not.toContain('rpc_timeout');
|
||||
});
|
||||
|
||||
it('keeps windows heartbeat recovery disabled while diagnostics degrade', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
|
||||
const { GatewayManager } = await import('@electron/gateway/manager');
|
||||
const { buildGatewayHealthSummary } = await import('@electron/utils/gateway-health');
|
||||
const manager = new GatewayManager();
|
||||
|
||||
const ws = {
|
||||
readyState: 1,
|
||||
send: vi.fn(),
|
||||
ping: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
on: vi.fn(),
|
||||
};
|
||||
|
||||
(manager as unknown as { ws: typeof ws }).ws = ws;
|
||||
(manager as unknown as { shouldReconnect: boolean }).shouldReconnect = true;
|
||||
(manager as unknown as { status: { state: string; port: number } }).status = {
|
||||
state: 'running',
|
||||
port: 18789,
|
||||
};
|
||||
const restartSpy = vi.spyOn(manager, 'restart').mockResolvedValue();
|
||||
|
||||
(manager as unknown as { startPing: () => void }).startPing();
|
||||
vi.advanceTimersByTime(400_000);
|
||||
|
||||
expect(restartSpy).not.toHaveBeenCalled();
|
||||
|
||||
const health = buildGatewayHealthSummary({
|
||||
status: manager.getStatus(),
|
||||
diagnostics: manager.getDiagnostics(),
|
||||
platform: 'win32',
|
||||
});
|
||||
expect(health.state).not.toBe('healthy');
|
||||
|
||||
(manager as unknown as { connectionMonitor: { clear: () => void } }).connectionMonitor.clear();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user