Files
hapi/cli/src/cursor/runCursor.test.ts
T
fdd286138a fix(cli): remap stale Cursor ACP model wires on resume (grok-4.5[fast=…] → cursor-grok-4.5-*) (#1271)
* fix(cli): remap stale Cursor grok wires on ACP resume (#1270)

When hub sessions still store legacy grok-4.5[fast=…] wires, remap to live
cursor-grok-4.5-* catalog ids before spawn and retry once on model_not_found.
Keeps #1198 honest errors when remap cannot find a candidate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): address HAPI bot review on grok wire remap (#1271)

Stop Available-models parsing at newline/Tip; remap legacy wires even when
stale id remains in mixed availableModels+cliModelSkus cache.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(shared): rank catalog SKUs by fast hint before effort score

When medium-fast is absent, grok-4.5[fast=true] must not lose to slow
medium just because default effort scoring double-counts medium.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): stderr remap fallback + queued model sync (#1271)

Retry model_not_found remaps on the original legacy wire when cache
pre-resolution picked a stale SKU; enqueue user turns from session model.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(shared): reject unavailable SKU variants without ACP wires

matchCliSkuToAcpWireId no longer nearest-matches same-base CLI SKUs
when no wire exists; legacy grok remap stays on remapStaleCursorModelId.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): suppress transient model rejection on remap retry

Defer surfacing Cannot use this model stderr until initialize/load
retry fails; success path no longer shows a false error in chat.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 14:25:51 +01:00

157 lines
5.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const mockCursorSession = vi.hoisted(() => ({
setPermissionMode: vi.fn(),
setModel: vi.fn(),
getModel: vi.fn(() => undefined as string | undefined),
pushKeepAlive: vi.fn(),
stopKeepAlive: vi.fn(),
canApplyModelConfig: vi.fn(() => false)
}));
const harness = vi.hoisted(() => ({
bootstrapArgs: [] as Array<Record<string, unknown>>,
loopArgs: [] as Array<Record<string, unknown>>,
loopError: null as Error | null,
session: {
onUserMessage: vi.fn(),
onCancelQueuedMessage: vi.fn(),
sendSessionEvent: vi.fn(),
rpcHandlerManager: {
registerHandler: vi.fn()
}
},
metadata: { sessionId: 'cursor-session-1' }
}));
vi.mock('@/agent/sessionFactory', () => ({
bootstrapSession: vi.fn(async (options: Record<string, unknown>) => {
harness.bootstrapArgs.push(options);
return {
api: {},
session: harness.session,
metadata: harness.metadata
};
}),
bootstrapExistingSession: vi.fn(async (options: Record<string, unknown>) => {
harness.bootstrapArgs.push(options);
return {
api: {},
session: harness.session,
metadata: harness.metadata
};
})
}));
vi.mock('./loop', () => ({
loop: vi.fn(async (options: Record<string, unknown>) => {
harness.loopArgs.push(options);
if (harness.loopError) {
throw harness.loopError;
}
const onSessionReady = options.onSessionReady as ((session: unknown) => void) | undefined;
onSessionReady?.(mockCursorSession);
})
}));
vi.mock('@/claude/registerKillSessionHandler', () => ({
registerKillSessionHandler: vi.fn()
}));
const lifecycleMock = vi.hoisted(() => ({
registerProcessHandlers: vi.fn(),
cleanupAndExit: vi.fn(async () => {}),
markCrash: vi.fn(),
setExitCode: vi.fn(),
setArchiveReason: vi.fn(),
setSessionEndReason: vi.fn()
}));
vi.mock('@/agent/runnerLifecycle', () => ({
createModeChangeHandler: vi.fn(() => vi.fn()),
createRunnerLifecycle: vi.fn(() => lifecycleMock),
setControlledByUser: vi.fn()
}));
vi.mock('@/agent/localHandoff', () => ({
registerLocalHandoffHandler: vi.fn()
}));
vi.mock('@/ui/logger', () => ({
logger: {
debug: vi.fn()
}
}));
vi.mock('@/utils/attachmentFormatter', () => ({
formatMessageWithAttachments: vi.fn((text: string) => text)
}));
vi.mock('./cursorUserMessageQueue', () => ({
enqueueCursorUserMessage: vi.fn()
}));
import { runCursor } from './runCursor';
import { enqueueCursorUserMessage } from './cursorUserMessageQueue';
describe('runCursor', () => {
beforeEach(() => {
harness.bootstrapArgs.length = 0;
harness.loopArgs.length = 0;
harness.loopError = null;
harness.session.onUserMessage.mockReset();
harness.session.onCancelQueuedMessage.mockReset();
harness.session.sendSessionEvent.mockReset();
harness.session.rpcHandlerManager.registerHandler.mockReset();
lifecycleMock.registerProcessHandlers.mockClear();
lifecycleMock.cleanupAndExit.mockClear();
lifecycleMock.markCrash.mockClear();
lifecycleMock.setExitCode.mockClear();
lifecycleMock.setArchiveReason.mockClear();
lifecycleMock.setSessionEndReason.mockClear();
mockCursorSession.getModel.mockReset();
mockCursorSession.getModel.mockReturnValue(undefined);
vi.mocked(enqueueCursorUserMessage).mockReset();
});
it('surfaces loop-level ACP failures to the web UI before archiving', async () => {
harness.loopError = new Error('WritableIterable is closed');
await runCursor({ startedBy: 'runner' });
expect(harness.session.sendSessionEvent).toHaveBeenCalledWith({
type: 'message',
message: 'Cursor Agent failed: WritableIterable is closed'
});
expect(lifecycleMock.markCrash).toHaveBeenCalledWith(harness.loopError);
expect(lifecycleMock.setSessionEndReason).not.toHaveBeenCalledWith('completed');
expect(lifecycleMock.cleanupAndExit).toHaveBeenCalled();
});
it('queues user turns with the live session model after startup remap', async () => {
mockCursorSession.getModel.mockReturnValue('cursor-grok-4.5-medium');
await runCursor({
startedBy: 'runner',
model: 'grok-4.5[fast=false]'
});
const userMessageHandler = harness.session.onUserMessage.mock.calls[0]?.[0] as
| ((msg: { content: { text: string } }, localId?: string) => void)
| undefined;
expect(userMessageHandler).toBeDefined();
userMessageHandler!({ content: { text: 'hello' } }, 'local-1');
expect(enqueueCursorUserMessage).toHaveBeenCalledWith(
expect.anything(),
'hello',
{
permissionMode: 'default',
model: 'cursor-grok-4.5-medium'
},
'local-1'
);
});
});