Files
NianToB/tests/unit/gateway-events.test.ts
inman 0abc48189c
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
feat: prepare Zhinian desktop pilot
2026-05-07 21:49:20 +08:00

169 lines
6.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const hostApiFetchMock = vi.fn();
const subscribeHostEventMock = vi.fn();
const chatStateMock = vi.hoisted(() => ({
state: {
currentSessionKey: 'session-1',
sessions: [] as Array<{ key: string }>,
sending: true,
activeRunId: 'run-1' as string | null,
pendingFinal: true,
lastUserMessageAt: 123,
error: null as string | null,
loadHistory: vi.fn(),
loadSessions: vi.fn(),
handleChatEvent: vi.fn(),
},
}));
vi.mock('@/lib/host-api', () => ({
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
}));
vi.mock('@/lib/host-events', () => ({
subscribeHostEvent: (...args: unknown[]) => subscribeHostEventMock(...args),
}));
vi.mock('@/stores/chat', () => ({
useChatStore: {
getState: () => chatStateMock.state,
setState: (patch: Record<string, unknown>) => {
Object.assign(chatStateMock.state, patch);
},
},
}));
describe('gateway store event wiring', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
Object.assign(chatStateMock.state, {
currentSessionKey: 'session-1',
sessions: [],
sending: true,
activeRunId: 'run-1',
pendingFinal: true,
lastUserMessageAt: 123,
error: null,
});
});
it('subscribes to host events through subscribeHostEvent on init', async () => {
hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 });
const handlers = new Map<string, (payload: unknown) => void>();
subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => {
handlers.set(eventName, handler);
return () => {};
});
const { useGatewayStore } = await import('@/stores/gateway');
await useGatewayStore.getState().init();
expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:status', expect.any(Function));
expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:error', expect.any(Function));
expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:notification', expect.any(Function));
expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:chat-message', expect.any(Function));
expect(subscribeHostEventMock).toHaveBeenCalledWith('gateway:channel-status', expect.any(Function));
handlers.get('gateway:status')?.({ state: 'stopped', port: 18789 });
expect(useGatewayStore.getState().status.state).toBe('stopped');
});
it('propagates gatewayReady field from status events', async () => {
hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789, gatewayReady: false });
const handlers = new Map<string, (payload: unknown) => void>();
subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => {
handlers.set(eventName, handler);
return () => {};
});
const { useGatewayStore } = await import('@/stores/gateway');
await useGatewayStore.getState().init();
// Initially gatewayReady=false from the status fetch
expect(useGatewayStore.getState().status.gatewayReady).toBe(false);
// Simulate gateway.ready event setting gatewayReady=true
handlers.get('gateway:status')?.({ state: 'running', port: 18789, gatewayReady: true });
expect(useGatewayStore.getState().status.gatewayReady).toBe(true);
});
it('treats undefined gatewayReady as ready for backwards compatibility', async () => {
hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 });
const handlers = new Map<string, (payload: unknown) => void>();
subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => {
handlers.set(eventName, handler);
return () => {};
});
const { useGatewayStore } = await import('@/stores/gateway');
await useGatewayStore.getState().init();
const status = useGatewayStore.getState().status;
// gatewayReady is undefined (old gateway version) — should be treated as ready
expect(status.gatewayReady).toBeUndefined();
expect(status.state === 'running' && status.gatewayReady !== false).toBe(true);
});
it('does not clear sending state on intermediate agent phase=end notifications', async () => {
hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 });
const handlers = new Map<string, (payload: unknown) => void>();
subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => {
handlers.set(eventName, handler);
return () => {};
});
const { useGatewayStore } = await import('@/stores/gateway');
await useGatewayStore.getState().init();
handlers.get('gateway:notification')?.({
method: 'agent',
params: {
phase: 'end',
runId: 'run-1',
sessionKey: 'session-1',
},
});
await vi.waitFor(() => {
expect(chatStateMock.state.loadHistory).toHaveBeenCalled();
});
expect(chatStateMock.state.sending).toBe(true);
expect(chatStateMock.state.activeRunId).toBe('run-1');
expect(chatStateMock.state.pendingFinal).toBe(true);
});
it('clears sending state on terminal agent completion notifications', async () => {
hostApiFetchMock.mockResolvedValueOnce({ state: 'running', port: 18789 });
const handlers = new Map<string, (payload: unknown) => void>();
subscribeHostEventMock.mockImplementation((eventName: string, handler: (payload: unknown) => void) => {
handlers.set(eventName, handler);
return () => {};
});
const { useGatewayStore } = await import('@/stores/gateway');
await useGatewayStore.getState().init();
handlers.get('gateway:notification')?.({
method: 'agent',
params: {
phase: 'completed',
runId: 'run-1',
sessionKey: 'session-1',
},
});
await vi.waitFor(() => {
expect(chatStateMock.state.sending).toBe(false);
});
expect(chatStateMock.state.activeRunId).toBeNull();
expect(chatStateMock.state.pendingFinal).toBe(false);
});
});