mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
fix(cursor): harden runtime follow-up regressions
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(/&/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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user