Files
DeskClaw/tests/unit/gateway-manager-diagnostics.test.ts

166 lines
5.4 KiB
TypeScript

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();
});
});