- Added telemetry utility to capture application events and metrics. - Integrated PostHog for event tracking with distinct user identification. - Implemented telemetry initialization, event capturing, and shutdown procedures. feat: add UV environment setup for Python management - Created utilities to manage Python installation and configuration. - Implemented network optimization checks for Python installation mirrors. - Added functions to set up managed Python environments with error handling. feat: enhance host API communication with token management - Introduced host API token retrieval and management for secure requests. - Updated host API fetch functions to include token in headers. - Added support for creating event sources with authentication. test: add comprehensive tests for gateway protocol and startup helpers - Implemented unit tests for gateway protocol helpers, event dispatching, and state management. - Added tests for startup recovery strategies and process policies. - Ensured coverage for connection monitoring and restart governance logic.
250 lines
7.2 KiB
TypeScript
250 lines
7.2 KiB
TypeScript
// @vitest-environment node
|
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
vi.mock('@electron/service/logger', () => {
|
|
const logger = {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
};
|
|
|
|
return {
|
|
default: logger,
|
|
logManager: logger,
|
|
};
|
|
});
|
|
|
|
import {
|
|
createErrorResponse,
|
|
createRequest,
|
|
createSuccessResponse,
|
|
GatewayEventType,
|
|
isNotification,
|
|
isRequest,
|
|
isResponse,
|
|
} from '../electron/gateway/protocol';
|
|
import {
|
|
dispatchJsonRpcNotification,
|
|
dispatchProtocolEvent,
|
|
} from '../electron/gateway/event-dispatch';
|
|
import { GatewayStateController } from '../electron/gateway/state';
|
|
import { createInitialGatewayDiagnostics } from '../electron/gateway/diagnostics';
|
|
|
|
type EmittedEvent = {
|
|
event: string;
|
|
payload: unknown;
|
|
};
|
|
|
|
function createEmitterRecorder() {
|
|
const events: EmittedEvent[] = [];
|
|
|
|
return {
|
|
events,
|
|
emitter: {
|
|
emit: vi.fn((event: string, payload: unknown) => {
|
|
events.push({ event, payload });
|
|
return true;
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('gateway protocol helpers', () => {
|
|
it('creates JSON-RPC request and responses', () => {
|
|
const request = createRequest('skills.status', { includeDisabled: true }, 'req-1');
|
|
expect(request).toEqual({
|
|
jsonrpc: '2.0',
|
|
id: 'req-1',
|
|
method: 'skills.status',
|
|
params: { includeDisabled: true },
|
|
});
|
|
|
|
expect(createSuccessResponse('req-1', { ok: true })).toEqual({
|
|
jsonrpc: '2.0',
|
|
id: 'req-1',
|
|
result: { ok: true },
|
|
});
|
|
|
|
expect(createErrorResponse('req-1', -32001, 'not connected')).toEqual({
|
|
jsonrpc: '2.0',
|
|
id: 'req-1',
|
|
error: {
|
|
code: -32001,
|
|
message: 'not connected',
|
|
data: undefined,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('generates a request id when one is not supplied', () => {
|
|
const request = createRequest('gateway.ping');
|
|
expect(request.jsonrpc).toBe('2.0');
|
|
expect(typeof request.id).toBe('string');
|
|
expect(request.id).not.toHaveLength(0);
|
|
});
|
|
|
|
it('detects requests, responses, and notifications', () => {
|
|
const request = createRequest('chat.send', { text: 'hello' }, 'req-2');
|
|
const response = createSuccessResponse('req-2', { runId: 'run-1' });
|
|
const notification = {
|
|
jsonrpc: '2.0' as const,
|
|
method: GatewayEventType.MESSAGE_RECEIVED,
|
|
params: { message: 'hi' },
|
|
};
|
|
|
|
expect(isRequest(request)).toBe(true);
|
|
expect(isResponse(request)).toBe(false);
|
|
expect(isNotification(request)).toBe(false);
|
|
|
|
expect(isResponse(response)).toBe(true);
|
|
expect(isRequest(response)).toBe(false);
|
|
expect(isNotification(response)).toBe(false);
|
|
|
|
expect(isNotification(notification)).toBe(true);
|
|
expect(isRequest(notification)).toBe(false);
|
|
expect(isResponse(notification)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('gateway event dispatch', () => {
|
|
it('dispatches protocol chat and gateway.ready events', () => {
|
|
const { emitter, events } = createEmitterRecorder();
|
|
|
|
dispatchProtocolEvent(emitter, 'chat', { message: 'delta' });
|
|
dispatchProtocolEvent(emitter, 'gateway.ready', { source: 'event' });
|
|
|
|
expect(events).toEqual([
|
|
{ event: 'chat:message', payload: { message: { message: 'delta' } } },
|
|
{ event: 'gateway:ready', payload: { source: 'event' } },
|
|
]);
|
|
});
|
|
|
|
it('dispatches unknown protocol events to notification listeners', () => {
|
|
const { emitter, events } = createEmitterRecorder();
|
|
|
|
dispatchProtocolEvent(emitter, 'skills.changed', { slug: 'minimax-xlsx' });
|
|
|
|
expect(events).toEqual([
|
|
{
|
|
event: 'notification',
|
|
payload: { method: 'skills.changed', params: { slug: 'minimax-xlsx' } },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('dispatches JSON-RPC notifications onto typed channels', () => {
|
|
const { emitter, events } = createEmitterRecorder();
|
|
|
|
dispatchJsonRpcNotification(emitter, {
|
|
jsonrpc: '2.0',
|
|
method: GatewayEventType.CHANNEL_STATUS_CHANGED,
|
|
params: { channelId: 'wx', status: 'connected' },
|
|
});
|
|
dispatchJsonRpcNotification(emitter, {
|
|
jsonrpc: '2.0',
|
|
method: GatewayEventType.MESSAGE_RECEIVED,
|
|
params: { message: { text: 'hello' } },
|
|
});
|
|
dispatchJsonRpcNotification(emitter, {
|
|
jsonrpc: '2.0',
|
|
method: GatewayEventType.ERROR,
|
|
params: { message: 'gateway boom' },
|
|
});
|
|
|
|
expect(events[0]).toEqual({
|
|
event: 'notification',
|
|
payload: {
|
|
jsonrpc: '2.0',
|
|
method: GatewayEventType.CHANNEL_STATUS_CHANGED,
|
|
params: { channelId: 'wx', status: 'connected' },
|
|
},
|
|
});
|
|
expect(events[1]).toEqual({
|
|
event: 'channel:status',
|
|
payload: { channelId: 'wx', status: 'connected' },
|
|
});
|
|
expect(events[2]).toEqual({
|
|
event: 'notification',
|
|
payload: {
|
|
jsonrpc: '2.0',
|
|
method: GatewayEventType.MESSAGE_RECEIVED,
|
|
params: { message: { text: 'hello' } },
|
|
},
|
|
});
|
|
expect(events[3]).toEqual({
|
|
event: 'chat:message',
|
|
payload: { message: { text: 'hello' } },
|
|
});
|
|
expect(events[4].event).toBe('notification');
|
|
expect(events[5].event).toBe('error');
|
|
expect((events[5].payload as Error).message).toBe('gateway boom');
|
|
});
|
|
});
|
|
|
|
describe('gateway state controller', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-04-23T00:00:00.000Z'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('tracks transitions, connectivity, and uptime', () => {
|
|
const emittedStatuses: Array<ReturnType<GatewayStateController['getStatus']>> = [];
|
|
const transitions: Array<[string, string]> = [];
|
|
const controller = new GatewayStateController({
|
|
emitStatus: (status) => {
|
|
emittedStatuses.push({ ...status });
|
|
},
|
|
onTransition: (previousState, nextState) => {
|
|
transitions.push([previousState, nextState]);
|
|
},
|
|
});
|
|
|
|
controller.setStatus({ state: 'starting', port: 18789 });
|
|
controller.setStatus({ state: 'running', port: 18789, connectedAt: Date.now() - 2_000 });
|
|
|
|
expect(controller.isConnected(false)).toBe(false);
|
|
expect(controller.isConnected(true)).toBe(true);
|
|
expect(emittedStatuses[0]).toMatchObject({ state: 'starting', port: 18789 });
|
|
expect(emittedStatuses[1]).toMatchObject({
|
|
state: 'running',
|
|
port: 18789,
|
|
connectedAt: Date.now() - 2_000,
|
|
});
|
|
expect(emittedStatuses[1].uptime).toBe(2_000);
|
|
expect(transitions).toEqual([
|
|
['stopped', 'starting'],
|
|
['starting', 'running'],
|
|
]);
|
|
|
|
vi.setSystemTime(new Date('2026-04-23T00:00:05.000Z'));
|
|
expect(controller.getStatus().uptime).toBe(7_000);
|
|
});
|
|
|
|
it('clears uptime when the gateway is no longer running', () => {
|
|
const controller = new GatewayStateController({
|
|
emitStatus: () => {},
|
|
});
|
|
|
|
controller.setStatus({ state: 'running', port: 18789, connectedAt: Date.now() - 1_000 });
|
|
expect(controller.getStatus().uptime).toBe(1_000);
|
|
|
|
controller.setStatus({ state: 'stopped' });
|
|
expect(controller.getStatus().uptime).toBeUndefined();
|
|
expect(controller.isConnected(true)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('gateway diagnostics helpers', () => {
|
|
it('creates an empty diagnostics snapshot', () => {
|
|
expect(createInitialGatewayDiagnostics()).toEqual({
|
|
consecutiveHeartbeatMisses: 0,
|
|
consecutiveRpcFailures: 0,
|
|
});
|
|
});
|
|
});
|