feat(cursor): harden live probe and runtime contracts

This commit is contained in:
Tam Nhu Tran committed 2026-04-12 01:51:26 -04:00
1 parent 2eaf760d20
commit e1049e38d4
23 files changed
+2186 -306

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { ReactNode } from 'react';
import { AllProviders } from '../../setup/test-utils';
import { useCursor } from '@/hooks/use-cursor';
function createJsonResponse(body: Record<string, unknown>, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
const wrapper = ({ children }: { children: ReactNode }) => <AllProviders>{children}</AllProviders>;
describe('useCursor', () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('preserves structured probe failures and refreshes live status queries', async () => {
const probeFailure = {
ok: false,
stage: 'daemon',
status: 503,
duration_ms: 321,
error_type: 'daemon_not_running',
message: 'Cursor daemon is not running.',
};
const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.endsWith('/api/cursor/status')) {
return Promise.resolve(
createJsonResponse({
enabled: true,
authenticated: true,
auth_method: 'manual',
token_age: 1,
token_expired: false,
daemon_running: false,
port: 20129,
auto_start: false,
ghost_mode: true,
})
);
}
if (url.endsWith('/api/cursor/settings')) {
return Promise.resolve(
createJsonResponse({
enabled: true,
port: 20129,
auto_start: false,
ghost_mode: true,
model: 'gpt-5.3-codex',
})
);
}
if (url.endsWith('/api/cursor/models')) {
return Promise.resolve(
createJsonResponse({
models: [{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', provider: 'openai' }],
current: 'gpt-5.3-codex',
})
);
}
if (url.endsWith('/api/cursor/settings/raw')) {
return Promise.resolve(
createJsonResponse({
settings: {},
mtime: 100,
path: '/tmp/cursor.settings.json',
exists: true,
})
);
}
if (url.endsWith('/api/cursor/probe') && init?.method === 'POST') {
return Promise.resolve(createJsonResponse(probeFailure, 503));
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
});
vi.stubGlobal('fetch', fetchMock);
const { result } = renderHook(() => useCursor(), { wrapper });
await waitFor(() => expect(result.current.status?.enabled).toBe(true));
await waitFor(() => expect(result.current.models.length).toBe(1));
await act(async () => {
const probeResult = await result.current.runProbeAsync();
expect(probeResult).toMatchObject(probeFailure);
});
await waitFor(() => expect(result.current.probeResult).toMatchObject(probeFailure));
await waitFor(() =>
expect(
fetchMock.mock.calls.filter(([input]) => String(input).endsWith('/api/cursor/status'))
.length
).toBeGreaterThanOrEqual(2)
);
await waitFor(() =>
expect(
fetchMock.mock.calls.filter(([input]) => String(input).endsWith('/api/cursor/models'))
.length
).toBeGreaterThanOrEqual(2)
);
});
});
+157
View File
@@ -0,0 +1,157 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils';
const mocks = vi.hoisted(() => ({
useCursor: vi.fn(),
runProbeAsync: vi.fn(),
resetProbe: vi.fn(),
refetchStatus: vi.fn(),
refetchRawSettings: vi.fn(),
updateConfigAsync: vi.fn(),
saveRawSettingsAsync: vi.fn(),
autoDetectAuthAsync: vi.fn(),
importManualAuthAsync: vi.fn(),
startDaemonAsync: vi.fn(),
stopDaemonAsync: vi.fn(),
}));
vi.mock('@/hooks/use-cursor', () => ({
useCursor: mocks.useCursor,
}));
const toastMocks = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn(),
}));
vi.mock('sonner', () => ({
toast: toastMocks,
}));
vi.mock('@/components/copilot/config-form/raw-editor-section', () => ({
RawEditorSection: () => <div>Raw Editor</div>,
}));
vi.mock('@/components/ui/searchable-select', () => ({
SearchableSelect: ({ value }: { value?: string }) => (
<div data-testid="searchable-select">{value ?? 'select'}</div>
),
}));
import { CursorPage } from '@/pages/cursor';
const probeFailure = {
ok: false,
stage: 'daemon',
status: 503,
duration_ms: 321,
error_type: 'daemon_not_running',
model: 'gpt-5.3-codex',
message: 'Cursor daemon is not running.',
};
function buildUseCursorResult(overrides: Record<string, unknown> = {}) {
return {
status: {
enabled: true,
authenticated: true,
auth_method: 'manual',
token_age: 1,
token_expired: false,
daemon_running: false,
port: 20129,
auto_start: false,
ghost_mode: true,
},
statusLoading: false,
refetchStatus: mocks.refetchStatus,
config: {
enabled: true,
port: 20129,
auto_start: false,
ghost_mode: true,
model: 'gpt-5.3-codex',
opus_model: 'gpt-5.1-codex-max',
sonnet_model: 'gpt-5.3-codex',
haiku_model: 'gpt-5-mini',
},
updateConfigAsync: mocks.updateConfigAsync,
isUpdatingConfig: false,
models: [{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', provider: 'openai' }],
modelsLoading: false,
currentModel: 'gpt-5.3-codex',
rawSettings: {
settings: {},
mtime: 100,
path: '/tmp/cursor.settings.json',
exists: true,
},
rawSettingsLoading: false,
refetchRawSettings: mocks.refetchRawSettings,
saveRawSettingsAsync: mocks.saveRawSettingsAsync,
isSavingRawSettings: false,
autoDetectAuthAsync: mocks.autoDetectAuthAsync,
isAutoDetectingAuth: false,
importManualAuthAsync: mocks.importManualAuthAsync,
isImportingManualAuth: false,
startDaemonAsync: mocks.startDaemonAsync,
isStartingDaemon: false,
stopDaemonAsync: mocks.stopDaemonAsync,
isStoppingDaemon: false,
runProbeAsync: mocks.runProbeAsync,
isRunningProbe: false,
probeResult: undefined,
resetProbe: mocks.resetProbe,
...overrides,
};
}
describe('CursorPage', () => {
beforeEach(() => {
mocks.runProbeAsync.mockReset();
mocks.resetProbe.mockReset();
mocks.runProbeAsync.mockResolvedValue(probeFailure);
mocks.useCursor.mockReturnValue(buildUseCursorResult());
toastMocks.error.mockReset();
toastMocks.success.mockReset();
});
it('renders persistent live probe results in the sidebar', async () => {
mocks.useCursor.mockReturnValue(
buildUseCursorResult({
probeResult: probeFailure,
})
);
render(<CursorPage />);
expect(screen.getByText('Live Probe')).toBeInTheDocument();
await waitFor(() => expect(screen.getByText('Probe failed')).toBeInTheDocument());
expect(screen.getByText('503')).toBeInTheDocument();
expect(screen.getByText('321 ms')).toBeInTheDocument();
expect(screen.getByText('Cursor daemon is not running.')).toBeInTheDocument();
expect(screen.getAllByText('gpt-5.3-codex').length).toBeGreaterThan(0);
});
it('runs the live probe from the sidebar action', async () => {
render(<CursorPage />);
await userEvent.click(screen.getByRole('button', { name: 'Run Live Probe' }));
await waitFor(() => expect(mocks.runProbeAsync).toHaveBeenCalledTimes(1));
});
it('blocks live probe runs while local edits are unsaved', async () => {
render(<CursorPage />);
await userEvent.click(screen.getByRole('tab', { name: 'Settings' }));
await userEvent.clear(screen.getByLabelText('Port'));
await userEvent.type(screen.getByLabelText('Port'), '20130');
await userEvent.click(screen.getByRole('button', { name: 'Run Live Probe' }));
expect(mocks.runProbeAsync).not.toHaveBeenCalled();
expect(toastMocks.error).toHaveBeenCalledWith(
'Save or discard Cursor changes before running the live probe'
);
});
});