mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* 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>
157 lines
5.0 KiB
TypeScript
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'
|
|
);
|
|
});
|
|
});
|