Public source validation / validate (push) Failing after 3m8s
122 lines
4.9 KiB
TypeScript
122 lines
4.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { LiveClient, type SocketFactory } from '../../src/liveClient';
|
|
import type { MetricQueryRequest } from '../../src/metricClient';
|
|
|
|
class FakeSocket {
|
|
readyState = 0;
|
|
onopen: (() => void) | null = null;
|
|
onmessage: ((event: { data: unknown }) => void) | null = null;
|
|
onerror: (() => void) | null = null;
|
|
onclose: (() => void) | null = null;
|
|
readonly sent: string[] = [];
|
|
|
|
open(): void { this.readyState = 1; this.onopen?.(); }
|
|
send(payload: string): void { this.sent.push(payload); }
|
|
close(): void { this.readyState = 3; this.onclose?.(); }
|
|
}
|
|
|
|
const request: MetricQueryRequest = {
|
|
metric: 'host.cpu.utilization',
|
|
scope: { serverId: 'smoke-host' },
|
|
range: { from: '2026-08-10T06:00:00.000Z', to: '2026-08-10T06:05:00.000Z', stepSeconds: 15 },
|
|
aggregation: 'avg',
|
|
};
|
|
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
describe('LiveClient subscription lifecycle', () => {
|
|
it('reuses one socket when React replaces an equivalent listener inside the release grace', async () => {
|
|
vi.useFakeTimers();
|
|
const sockets: FakeSocket[] = [];
|
|
const factory = vi.fn(() => {
|
|
const socket = new FakeSocket();
|
|
sockets.push(socket);
|
|
return socket;
|
|
}) as unknown as SocketFactory;
|
|
const client = new LiveClient('/api/v1/live', factory);
|
|
|
|
const first = client.subscribe(request, () => undefined);
|
|
sockets[0].open();
|
|
await Promise.resolve();
|
|
first.unsubscribe();
|
|
|
|
const shifted = { ...request, range: { ...request.range, from: '2026-08-10T06:01:00.000Z', to: '2026-08-10T06:06:00.000Z' } };
|
|
const second = client.subscribe(shifted, () => undefined);
|
|
await vi.advanceTimersByTimeAsync(300);
|
|
|
|
expect(factory).toHaveBeenCalledTimes(1);
|
|
expect(sockets[0].readyState).toBe(1);
|
|
expect(sockets[0].sent.filter((payload) => payload.includes('unsubscribe'))).toHaveLength(0);
|
|
|
|
await vi.advanceTimersByTimeAsync(30_000);
|
|
expect(sockets[0].sent.some((payload) => payload.includes('"type":"ping"'))).toBe(true);
|
|
|
|
second.unsubscribe();
|
|
await vi.advanceTimersByTimeAsync(251);
|
|
expect(sockets[0].readyState).toBe(1);
|
|
await vi.advanceTimersByTimeAsync(10_000);
|
|
expect(sockets[0].readyState).toBe(3);
|
|
});
|
|
|
|
it('reuses an idle transport while a rotating dashboard loads its next query', async () => {
|
|
vi.useFakeTimers();
|
|
const sockets: FakeSocket[] = [];
|
|
const factory = vi.fn(() => {
|
|
const socket = new FakeSocket();
|
|
sockets.push(socket);
|
|
return socket;
|
|
}) as unknown as SocketFactory;
|
|
const client = new LiveClient('/api/v1/live', factory);
|
|
|
|
const first = client.subscribe(request, () => undefined);
|
|
sockets[0].open();
|
|
await Promise.resolve();
|
|
first.unsubscribe();
|
|
|
|
// Subscription state is released after 250 ms, but the bounded transport
|
|
// grace bridges a slower dashboard document fetch.
|
|
await vi.advanceTimersByTimeAsync(2_000);
|
|
expect(sockets[0].readyState).toBe(1);
|
|
|
|
const nextRequest = { ...request, metric: 'host.memory.utilization' };
|
|
const second = client.subscribe(nextRequest, () => undefined);
|
|
await Promise.resolve();
|
|
|
|
expect(factory).toHaveBeenCalledTimes(1);
|
|
expect(sockets[0].readyState).toBe(1);
|
|
expect(sockets[0].sent.some((payload) => payload.includes('host.memory.utilization'))).toBe(true);
|
|
|
|
second.unsubscribe();
|
|
await vi.advanceTimersByTimeAsync(10_251);
|
|
expect(sockets[0].readyState).toBe(3);
|
|
});
|
|
|
|
it('resubscribes once after reconnect and resumes samples on the bounded subscription', async () => {
|
|
vi.useFakeTimers();
|
|
const sockets: FakeSocket[] = [];
|
|
const factory = vi.fn(() => { const socket = new FakeSocket(); sockets.push(socket); return socket; }) as unknown as SocketFactory;
|
|
const events: string[] = [];
|
|
const client = new LiveClient('/api/v1/live', factory);
|
|
const subscription = client.subscribe(request, (event) => events.push(event.type));
|
|
sockets[0].open();
|
|
await Promise.resolve();
|
|
expect(sockets[0].sent.filter((payload) => payload.includes('"type":"subscribe"'))).toHaveLength(1);
|
|
|
|
sockets[0].close();
|
|
expect(events).toContain('status');
|
|
await vi.advanceTimersByTimeAsync(1_000);
|
|
expect(factory).toHaveBeenCalledTimes(2);
|
|
sockets[1].open();
|
|
await Promise.resolve();
|
|
const subscribe = JSON.parse(sockets[1].sent.find((payload) => payload.includes('"type":"subscribe"')) ?? '{}') as { subscriptionId?: string };
|
|
expect(sockets[1].sent.filter((payload) => payload.includes('"type":"subscribe"'))).toHaveLength(1);
|
|
sockets[1].onmessage?.({ data: JSON.stringify({ type: 'samples', subscriptionId: subscribe.subscriptionId, sequence: 1, samples: [{ timestamp: '2026-08-10T06:06:00.000Z', value: 42, labels: {} }] }) });
|
|
expect(events.at(-1)).toBe('samples');
|
|
expect(sockets).toHaveLength(2);
|
|
|
|
subscription.unsubscribe();
|
|
await vi.advanceTimersByTimeAsync(10_251);
|
|
});
|
|
});
|