Files
zn-ai/tests/gateway-protocol-state.test.ts
DEV_DSW 71bcc3b3c5 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.
2026-04-23 17:21:57 +08:00

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