import { fireEvent, render, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ExecutionDashboard } from '../../src/components/ExecutionDashboard';
import {
connectHeartbeat,
fetchOpsSnapshot,
getAuthToken,
normalizeServerUrl,
probeBackendStatus,
setBackendUrl,
streamOpsSnapshot,
type OpsSnapshot,
} from '../../src/services/backend-client';
vi.mock('../../src/services/backend-client', () => ({
connectHeartbeat: vi.fn(() => () => {}),
fetchOpsSnapshot: vi.fn(),
getAuthToken: vi.fn(() => ''),
getBackendUrl: vi.fn(() => 'http://127.0.0.1:4747'),
normalizeServerUrl: vi.fn((url: string) => url),
probeBackendStatus: vi.fn(),
setBackendUrl: vi.fn(),
streamOpsSnapshot: vi.fn(),
}));
const emptyMetrics = {
total: 0,
active: 0,
queued: 0,
complete: 0,
failed: 0,
byStatus: {
queued: 0,
cloning: 0,
analyzing: 0,
loading: 0,
complete: 0,
failed: 0,
},
avgDurationMs: null,
maxDurationMs: null,
activeProgressSum: 0,
};
const emptySnap = (): OpsSnapshot => ({
generatedAt: 1,
uptimeMs: 1,
health: 'ok',
server: { version: '1', launchContext: 'local', nodeVersion: 'v22' },
analyze: { jobs: [], metrics: emptyMetrics },
embed: { jobs: [], metrics: emptyMetrics },
totals: { jobs: 0, active: 0, failed: 0, complete: 0 },
});
describe('ExecutionDashboard safety poll', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(probeBackendStatus).mockResolvedValue('ok');
vi.mocked(connectHeartbeat).mockReturnValue(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('stops the safety poll once the SSE stream delivers a frame', async () => {
let onFrame: ((snapshot: OpsSnapshot) => void) | undefined;
vi.mocked(streamOpsSnapshot).mockImplementation((onSnapshot) => {
onFrame = onSnapshot;
return { abort: vi.fn() } as unknown as AbortController;
});
vi.mocked(fetchOpsSnapshot).mockRejectedValue(new Error('rest down'));
const setIntervalSpy = vi.spyOn(window, 'setInterval');
const clearIntervalSpy = vi.spyOn(window, 'clearInterval');
const { getByText } = render();
await waitFor(() => expect(setIntervalSpy).toHaveBeenCalled());
const timerId = setIntervalSpy.mock.results[0]?.value;
expect(onFrame).toBeTypeOf('function');
onFrame!(emptySnap());
await waitFor(() => expect(clearIntervalSpy).toHaveBeenCalledWith(timerId));
expect(getByText(/ยท sse/)).toBeInTheDocument();
});
it('polls /api/ops at 2s so the fallback stays under the 60/min route limit', async () => {
vi.mocked(streamOpsSnapshot).mockImplementation(
() => ({ abort: vi.fn() }) as unknown as AbortController,
);
vi.mocked(fetchOpsSnapshot).mockRejectedValue(new Error('rest down'));
const setIntervalSpy = vi.spyOn(window, 'setInterval');
render();
await waitFor(() => expect(setIntervalSpy).toHaveBeenCalled());
expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 2_000);
});
it('clears the prior snapshot when connecting to an unreachable server', async () => {
const first = emptySnap();
first.server = { ...first.server, version: 'old-server' };
vi.mocked(streamOpsSnapshot).mockImplementation((onSnapshot) => {
onSnapshot(first);
return { abort: vi.fn() } as unknown as AbortController;
});
vi.mocked(fetchOpsSnapshot).mockResolvedValue(first);
const { getByText, getByPlaceholderText, queryByText } = render();
await waitFor(() => expect(getByText('old-server')).toBeInTheDocument());
vi.mocked(probeBackendStatus).mockResolvedValue('unreachable');
vi.mocked(fetchOpsSnapshot).mockRejectedValue(new Error('down'));
vi.mocked(streamOpsSnapshot).mockImplementation(
() => ({ abort: vi.fn() }) as unknown as AbortController,
);
const input = getByPlaceholderText('http://localhost:4747');
fireEvent.change(input, { target: { value: 'http://127.0.0.1:9999' } });
fireEvent.submit(input.closest('form')!);
await waitFor(() => {
expect(queryByText('old-server')).not.toBeInTheDocument();
});
});
it('keeps an invalid-server error when the prior stream delivers a snapshot', async () => {
let onFrame: ((snapshot: OpsSnapshot) => void) | undefined;
vi.mocked(streamOpsSnapshot).mockImplementation((onSnapshot) => {
onFrame = onSnapshot;
return { abort: vi.fn() } as unknown as AbortController;
});
vi.mocked(fetchOpsSnapshot).mockResolvedValue(emptySnap());
vi.mocked(normalizeServerUrl).mockImplementation((url: string) => {
if (url.includes('bad')) throw new Error('Invalid backend URL');
return url;
});
const { getByText, getByPlaceholderText } = render();
await waitFor(() => expect(onFrame).toBeTypeOf('function'));
const input = getByPlaceholderText('http://localhost:4747');
fireEvent.change(input, { target: { value: 'http://bad' } });
fireEvent.submit(input.closest('form')!);
await waitFor(() => expect(getByText('Invalid backend URL')).toBeInTheDocument());
onFrame!(emptySnap());
await waitFor(() => expect(getByText('Invalid backend URL')).toBeInTheDocument());
});
});
describe('ExecutionDashboard ?server= link', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(probeBackendStatus).mockResolvedValue('ok');
vi.mocked(fetchOpsSnapshot).mockResolvedValue(emptySnap());
vi.mocked(streamOpsSnapshot).mockReturnValue({ abort: vi.fn() } as unknown as AbortController);
window.history.replaceState({}, '', '/?view=ops&server=https://other.example');
});
afterEach(() => {
window.history.replaceState({}, '', '/');
vi.mocked(getAuthToken).mockReturnValue('');
});
it('does not auto-connect a foreign server while a deploy token is held', async () => {
vi.mocked(getAuthToken).mockReturnValue('deploy-token');
const { getByDisplayValue } = render();
expect(getByDisplayValue('https://other.example')).toBeInTheDocument();
expect(setBackendUrl).not.toHaveBeenCalled();
expect(streamOpsSnapshot).not.toHaveBeenCalled();
});
it('auto-connects the linked server when no token is held', async () => {
render();
await waitFor(() => expect(setBackendUrl).toHaveBeenCalledWith('https://other.example'));
});
});