feat: implement telemetry system for application usage tracking
- 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.
This commit is contained in:
249
tests/gateway-protocol-state.test.ts
Normal file
249
tests/gateway-protocol-state.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
// @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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user