Address b/424256913 - fixed error in correctStringEscaping() and improved backslash handling (#1007)
This commit is contained in:
parent
3bcb3c3666
commit
93909a2dd3
|
@ -32,6 +32,7 @@ vi.mock('../core/client.js', () => ({
|
||||||
import {
|
import {
|
||||||
countOccurrences,
|
countOccurrences,
|
||||||
ensureCorrectEdit,
|
ensureCorrectEdit,
|
||||||
|
ensureCorrectFileContent,
|
||||||
unescapeStringForGeminiBug,
|
unescapeStringForGeminiBug,
|
||||||
resetEditCorrectorCaches_TEST_ONLY,
|
resetEditCorrectorCaches_TEST_ONLY,
|
||||||
} from './editCorrector.js';
|
} from './editCorrector.js';
|
||||||
|
@ -102,7 +103,7 @@ describe('editCorrector', () => {
|
||||||
'First line\nSecond line',
|
'First line\nSecond line',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should handle multiple backslashes before an escapable character', () => {
|
it('should handle multiple backslashes before an escapable character (aggressive unescaping)', () => {
|
||||||
expect(unescapeStringForGeminiBug('\\\\n')).toBe('\n');
|
expect(unescapeStringForGeminiBug('\\\\n')).toBe('\n');
|
||||||
expect(unescapeStringForGeminiBug('\\\\\\t')).toBe('\t');
|
expect(unescapeStringForGeminiBug('\\\\\\t')).toBe('\t');
|
||||||
expect(unescapeStringForGeminiBug('\\\\\\\\`')).toBe('`');
|
expect(unescapeStringForGeminiBug('\\\\\\\\`')).toBe('`');
|
||||||
|
@ -126,6 +127,21 @@ describe('editCorrector', () => {
|
||||||
unescapeStringForGeminiBug('\\\\\\\nLine1\\\nLine2\\tTab\\\\`Tick\\"'),
|
unescapeStringForGeminiBug('\\\\\\\nLine1\\\nLine2\\tTab\\\\`Tick\\"'),
|
||||||
).toBe('\nLine1\nLine2\tTab`Tick"');
|
).toBe('\nLine1\nLine2\tTab`Tick"');
|
||||||
});
|
});
|
||||||
|
it('should handle escaped backslashes', () => {
|
||||||
|
expect(unescapeStringForGeminiBug('\\\\')).toBe('\\');
|
||||||
|
expect(unescapeStringForGeminiBug('C:\\\\Users')).toBe('C:\\Users');
|
||||||
|
expect(unescapeStringForGeminiBug('path\\\\to\\\\file')).toBe(
|
||||||
|
'path\to\\file',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should handle escaped backslashes mixed with other escapes (aggressive unescaping)', () => {
|
||||||
|
expect(unescapeStringForGeminiBug('line1\\\\\\nline2')).toBe(
|
||||||
|
'line1\nline2',
|
||||||
|
);
|
||||||
|
expect(unescapeStringForGeminiBug('quote\\\\"text\\\\nline')).toBe(
|
||||||
|
'quote"text\nline',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ensureCorrectEdit', () => {
|
describe('ensureCorrectEdit', () => {
|
||||||
|
@ -335,16 +351,13 @@ describe('editCorrector', () => {
|
||||||
old_string: 'find \\\\me',
|
old_string: 'find \\\\me',
|
||||||
new_string: 'replace with foobar',
|
new_string: 'replace with foobar',
|
||||||
};
|
};
|
||||||
mockResponses.push({
|
|
||||||
corrected_target_snippet: 'find \\me',
|
|
||||||
});
|
|
||||||
const result = await ensureCorrectEdit(
|
const result = await ensureCorrectEdit(
|
||||||
currentContent,
|
currentContent,
|
||||||
originalParams,
|
originalParams,
|
||||||
mockGeminiClientInstance,
|
mockGeminiClientInstance,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
);
|
);
|
||||||
expect(mockGenerateJson).toHaveBeenCalledTimes(1);
|
expect(mockGenerateJson).toHaveBeenCalledTimes(0);
|
||||||
expect(result.params.new_string).toBe('replace with foobar');
|
expect(result.params.new_string).toBe('replace with foobar');
|
||||||
expect(result.params.old_string).toBe('find \\me');
|
expect(result.params.old_string).toBe('find \\me');
|
||||||
expect(result.occurrences).toBe(1);
|
expect(result.occurrences).toBe(1);
|
||||||
|
@ -500,4 +513,167 @@ describe('editCorrector', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ensureCorrectFileContent', () => {
|
||||||
|
let mockGeminiClientInstance: Mocked<GeminiClient>;
|
||||||
|
let mockToolRegistry: Mocked<ToolRegistry>;
|
||||||
|
let mockConfigInstance: Config;
|
||||||
|
const abortSignal = new AbortController().signal;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockToolRegistry = new ToolRegistry({} as Config) as Mocked<ToolRegistry>;
|
||||||
|
const configParams = {
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
model: 'test-model',
|
||||||
|
sandbox: false as boolean | string,
|
||||||
|
targetDir: '/test',
|
||||||
|
debugMode: false,
|
||||||
|
question: undefined as string | undefined,
|
||||||
|
fullContext: false,
|
||||||
|
coreTools: undefined as string[] | undefined,
|
||||||
|
toolDiscoveryCommand: undefined as string | undefined,
|
||||||
|
toolCallCommand: undefined as string | undefined,
|
||||||
|
mcpServerCommand: undefined as string | undefined,
|
||||||
|
mcpServers: undefined as Record<string, any> | undefined,
|
||||||
|
userAgent: 'test-agent',
|
||||||
|
userMemory: '',
|
||||||
|
geminiMdFileCount: 0,
|
||||||
|
alwaysSkipModificationConfirmation: false,
|
||||||
|
};
|
||||||
|
mockConfigInstance = {
|
||||||
|
...configParams,
|
||||||
|
getApiKey: vi.fn(() => configParams.apiKey),
|
||||||
|
getModel: vi.fn(() => configParams.model),
|
||||||
|
getSandbox: vi.fn(() => configParams.sandbox),
|
||||||
|
getTargetDir: vi.fn(() => configParams.targetDir),
|
||||||
|
getToolRegistry: vi.fn(() => mockToolRegistry),
|
||||||
|
getDebugMode: vi.fn(() => configParams.debugMode),
|
||||||
|
getQuestion: vi.fn(() => configParams.question),
|
||||||
|
getFullContext: vi.fn(() => configParams.fullContext),
|
||||||
|
getCoreTools: vi.fn(() => configParams.coreTools),
|
||||||
|
getToolDiscoveryCommand: vi.fn(() => configParams.toolDiscoveryCommand),
|
||||||
|
getToolCallCommand: vi.fn(() => configParams.toolCallCommand),
|
||||||
|
getMcpServerCommand: vi.fn(() => configParams.mcpServerCommand),
|
||||||
|
getMcpServers: vi.fn(() => configParams.mcpServers),
|
||||||
|
getUserAgent: vi.fn(() => configParams.userAgent),
|
||||||
|
getUserMemory: vi.fn(() => configParams.userMemory),
|
||||||
|
setUserMemory: vi.fn((mem: string) => {
|
||||||
|
configParams.userMemory = mem;
|
||||||
|
}),
|
||||||
|
getGeminiMdFileCount: vi.fn(() => configParams.geminiMdFileCount),
|
||||||
|
setGeminiMdFileCount: vi.fn((count: number) => {
|
||||||
|
configParams.geminiMdFileCount = count;
|
||||||
|
}),
|
||||||
|
getAlwaysSkipModificationConfirmation: vi.fn(
|
||||||
|
() => configParams.alwaysSkipModificationConfirmation,
|
||||||
|
),
|
||||||
|
setAlwaysSkipModificationConfirmation: vi.fn((skip: boolean) => {
|
||||||
|
configParams.alwaysSkipModificationConfirmation = skip;
|
||||||
|
}),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
callCount = 0;
|
||||||
|
mockResponses.length = 0;
|
||||||
|
mockGenerateJson = vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((_contents, _schema, signal) => {
|
||||||
|
if (signal && signal.aborted) {
|
||||||
|
return Promise.reject(new Error('Aborted'));
|
||||||
|
}
|
||||||
|
const response = mockResponses[callCount];
|
||||||
|
callCount++;
|
||||||
|
if (response === undefined) return Promise.resolve({});
|
||||||
|
return Promise.resolve(response);
|
||||||
|
});
|
||||||
|
mockStartChat = vi.fn();
|
||||||
|
mockSendMessageStream = vi.fn();
|
||||||
|
|
||||||
|
mockGeminiClientInstance = new GeminiClient(
|
||||||
|
mockConfigInstance,
|
||||||
|
) as Mocked<GeminiClient>;
|
||||||
|
resetEditCorrectorCaches_TEST_ONLY();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return content unchanged if no escaping issues detected', async () => {
|
||||||
|
const content = 'This is normal content without escaping issues';
|
||||||
|
const result = await ensureCorrectFileContent(
|
||||||
|
content,
|
||||||
|
mockGeminiClientInstance,
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
expect(result).toBe(content);
|
||||||
|
expect(mockGenerateJson).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call correctStringEscaping for potentially escaped content', async () => {
|
||||||
|
const content = 'console.log(\\"Hello World\\");';
|
||||||
|
const correctedContent = 'console.log("Hello World");';
|
||||||
|
mockResponses.push({
|
||||||
|
corrected_string_escaping: correctedContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ensureCorrectFileContent(
|
||||||
|
content,
|
||||||
|
mockGeminiClientInstance,
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(correctedContent);
|
||||||
|
expect(mockGenerateJson).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle correctStringEscaping returning corrected content via correct property name', async () => {
|
||||||
|
// This test specifically verifies the property name fix
|
||||||
|
const content = 'const message = \\"Hello\\nWorld\\";';
|
||||||
|
const correctedContent = 'const message = "Hello\nWorld";';
|
||||||
|
|
||||||
|
// Mock the response with the correct property name
|
||||||
|
mockResponses.push({
|
||||||
|
corrected_string_escaping: correctedContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ensureCorrectFileContent(
|
||||||
|
content,
|
||||||
|
mockGeminiClientInstance,
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(correctedContent);
|
||||||
|
expect(mockGenerateJson).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return original content if LLM correction fails', async () => {
|
||||||
|
const content = 'console.log(\\"Hello World\\");';
|
||||||
|
// Mock empty response to simulate LLM failure
|
||||||
|
mockResponses.push({});
|
||||||
|
|
||||||
|
const result = await ensureCorrectFileContent(
|
||||||
|
content,
|
||||||
|
mockGeminiClientInstance,
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(content);
|
||||||
|
expect(mockGenerateJson).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle various escape sequences that need correction', async () => {
|
||||||
|
const content =
|
||||||
|
'const obj = { name: \\"John\\", age: 30, bio: \\"Developer\\nEngineer\\" };';
|
||||||
|
const correctedContent =
|
||||||
|
'const obj = { name: "John", age: 30, bio: "Developer\nEngineer" };';
|
||||||
|
|
||||||
|
mockResponses.push({
|
||||||
|
corrected_string_escaping: correctedContent,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ensureCorrectFileContent(
|
||||||
|
content,
|
||||||
|
mockGeminiClientInstance,
|
||||||
|
abortSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(correctedContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -511,10 +511,10 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
|
||||||
|
|
||||||
if (
|
if (
|
||||||
result &&
|
result &&
|
||||||
typeof result.corrected_new_string_escaping === 'string' &&
|
typeof result.corrected_string_escaping === 'string' &&
|
||||||
result.corrected_new_string_escaping.length > 0
|
result.corrected_string_escaping.length > 0
|
||||||
) {
|
) {
|
||||||
return result.corrected_new_string_escaping;
|
return result.corrected_string_escaping;
|
||||||
} else {
|
} else {
|
||||||
return potentiallyProblematicString;
|
return potentiallyProblematicString;
|
||||||
}
|
}
|
||||||
|
@ -564,15 +564,18 @@ function trimPairIfPossible(
|
||||||
*/
|
*/
|
||||||
export function unescapeStringForGeminiBug(inputString: string): string {
|
export function unescapeStringForGeminiBug(inputString: string): string {
|
||||||
// Regex explanation:
|
// Regex explanation:
|
||||||
// \\+ : Matches one or more literal backslash characters.
|
// \\ : Matches exactly one literal backslash character.
|
||||||
// (n|t|r|'|"|`|\n) : This is a capturing group. It matches one of the following:
|
// (n|t|r|'|"|`|\\|\n) : This is a capturing group. It matches one of the following:
|
||||||
// n, t, r, ', ", ` : These match the literal characters 'n', 't', 'r', single quote, double quote, or backtick.
|
// n, t, r, ', ", ` : These match the literal characters 'n', 't', 'r', single quote, double quote, or backtick.
|
||||||
// This handles cases like "\\n", "\\\\`", etc.
|
// This handles cases like "\\n", "\\`", etc.
|
||||||
|
// \\ : This matches a literal backslash. This handles cases like "\\\\" (escaped backslash).
|
||||||
// \n : This matches an actual newline character. This handles cases where the input
|
// \n : This matches an actual newline character. This handles cases where the input
|
||||||
// string might have something like "\\\n" (a literal backslash followed by a newline).
|
// string might have something like "\\\n" (a literal backslash followed by a newline).
|
||||||
// g : Global flag, to replace all occurrences.
|
// g : Global flag, to replace all occurrences.
|
||||||
|
|
||||||
return inputString.replace(/\\+(n|t|r|'|"|`|\n)/g, (match, capturedChar) => {
|
return inputString.replace(
|
||||||
|
/\\+(n|t|r|'|"|`|\\|\n)/g,
|
||||||
|
(match, capturedChar) => {
|
||||||
// 'match' is the entire erroneous sequence, e.g., if the input (in memory) was "\\\\`", match is "\\\\`".
|
// 'match' is the entire erroneous sequence, e.g., if the input (in memory) was "\\\\`", match is "\\\\`".
|
||||||
// 'capturedChar' is the character that determines the true meaning, e.g., '`'.
|
// 'capturedChar' is the character that determines the true meaning, e.g., '`'.
|
||||||
|
|
||||||
|
@ -589,6 +592,8 @@ export function unescapeStringForGeminiBug(inputString: string): string {
|
||||||
return '"'; // Correctly escaped: " (quotation mark character)
|
return '"'; // Correctly escaped: " (quotation mark character)
|
||||||
case '`':
|
case '`':
|
||||||
return '`'; // Correctly escaped: ` (backtick character)
|
return '`'; // Correctly escaped: ` (backtick character)
|
||||||
|
case '\\': // This handles when 'capturedChar' is a literal backslash
|
||||||
|
return '\\'; // Replace escaped backslash (e.g., "\\\\") with single backslash
|
||||||
case '\n': // This handles when 'capturedChar' is an actual newline
|
case '\n': // This handles when 'capturedChar' is an actual newline
|
||||||
return '\n'; // Replace the whole erroneous sequence (e.g., "\\\n" in memory) with a clean newline
|
return '\n'; // Replace the whole erroneous sequence (e.g., "\\\n" in memory) with a clean newline
|
||||||
default:
|
default:
|
||||||
|
@ -596,7 +601,8 @@ export function unescapeStringForGeminiBug(inputString: string): string {
|
||||||
// It would return the original matched sequence if an unexpected character was captured.
|
// It would return the original matched sequence if an unexpected character was captured.
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue