Merge pull request #971 from kaitranntt/kai/feat/966-cursor-integration-usable

feat: make Cursor integration usable and supportable
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-04-12 16:42:32 -04:00
commit a18146b83e
25 files changed
+2455 -319

No files matched your search

+14
View File
@@ -85,6 +85,20 @@ describe('npm CLI', () => {
assert(!output.includes("Profile '--verbose' not found"), 'Should not treat flags as profiles');
}
});
it('routes cursor probe through the cursor command handler', function() {
try {
execSync(`bun "${srcCcsPath}" cursor probe`, {
stdio: 'pipe',
timeout: 3000,
env: { ...process.env, CCS_HOME: testCcsHome }
});
} catch (e) {
const output = e.stderr?.toString() || e.stdout?.toString() || '';
assert(!output.includes("Profile 'cursor' not found"), 'Should not fall through to profile lookup');
assert(output.includes('Cursor Live Probe'), 'Should render the cursor probe command output');
}
});
});
describe('Profile handling', () => {
+179 -14
View File
@@ -3,6 +3,7 @@
*/
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -15,6 +16,7 @@ import {
checkAuthStatus,
deleteCredentials,
autoDetectTokens,
getTokenStorageCandidates,
} from '../../../src/cursor/cursor-auth';
// Test isolation
@@ -403,26 +405,40 @@ describe('deleteCredentials', () => {
});
describe('autoDetectTokens', () => {
it('should return not found for Windows platform', () => {
// Save original platform
it('should report checked paths when no Windows database exists', () => {
const originalPlatform = process.platform;
const originalUserProfile = process.env.USERPROFILE;
const originalAppData = process.env.APPDATA;
const originalLocalAppData = process.env.LOCALAPPDATA;
const fakeHome = path.join(tempDir, 'win-home');
process.env.USERPROFILE = fakeHome;
delete process.env.APPDATA;
delete process.env.LOCALAPPDATA;
// Mock Windows platform
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true,
});
const result = autoDetectTokens();
try {
const result = autoDetectTokens();
expect(result.found).toBe(false);
expect(result.error).toContain('not supported on Windows');
// Restore original platform
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
});
expect(result.found).toBe(false);
expect(result.reason).toBe('db_not_found');
expect(result.checkedPaths?.length).toBeGreaterThan(0);
expect(result.error).toContain('Checked:');
} finally {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
});
if (originalUserProfile !== undefined) process.env.USERPROFILE = originalUserProfile;
else delete process.env.USERPROFILE;
if (originalAppData !== undefined) process.env.APPDATA = originalAppData;
else delete process.env.APPDATA;
if (originalLocalAppData !== undefined) process.env.LOCALAPPDATA = originalLocalAppData;
else delete process.env.LOCALAPPDATA;
}
});
it('should return not found when database file does not exist', () => {
@@ -438,9 +454,10 @@ describe('autoDetectTokens', () => {
try {
const result = autoDetectTokens();
// Should fail because isolated test home has no Cursor database
expect(result.found).toBe(false);
expect(result.error).toBeDefined();
expect(result.reason).toBe('db_not_found');
expect(result.checkedPaths?.length).toBeGreaterThan(0);
expect(result.error).toContain('Checked:');
} finally {
if (originalHome !== undefined) {
process.env.HOME = originalHome;
@@ -457,4 +474,152 @@ describe('autoDetectTokens', () => {
expect(result).toHaveProperty('found');
expect(typeof result.found).toBe('boolean');
});
it('should report sqlite_unavailable when database exists but sqlite3 is missing', () => {
const originalHome = process.env.HOME;
const originalPath = process.env.PATH;
const originalSqliteBin = process.env.CCS_CURSOR_SQLITE_BIN;
const fakeHome = path.join(tempDir, 'sqlite-missing-home');
process.env.HOME = fakeHome;
process.env.CCS_CURSOR_SQLITE_BIN = 'definitely-missing-sqlite3';
try {
const dbPath = getTokenStorageCandidates()[0];
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
fs.writeFileSync(dbPath, '');
const result = autoDetectTokens();
expect(result.found).toBe(false);
expect(result.reason).toBe('sqlite_unavailable');
expect(result.dbPath).toBe(dbPath);
} finally {
if (originalHome !== undefined) process.env.HOME = originalHome;
else delete process.env.HOME;
if (originalPath !== undefined) process.env.PATH = originalPath;
else delete process.env.PATH;
if (originalSqliteBin !== undefined) process.env.CCS_CURSOR_SQLITE_BIN = originalSqliteBin;
else delete process.env.CCS_CURSOR_SQLITE_BIN;
}
});
it('should use fallback keys and normalize JSON-encoded sqlite values', () => {
if (process.platform === 'win32') return;
try {
execFileSync('sqlite3', ['--version'], { stdio: 'ignore' });
} catch {
return;
}
const originalHome = process.env.HOME;
const fakeHome = path.join(tempDir, 'sqlite-fallback-home');
process.env.HOME = fakeHome;
try {
const dbPath = getTokenStorageCandidates()[0];
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
execFileSync('sqlite3', [
dbPath,
'CREATE TABLE IF NOT EXISTS itemTable (key TEXT PRIMARY KEY, value TEXT);',
]);
execFileSync('sqlite3', [
dbPath,
`INSERT OR REPLACE INTO itemTable (key, value) VALUES ('cursorAuth/token', '"${'a'.repeat(60)}"');`,
]);
execFileSync('sqlite3', [
dbPath,
`INSERT OR REPLACE INTO itemTable (key, value) VALUES ('storage.machineId', '"1234567890abcdef1234567890abcdef"');`,
]);
const result = autoDetectTokens();
expect(result.found).toBe(true);
expect(result.accessToken).toBe('a'.repeat(60));
expect(result.machineId).toBe('1234567890abcdef1234567890abcdef');
} finally {
if (originalHome !== undefined) process.env.HOME = originalHome;
else delete process.env.HOME;
}
});
it('should continue scanning later database candidates after one invalid credential pair', () => {
if (process.platform !== 'darwin') {
return;
}
try {
execFileSync('sqlite3', ['--version'], { stdio: 'ignore' });
} catch {
return;
}
const originalHome = process.env.HOME;
const fakeHome = path.join(tempDir, 'candidate-scan-home');
process.env.HOME = fakeHome;
try {
const [stablePath, insidersPath] = getTokenStorageCandidates();
fs.mkdirSync(path.dirname(stablePath), { recursive: true });
fs.mkdirSync(path.dirname(insidersPath), { recursive: true });
execFileSync('sqlite3', [
stablePath,
'CREATE TABLE IF NOT EXISTS itemTable (key TEXT PRIMARY KEY, value TEXT);',
]);
execFileSync('sqlite3', [
stablePath,
`INSERT OR REPLACE INTO itemTable (key, value) VALUES ('cursorAuth/accessToken', 'short');`,
]);
execFileSync('sqlite3', [
stablePath,
`INSERT OR REPLACE INTO itemTable (key, value) VALUES ('storage.serviceMachineId', 'bad');`,
]);
execFileSync('sqlite3', [
insidersPath,
'CREATE TABLE IF NOT EXISTS itemTable (key TEXT PRIMARY KEY, value TEXT);',
]);
execFileSync('sqlite3', [
insidersPath,
`INSERT OR REPLACE INTO itemTable (key, value) VALUES ('cursorAuth/accessToken', '${'a'.repeat(60)}');`,
]);
execFileSync('sqlite3', [
insidersPath,
`INSERT OR REPLACE INTO itemTable (key, value) VALUES ('storage.serviceMachineId', '1234567890abcdef1234567890abcdef');`,
]);
const result = autoDetectTokens();
expect(result.found).toBe(true);
expect(result.dbPath).toBe(insidersPath);
} finally {
if (originalHome !== undefined) process.env.HOME = originalHome;
else delete process.env.HOME;
}
});
it('should report db_query_failed when the database exists but sqlite cannot query it', () => {
if (process.platform === 'win32') return;
try {
execFileSync('sqlite3', ['--version'], { stdio: 'ignore' });
} catch {
return;
}
const originalHome = process.env.HOME;
const fakeHome = path.join(tempDir, 'query-failed-home');
process.env.HOME = fakeHome;
try {
const dbPath = getTokenStorageCandidates()[0];
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
fs.writeFileSync(dbPath, 'not-a-sqlite-database');
const result = autoDetectTokens();
expect(result.found).toBe(false);
expect(result.reason).toBe('db_query_failed');
} finally {
if (originalHome !== undefined) process.env.HOME = originalHome;
else delete process.env.HOME;
}
});
});
+3
View File
@@ -484,6 +484,9 @@ describe('renderCursorHelp', () => {
expect(exitCode).toBe(0);
expect(logs.some((line) => line.includes('Usage: ccs cursor <subcommand>'))).toBe(true);
expect(logs.some((line) => line.includes('probe Run a live authenticated runtime probe'))).toBe(
true
);
expect(
logs.some((line) => line.includes('ccs cursor [claude args]'))
).toBe(true);
+232 -22
View File
@@ -742,14 +742,27 @@ describe('Message Translation', () => {
describe('Request Encoding', () => {
describe('generateCursorBody', () => {
it('should encode basic text message', () => {
it('should encode a raw top-level request protobuf for basic text messages', () => {
const result = generateCursorBody([{ role: 'user', content: 'Hello' }], 'gpt-4', [], null);
const topLevel = decodeMessage(result);
const requestPayload = topLevel.get(FIELD.Request.REQUEST)?.[0]?.value as Uint8Array;
const chatRequest = decodeMessage(requestPayload);
const encodedMessages = (chatRequest.get(FIELD.Chat.MESSAGES) || []).map((entry) =>
decodeMessage(entry.value as Uint8Array)
);
const decoder = new TextDecoder();
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBeGreaterThan(0);
expect(Array.from(result.slice(0, 5))).not.toEqual([0, 0, 0, 1, 107]);
expect(topLevel.has(FIELD.Request.REQUEST)).toBe(true);
expect(encodedMessages).toHaveLength(1);
expect(decoder.decode(encodedMessages[0].get(FIELD.Message.CONTENT)?.[0]?.value as Uint8Array)).toBe(
'Hello'
);
});
it('should encode message with tools', () => {
it('should encode message with tools into the raw request payload', () => {
const tools = [
{
type: 'function' as const,
@@ -773,9 +786,14 @@ describe('Request Encoding', () => {
tools,
null
);
const topLevel = decodeMessage(result);
const requestPayload = topLevel.get(FIELD.Request.REQUEST)?.[0]?.value as Uint8Array;
const chatRequest = decodeMessage(requestPayload);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBeGreaterThan(0);
expect(topLevel.has(FIELD.Request.REQUEST)).toBe(true);
expect((chatRequest.get(FIELD.Chat.MCP_TOOLS) || []).length).toBe(1);
});
it('should preserve flattened tool_result blocks through protobuf encoding', () => {
@@ -806,10 +824,7 @@ describe('Request Encoding', () => {
);
const body = generateCursorBody(translated.messages, 'gpt-4', [], null);
const frame = parseConnectRPCFrame(Buffer.from(body));
expect(frame).not.toBeNull();
const topLevel = decodeMessage(frame!.payload);
const topLevel = decodeMessage(body);
const requestPayload = topLevel.get(FIELD.Request.REQUEST)?.[0]?.value as Uint8Array;
const chatRequest = decodeMessage(requestPayload);
const encodedMessages = (chatRequest.get(FIELD.Chat.MESSAGES) || []).map((entry) =>
@@ -831,7 +846,7 @@ describe('Request Encoding', () => {
});
describe('Edge cases', () => {
it('should handle malformed frame gracefully', () => {
it('should reject malformed frame headers', async () => {
const executor = new CursorExecutor();
// Incomplete frame header (only 3 bytes instead of 5)
@@ -841,11 +856,13 @@ describe('Request Encoding', () => {
messages: [],
});
// Should return valid response even with malformed input
expect(result.status).toBe(200);
expect(result.status).toBe(502);
const body = JSON.parse(await result.text());
expect(body.error.type).toBe('server_error');
expect(body.error.message).toContain('Truncated Cursor ConnectRPC frame');
});
it('should handle truncated payload', () => {
it('should reject truncated payloads', async () => {
const executor = new CursorExecutor();
// Frame header says payload is 100 bytes but only 5 bytes follow
@@ -857,8 +874,10 @@ describe('Request Encoding', () => {
messages: [],
});
// Should handle gracefully
expect(result.status).toBe(200);
expect(result.status).toBe(502);
const body = JSON.parse(await result.text());
expect(body.error.type).toBe('server_error');
expect(body.error.message).toContain('Truncated Cursor ConnectRPC frame');
});
it('should handle multi-frame buffer', () => {
@@ -1042,6 +1061,29 @@ describe('CursorExecutor', () => {
expect(body.error.type).toBe('rate_limit_error');
});
it('should map unavailable end-stream errors to 503', async () => {
const unavailableFrame = buildFrame(
new TextEncoder().encode(
JSON.stringify({
error: {
code: 'unavailable',
message: 'upstream down',
},
})
),
0x02
);
const result = executor.transformProtobufToJSON(unavailableFrame, 'gpt-4', {
messages: [],
});
expect(result.status).toBe(503);
const body = JSON.parse(await result.text());
expect(body.error.type).toBe('api_error');
expect(body.error.message).toContain('upstream down');
});
it('should surface reasoning_content when thinking payload is present', async () => {
const textContent = 'Final answer';
const thinkingContent = 'Internal reasoning trail';
@@ -1177,7 +1219,7 @@ describe('CursorExecutor', () => {
});
describe('decompressPayload error handling', () => {
it('should return empty buffer on decompression failure', () => {
it('returns an explicit executor error on decompression failure', async () => {
// Create invalid gzip data
const invalidGzip = Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0xff, 0xff]);
const frame = new Uint8Array(5 + invalidGzip.length);
@@ -1192,13 +1234,15 @@ describe('CursorExecutor', () => {
messages: [],
});
// Should handle gracefully and return valid response
expect(result.status).toBe(200);
expect(result.status).toBe(502);
const body = JSON.parse(await result.text());
expect(body.error.type).toBe('server_error');
expect(body.error.message).toContain('decompress');
});
});
describe('error handling', () => {
it('should return empty buffer on decompression failure', () => {
it('returns an explicit error for invalid compressed payloads', async () => {
const executor = new CursorExecutor();
// Invalid compressed payload (not actually gzipped)
@@ -1217,13 +1261,80 @@ describe('CursorExecutor', () => {
const buffer = Buffer.from(frame);
// Should not crash - decompression failure returns empty buffer
const result = executor.transformProtobufToJSON(buffer, 'test-model', {
messages: [],
stream: false,
});
expect(result.status).toBe(502);
const body = JSON.parse(await result.text());
expect(body.error.type).toBe('server_error');
expect(body.error.message).toContain('decompress');
});
it('surfaces first-frame protocol errors in SSE mode without pretending success', async () => {
const executor = new CursorExecutor();
const invalidGzipPayload = new Uint8Array([1, 2, 3, 4, 5]);
const frame = buildFrame(invalidGzipPayload, 0x01);
const result = executor.transformProtobufToSSE(frame, 'test-model', {
messages: [],
stream: true,
});
expect(result.status).toBe(502);
const body = JSON.parse(await result.text());
expect(body.error.type).toBe('server_error');
expect(body.error.message).toContain('decompress');
});
it('emits an SSE error event without [DONE] when a later frame fails', async () => {
const executor = new CursorExecutor();
const combined = Buffer.concat([
buildTextFrame('Before failure'),
buildFrame(new Uint8Array([1, 2, 3, 4, 5]), 0x01),
]);
const result = executor.transformProtobufToSSE(combined, 'test-model', {
messages: [],
stream: true,
});
expect(result.status).toBe(200);
const body = await result.text();
expect(body).toContain('Before failure');
expect(body).toContain('event: error');
expect(body).toContain('"type":"server_error"');
expect(body).not.toContain('data: [DONE]');
});
it('emits an SSE error event when trailing bytes leave a truncated frame', async () => {
const executor = new CursorExecutor();
const combined = Buffer.concat([buildTextFrame('Partial success'), Buffer.from([0x00, 0x00, 0x00])]);
const result = executor.transformProtobufToSSE(combined, 'test-model', {
messages: [],
stream: true,
});
expect(result.status).toBe(200);
const body = await result.text();
expect(body).toContain('Partial success');
expect(body).toContain('event: error');
expect(body).toContain('Truncated Cursor ConnectRPC frame');
expect(body).not.toContain('data: [DONE]');
});
it('returns an explicit error for unknown ConnectRPC frame flags', async () => {
const executor = new CursorExecutor();
const result = executor.transformProtobufToJSON(buildFrame(new Uint8Array([0x01]), 0x04), 'gpt-4', {
messages: [],
});
expect(result.status).toBe(502);
const body = JSON.parse(await result.text());
expect(body.error.type).toBe('server_error');
expect(body.error.message).toContain('0x04');
});
it('should log unknown message roles in debug mode', () => {
@@ -1418,6 +1529,55 @@ describe('StreamingFrameParser', () => {
}
});
it('should surface end-stream JSON errors instead of treating them as gzip', () => {
const parser = new StreamingFrameParser();
const endStreamError = buildFrame(
new TextEncoder().encode(
JSON.stringify({
error: {
code: 'invalid_argument',
message: 'parse binary: illegal tag: field no 0 wire type 0',
},
})
),
0x02
);
const results = parser.push(endStreamError);
expect(results.length).toBe(1);
expect(results[0].type).toBe('error');
if (results[0].type === 'error') {
expect(results[0].status).toBe(400);
expect(results[0].errorType).toBe('api_error');
expect(results[0].message).toContain('illegal tag');
}
});
it('should map unavailable end-stream errors to 503 in the parser', () => {
const parser = new StreamingFrameParser();
const endStreamError = buildFrame(
new TextEncoder().encode(
JSON.stringify({
error: {
code: 'unavailable',
message: 'upstream down',
},
})
),
0x02
);
const results = parser.push(endStreamError);
expect(results.length).toBe(1);
expect(results[0].type).toBe('error');
if (results[0].type === 'error') {
expect(results[0].status).toBe(503);
expect(results[0].errorType).toBe('api_error');
}
});
it('should parse thinking frames', () => {
const parser = new StreamingFrameParser();
const frame = buildThinkingFrame('Think step by step');
@@ -1458,11 +1618,39 @@ describe('StreamingFrameParser', () => {
expect(results.length).toBe(1);
expect(results[0].type).toBe('error');
if (results[0].type === 'error') {
expect(results[0].status).toBe(502);
expect(results[0].errorType).toBe('server_error');
expect(results[0].message).toContain('Malformed protobuf response');
}
});
it('should reject invalid gzip-compressed frames explicitly', () => {
const parser = new StreamingFrameParser();
const invalidCompressedFrame = buildFrame(new Uint8Array([1, 2, 3, 4, 5]), 0x01);
const results = parser.push(invalidCompressedFrame);
expect(results.length).toBe(1);
expect(results[0].type).toBe('error');
if (results[0].type === 'error') {
expect(results[0].status).toBe(502);
expect(results[0].errorType).toBe('server_error');
expect(results[0].message).toContain('decompress');
}
});
it('should reject unknown ConnectRPC frame flag bits', () => {
const parser = new StreamingFrameParser();
const results = parser.push(buildFrame(new Uint8Array([0x01]), 0x04));
expect(results.length).toBe(1);
expect(results[0].type).toBe('error');
if (results[0].type === 'error') {
expect(results[0].status).toBe(502);
expect(results[0].errorType).toBe('server_error');
expect(results[0].message).toContain('0x04');
}
});
it('should report hasPartial() correctly', () => {
const parser = new StreamingFrameParser();
expect(parser.hasPartial()).toBe(false);
@@ -1481,6 +1669,20 @@ describe('StreamingFrameParser', () => {
parser2.push(emptyFrame);
expect(parser2.hasPartial()).toBe(false);
});
it('should surface truncated trailing bytes when the stream finishes', () => {
const parser = new StreamingFrameParser();
parser.push(Buffer.from([0x00, 0x00, 0x00]));
const results = parser.finish();
expect(results.length).toBe(1);
expect(results[0].type).toBe('error');
if (results[0].type === 'error') {
expect(results[0].status).toBe(502);
expect(results[0].message).toContain('Truncated Cursor ConnectRPC frame');
}
});
});
describe('decompressPayload', () => {
@@ -1496,10 +1698,18 @@ describe('decompressPayload', () => {
expect(result).toEqual(errorPayload);
});
it('should return empty buffer on invalid gzip data', () => {
it('should throw on invalid gzip data', () => {
const invalidGzip = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]);
const result = decompressPayload(invalidGzip, 0x01);
expect(result.length).toBe(0);
expect(() => decompressPayload(invalidGzip, 0x01)).toThrow(
'Failed to decompress Cursor ConnectRPC frame.'
);
});
it('should throw on unknown ConnectRPC frame flags', () => {
const payload = Buffer.from('payload');
expect(() => decompressPayload(payload, 0x04)).toThrow(
'Unsupported ConnectRPC frame flags: 0x04'
);
});
it('should decompress valid gzip payload', () => {
@@ -1511,12 +1721,12 @@ describe('decompressPayload', () => {
expect(result.toString()).toBe('Hello compressed world');
});
it('should handle GZIP_ALT and GZIP_BOTH flags', () => {
it('should not decompress plain end-stream trailers and should handle compressed end-stream trailers', () => {
const zlib = require('zlib');
const original = Buffer.from('test data');
const compressed = zlib.gzipSync(original);
expect(decompressPayload(compressed, 0x02).toString()).toBe('test data');
expect(decompressPayload(original, 0x02)).toEqual(original);
expect(decompressPayload(compressed, 0x03).toString()).toBe('test data');
});
});
@@ -0,0 +1,132 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as http from 'http';
import * as os from 'os';
import * as path from 'path';
import type { CursorConfig } from '../../../src/config/unified-config-types';
let tempDir = '';
let originalCcsHome: string | undefined;
let originalFetch: typeof globalThis.fetch;
let setGlobalConfigDir: (dir: string | undefined) => void;
let saveCredentials: (credentials: {
accessToken: string;
machineId: string;
authMethod: 'manual' | 'auto-detect';
importedAt: string;
}) => void;
let deleteCredentials: () => boolean;
let probeCursorRuntime: (config: CursorConfig) => Promise<{
ok: boolean;
stage: 'config' | 'auth' | 'daemon' | 'runtime';
status: number;
duration_ms: number;
error_type?: string | null;
message: string;
}>;
beforeEach(async () => {
originalCcsHome = process.env.CCS_HOME;
originalFetch = globalThis.fetch;
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cursor-runtime-probe-test-'));
process.env.CCS_HOME = tempDir;
const configManager = await import('../../../src/utils/config-manager');
setGlobalConfigDir = configManager.setGlobalConfigDir;
setGlobalConfigDir(undefined);
const cursorAuth = await import('../../../src/cursor/cursor-auth');
saveCredentials = cursorAuth.saveCredentials;
deleteCredentials = cursorAuth.deleteCredentials;
const runtimeProbe = await import(
`../../../src/cursor/cursor-runtime-probe?cursor-runtime-probe-test=${Date.now()}`
);
probeCursorRuntime = runtimeProbe.probeCursorRuntime;
deleteCredentials();
});
afterEach(() => {
globalThis.fetch = originalFetch;
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
setGlobalConfigDir(undefined);
if (tempDir && fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('probeCursorRuntime', () => {
it('classifies daemon connection races as daemon-stage failures', async () => {
const port = 23000 + Math.floor(Math.random() * 2000);
saveCredentials({
accessToken: 'a'.repeat(60),
machineId: '1234567890abcdef1234567890abcdef',
authMethod: 'manual',
importedAt: new Date().toISOString(),
});
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ service: 'cursor-daemon' }));
setImmediate(() => {
server.close();
});
return;
}
res.writeHead(404);
res.end();
});
await new Promise<void>((resolve) => server.listen(port, '127.0.0.1', () => resolve()));
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input);
if (url.startsWith('https://api2.cursor.sh')) {
return new Response(
JSON.stringify({
models: [{ name: 'gpt-5.3-codex' }],
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
);
}
if (url.startsWith(`http://127.0.0.1:${port}/v1/chat/completions`)) {
const error = new Error('fetch failed') as Error & { cause?: { code: string } };
error.cause = { code: 'ECONNREFUSED' };
throw error;
}
throw new Error(`Unexpected fetch: ${url}`);
}) as typeof globalThis.fetch;
const result = await probeCursorRuntime({
enabled: true,
port,
auto_start: false,
ghost_mode: true,
model: 'gpt-5.3-codex',
} as CursorConfig);
expect(result.ok).toBe(false);
expect(result.stage).toBe('daemon');
expect(result.status).toBe(503);
expect(result.error_type).toBe('daemon_unreachable');
expect(result.message).toContain('unreachable');
});
});
+121 -1
View File
@@ -55,6 +55,15 @@ let getTokenStoragePath: () => string;
let getDaemonStartPreconditionError: (
input: { enabled: boolean; authenticated: boolean; tokenExpired?: boolean }
) => { status: number; error: string } | null;
let getAutoDetectFailureStatus: (
reason?:
| 'db_not_found'
| 'sqlite_unavailable'
| 'db_query_failed'
| 'access_token_not_found'
| 'machine_id_not_found'
| 'invalid_token_format'
) => number;
function seedCursorConfig(overrides: {
enabled?: boolean;
@@ -107,6 +116,7 @@ beforeAll(async () => {
const cursorRoutesModule = await import('../../../src/web-server/routes/cursor-routes');
getDaemonStartPreconditionError = cursorRoutesModule.getDaemonStartPreconditionError;
getAutoDetectFailureStatus = cursorRoutesModule.getAutoDetectFailureStatus;
const app = express();
app.use(express.json());
@@ -207,6 +217,10 @@ describe('Cursor Routes Logic', () => {
});
describe('HTTP contracts', () => {
it('maps invalid_token_format auto-detect failures to HTTP 400', () => {
expect(getAutoDetectFailureStatus('invalid_token_format')).toBe(400);
});
it('GET /api/cursor/status returns current state', async () => {
const res = await fetch(`${baseUrl}/api/cursor/status`);
expect(res.status).toBe(200);
@@ -280,9 +294,11 @@ describe('Cursor Routes Logic', () => {
});
expect(res.status).toBe(404);
const json = (await res.json()) as { error?: string };
const json = (await res.json()) as { error?: string; reason?: string };
expect(typeof json.error).toBe('string');
expect(json.error?.length).toBeGreaterThan(0);
expect(json.reason).toBe('db_not_found');
expect(json.error).not.toContain(isolatedHome);
} finally {
if (originalHome !== undefined) {
process.env.HOME = originalHome;
@@ -344,6 +360,87 @@ describe('Cursor Routes Logic', () => {
}
});
it('POST /api/cursor/auth/auto-detect returns 503 when sqlite3 is unavailable', async () => {
if (process.platform === 'win32') {
return;
}
const originalHome = process.env.HOME;
const originalPath = process.env.PATH;
const originalSqliteBin = process.env.CCS_CURSOR_SQLITE_BIN;
const fakeHome = path.join(tempDir, 'sqlite-missing-home');
process.env.HOME = fakeHome;
process.env.CCS_CURSOR_SQLITE_BIN = 'definitely-missing-sqlite3';
try {
const dbPath = getTokenStoragePath();
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
fs.writeFileSync(dbPath, '');
const res = await fetch(`${baseUrl}/api/cursor/auth/auto-detect`, {
method: 'POST',
});
expect(res.status).toBe(503);
const json = (await res.json()) as { reason?: string; db_path?: string };
expect(json.reason).toBe('sqlite_unavailable');
expect(json.db_path).toBeUndefined();
} finally {
if (originalHome !== undefined) {
process.env.HOME = originalHome;
} else {
delete process.env.HOME;
}
if (originalPath !== undefined) {
process.env.PATH = originalPath;
} else {
delete process.env.PATH;
}
if (originalSqliteBin !== undefined) {
process.env.CCS_CURSOR_SQLITE_BIN = originalSqliteBin;
} else {
delete process.env.CCS_CURSOR_SQLITE_BIN;
}
}
});
it('POST /api/cursor/auth/auto-detect returns 500 when the database exists but cannot be queried', async () => {
if (process.platform === 'win32') {
return;
}
const sqliteCheck = spawnSync('sqlite3', ['--version'], { stdio: 'ignore' });
if (sqliteCheck.status !== 0) {
return;
}
const originalHome = process.env.HOME;
const fakeHome = path.join(tempDir, 'query-failed-home');
process.env.HOME = fakeHome;
try {
const dbPath = getTokenStoragePath();
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
fs.writeFileSync(dbPath, 'not-a-sqlite-database');
const res = await fetch(`${baseUrl}/api/cursor/auth/auto-detect`, {
method: 'POST',
});
expect(res.status).toBe(500);
const json = (await res.json()) as { reason?: string; db_path?: string };
expect(json.reason).toBe('db_query_failed');
expect(json.db_path).toBeUndefined();
} finally {
if (originalHome !== undefined) {
process.env.HOME = originalHome;
} else {
delete process.env.HOME;
}
}
});
it('POST /api/cursor/daemon/start returns 400 when integration is disabled', async () => {
seedCursorConfig({ enabled: false });
seedCredentials(false);
@@ -412,5 +509,28 @@ describe('Cursor Routes Logic', () => {
expect(json.models.length).toBeGreaterThan(0);
expect(json.current).toBe('gpt-5.3-codex');
});
it('POST /api/cursor/probe returns auth failure when credentials are missing', async () => {
const res = await fetch(`${baseUrl}/api/cursor/probe`, { method: 'POST' });
expect(res.status).toBe(401);
const json = (await res.json()) as { ok?: boolean; stage?: string; error_type?: string };
expect(json.ok).toBe(false);
expect(json.stage).toBe('auth');
expect(json.error_type).toBe('authentication_error');
});
it('POST /api/cursor/probe returns daemon failure when daemon is down and auto_start is false', async () => {
seedCursorConfig({ enabled: true, auto_start: false });
seedCredentials(false);
const res = await fetch(`${baseUrl}/api/cursor/probe`, { method: 'POST' });
expect(res.status).toBe(503);
const json = (await res.json()) as { ok?: boolean; stage?: string; error_type?: string };
expect(json.ok).toBe(false);
expect(json.stage).toBe('daemon');
expect(json.error_type).toBe('daemon_not_running');
});
});
});