fix(cursor): harden runtime follow-up regressions

This commit is contained in:
Tam Nhu Tran
2026-04-02 03:21:38 -04:00
parent 2d67f40175
commit f7ddad6c19
7 changed files with 126 additions and 32 deletions
+2 -3
View File
@@ -21,7 +21,7 @@ import {
CompositeVariantConfig,
CompositeTierConfig,
} from '../config/unified-config-types';
import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loader';
import { loadUnifiedConfig, isUnifiedMode, getCursorConfig } from '../config/unified-config-loader';
import { getCcsDir } from '../utils/config-manager';
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../utils/profile-compat';
import type { CLIProxyProvider } from '../cliproxy/types';
@@ -308,8 +308,7 @@ class ProfileDetector {
// Priority 0.75: Check Cursor profile - local Cursor daemon runtime
if (profileName === 'cursor') {
const unifiedConfig = this.readUnifiedConfig();
const cursorConfig = unifiedConfig?.cursor;
const cursorConfig = getCursorConfig();
if (!cursorConfig?.enabled) {
const error = new Error(
+2 -1
View File
@@ -119,7 +119,8 @@ function escapeXml(text: string): string {
}
function buildToolResultBlock(toolName: string, toolCallId: string, resultText: string): string {
const cleanResult = truncateToolResultText(escapeXml(sanitizeToolResultText(resultText)));
// Truncate raw tool output before XML escaping so the cap reflects original content.
const cleanResult = escapeXml(truncateToolResultText(sanitizeToolResultText(resultText)));
return [
'<tool_result>',
@@ -396,6 +396,16 @@ function resolveBackend(
};
}
if (profileType === 'cursor' || profileName === 'cursor') {
return {
backendId: null,
backendDisplayName: null,
resolutionSource: 'unresolved',
reason:
'Cursor image analysis does not inherit the global fallback backend. Set image_analysis.profile_backends.cursor to an explicit provider to enable transformer-backed image analysis.',
};
}
if (profileType === 'copilot' || profileName === 'copilot') {
const backendId = normalizeImageAnalysisBackendId('ghcp', Object.keys(config.provider_models));
return {
+42 -26
View File
@@ -197,21 +197,14 @@ describe('ProfileDetector', () => {
});
it('should detect cursor as a first-class runtime profile when enabled', () => {
const mockUnifiedConfig = {
version: 2,
cursor: {
enabled: true,
port: 20129,
auto_start: true,
ghost_mode: true,
model: 'gpt-5.3-codex',
},
};
const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true);
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(
mockUnifiedConfig as any
);
const getCursorConfigSpy = spyOn(unifiedConfigLoader, 'getCursorConfig').mockReturnValue({
enabled: true,
port: 20129,
auto_start: true,
ghost_mode: true,
model: 'gpt-5.3-codex',
});
try {
const result = detector.detectProfileType('cursor');
@@ -220,32 +213,55 @@ describe('ProfileDetector', () => {
expect(result.cursorConfig?.auto_start).toBe(true);
} finally {
isUnifiedModeSpy.mockRestore();
loadUnifiedConfigSpy.mockRestore();
getCursorConfigSpy.mockRestore();
}
});
it('should throw a helpful error when cursor profile is disabled', () => {
const mockUnifiedConfig = {
version: 2,
cursor: {
enabled: false,
it('should merge default cursor fields when enabled via partial unified config', () => {
const originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempDir;
const ccsDir = path.join(tempDir, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
['version: 12', 'cursor:', ' enabled: true'].join('\n')
);
try {
const localDetector = new ProfileDetector();
const result = localDetector.detectProfileType('cursor');
expect(result.type).toBe('cursor');
expect(result.cursorConfig).toEqual({
enabled: true,
port: 20129,
auto_start: false,
ghost_mode: true,
model: 'gpt-5.3-codex',
},
};
});
} finally {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
}
});
it('should throw a helpful error when cursor profile is disabled', () => {
const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true);
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(
mockUnifiedConfig as any
);
const getCursorConfigSpy = spyOn(unifiedConfigLoader, 'getCursorConfig').mockReturnValue({
enabled: false,
port: 20129,
auto_start: false,
ghost_mode: true,
model: 'gpt-5.3-codex',
});
try {
expect(() => detector.detectProfileType('cursor')).toThrow(/Cursor profile is not enabled/);
} finally {
isUnifiedModeSpy.mockRestore();
loadUnifiedConfigSpy.mockRestore();
getCursorConfigSpy.mockRestore();
}
});
});
@@ -6,6 +6,7 @@ import * as path from 'path';
import {
executeCursorProfile,
generateCursorEnv,
resolveCursorImageAnalysisEnv,
} from '../../../src/cursor/cursor-profile-executor';
import { saveCredentials } from '../../../src/cursor/cursor-auth';
import type { CursorConfig } from '../../../src/config/unified-config-types';
@@ -57,6 +58,14 @@ describe('cursor-profile-executor', () => {
expect(env.CLAUDE_CONFIG_DIR).toBe('/tmp/claude-config');
});
it('skips image-analysis provider routing for cursor unless explicitly mapped', async () => {
const { env, warning } = await resolveCursorImageAnalysisEnv();
expect(env.CCS_CURRENT_PROVIDER).toBe('');
expect(env.CCS_IMAGE_ANALYSIS_SKIP).toBe('1');
expect(warning).toBeNull();
});
it('fails fast when Cursor integration is disabled', async () => {
const exitCode = await executeCursorProfile({ ...BASE_CONFIG, enabled: false }, []);
expect(exitCode).toBe(1);
+23 -2
View File
@@ -22,6 +22,25 @@ import { CursorExecutor } from '../../../src/cursor/cursor-executor';
import { WIRE_TYPE, FIELD } from '../../../src/cursor/cursor-protobuf-schema';
import { StreamingFrameParser, decompressPayload } from '../../../src/cursor/cursor-stream-parser';
const MAX_TOOL_RESULT_CHARS = 12_000;
function computeExpectedToolResultOmittedChars(textLength: number): number {
if (textLength <= MAX_TOOL_RESULT_CHARS) {
return 0;
}
let omittedChars = textLength - MAX_TOOL_RESULT_CHARS;
while (true) {
const suffix = `\n[truncated ${omittedChars} chars]`;
const keepLength = Math.max(MAX_TOOL_RESULT_CHARS - suffix.length, 0);
const nextOmittedChars = textLength - keepLength;
if (nextOmittedChars === omittedChars) {
return omittedChars;
}
omittedChars = nextOmittedChars;
}
}
describe('Protobuf Encoding/Decoding', () => {
describe('encodeVarint / decodeVarint round-trip', () => {
it('should encode and decode 0', () => {
@@ -555,6 +574,8 @@ describe('Message Translation', () => {
it('should truncate oversized tool result payloads', () => {
const oversizedResult = '&'.repeat(12_050);
const omittedChars = computeExpectedToolResultOmittedChars(oversizedResult.length);
const preservedChars = oversizedResult.length - omittedChars;
const result = buildCursorRequest(
'gpt-4',
{
@@ -583,11 +604,11 @@ describe('Message Translation', () => {
expect(result.messages).toHaveLength(2);
expect(result.messages[1].content).toContain('[truncated ');
expect(result.messages[1].content.length).toBeLessThan(12_250);
expect(result.messages[1].content).toContain(`[truncated ${omittedChars} chars]`);
const resultMatch = result.messages[1].content.match(/<result>([\s\S]*)<\/result>/);
expect(resultMatch).not.toBeNull();
expect(resultMatch?.[1].length).toBeLessThanOrEqual(12_000);
expect((resultMatch?.[1].match(/&amp;/g) ?? []).length).toBe(preservedChars);
});
it('should mark unserializable structured tool results explicitly', () => {
@@ -44,6 +44,44 @@ describe('image-analysis-backend-resolver', () => {
expect(status.resolutionSource).toBe('copilot-alias');
});
it('does not route cursor image analysis through the fallback backend by default', () => {
const status = resolveImageAnalysisStatus(
{
profileName: 'cursor',
profileType: 'cursor',
},
DEFAULT_IMAGE_ANALYSIS_CONFIG
);
expect(status.supported).toBe(false);
expect(status.backendId).toBeNull();
expect(status.status).toBe('skipped');
expect(status.resolutionSource).toBe('unresolved');
expect(status.reason).toContain('profile_backends.cursor');
});
it('allows cursor image analysis only when explicitly mapped to a backend', () => {
const config: ImageAnalysisConfig = {
...DEFAULT_IMAGE_ANALYSIS_CONFIG,
profile_backends: {
cursor: 'ghcp',
},
};
const status = resolveImageAnalysisStatus(
{
profileName: 'cursor',
profileType: 'cursor',
},
config
);
expect(status.supported).toBe(true);
expect(status.backendId).toBe('ghcp');
expect(status.status).toBe('mapped');
expect(status.resolutionSource).toBe('profile-backend');
});
it('uses the fallback backend for an unmapped third-party settings profile', () => {
const status = resolveImageAnalysisStatus(
{