Download packages/core/src/utils/editCorrector.test.ts from Jimmi42/openCLI: direct link, hf CLI and curl.
- Browser
- Download file 27.8 kB
-
https://huggingface.co/Jimmi42/openCLI/resolve/main/packages/core/src/utils/editCorrector.test.ts
- Command line
-
hf download hf://Jimmi42/openCLI/packages/core/src/utils/editCorrector.test.ts
-
curl -L -o editCorrector.test.ts https://huggingface.co/Jimmi42/openCLI/resolve/main/packages/core/src/utils/editCorrector.test.ts
27.8 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| import { vi, describe, it, expect, beforeEach, type Mocked } from 'vitest'; | |
| // MOCKS | |
| let callCount = 0; | |
| const mockResponses: any[] = []; | |
| let mockGenerateJson: any; | |
| let mockStartChat: any; | |
| let mockSendMessageStream: any; | |
| vi.mock('../core/client.js', () => ({ | |
| GeminiClient: vi.fn().mockImplementation(function ( | |
| this: any, | |
| _config: Config, | |
| ) { | |
| this.generateJson = (...params: any[]) => mockGenerateJson(...params); // Corrected: use mockGenerateJson | |
| this.startChat = (...params: any[]) => mockStartChat(...params); // Corrected: use mockStartChat | |
| this.sendMessageStream = (...params: any[]) => | |
| mockSendMessageStream(...params); // Corrected: use mockSendMessageStream | |
| return this; | |
| }), | |
| })); | |
| // END MOCKS | |
| import { | |
| countOccurrences, | |
| ensureCorrectEdit, | |
| ensureCorrectFileContent, | |
| unescapeStringForGeminiBug, | |
| resetEditCorrectorCaches_TEST_ONLY, | |
| } from './editCorrector.js'; | |
| import { GeminiClient } from '../core/client.js'; | |
| import type { Config } from '../config/config.js'; | |
| import { ToolRegistry } from '../tools/tool-registry.js'; | |
| vi.mock('../tools/tool-registry.js'); | |
| describe('editCorrector', () => { | |
| describe('countOccurrences', () => { | |
| it('should return 0 for empty string', () => { | |
| expect(countOccurrences('', 'a')).toBe(0); | |
| }); | |
| it('should return 0 for empty substring', () => { | |
| expect(countOccurrences('abc', '')).toBe(0); | |
| }); | |
| it('should return 0 if substring is not found', () => { | |
| expect(countOccurrences('abc', 'd')).toBe(0); | |
| }); | |
| it('should return 1 if substring is found once', () => { | |
| expect(countOccurrences('abc', 'b')).toBe(1); | |
| }); | |
| it('should return correct count for multiple occurrences', () => { | |
| expect(countOccurrences('ababa', 'a')).toBe(3); | |
| expect(countOccurrences('ababab', 'ab')).toBe(3); | |
| }); | |
| it('should count non-overlapping occurrences', () => { | |
| expect(countOccurrences('aaaaa', 'aa')).toBe(2); | |
| expect(countOccurrences('ababab', 'aba')).toBe(1); | |
| }); | |
| it('should correctly count occurrences when substring is longer', () => { | |
| expect(countOccurrences('abc', 'abcdef')).toBe(0); | |
| }); | |
| it('should be case sensitive', () => { | |
| expect(countOccurrences('abcABC', 'a')).toBe(1); | |
| expect(countOccurrences('abcABC', 'A')).toBe(1); | |
| }); | |
| }); | |
| describe('unescapeStringForGeminiBug', () => { | |
| it('should unescape common sequences', () => { | |
| expect(unescapeStringForGeminiBug('\\n')).toBe('\n'); | |
| expect(unescapeStringForGeminiBug('\\t')).toBe('\t'); | |
| expect(unescapeStringForGeminiBug("\\'")).toBe("'"); | |
| expect(unescapeStringForGeminiBug('\\"')).toBe('"'); | |
| expect(unescapeStringForGeminiBug('\\`')).toBe('`'); | |
| }); | |
| it('should handle multiple escaped sequences', () => { | |
| expect(unescapeStringForGeminiBug('Hello\\nWorld\\tTest')).toBe( | |
| 'Hello\nWorld\tTest', | |
| ); | |
| }); | |
| it('should not alter already correct sequences', () => { | |
| expect(unescapeStringForGeminiBug('\n')).toBe('\n'); | |
| expect(unescapeStringForGeminiBug('Correct string')).toBe( | |
| 'Correct string', | |
| ); | |
| }); | |
| it('should handle mixed correct and incorrect sequences', () => { | |
| expect(unescapeStringForGeminiBug('\\nCorrect\t\\`')).toBe( | |
| '\nCorrect\t`', | |
| ); | |
| }); | |
| it('should handle backslash followed by actual newline character', () => { | |
| expect(unescapeStringForGeminiBug('\\\n')).toBe('\n'); | |
| expect(unescapeStringForGeminiBug('First line\\\nSecond line')).toBe( | |
| 'First line\nSecond line', | |
| ); | |
| }); | |
| it('should handle multiple backslashes before an escapable character (aggressive unescaping)', () => { | |
| expect(unescapeStringForGeminiBug('\\\\n')).toBe('\n'); | |
| expect(unescapeStringForGeminiBug('\\\\\\t')).toBe('\t'); | |
| expect(unescapeStringForGeminiBug('\\\\\\\\`')).toBe('`'); | |
| }); | |
| it('should return empty string for empty input', () => { | |
| expect(unescapeStringForGeminiBug('')).toBe(''); | |
| }); | |
| it('should not alter strings with no targeted escape sequences', () => { | |
| expect(unescapeStringForGeminiBug('abc def')).toBe('abc def'); | |
| expect(unescapeStringForGeminiBug('C:\\Folder\\File')).toBe( | |
| 'C:\\Folder\\File', | |
| ); | |
| }); | |
| it('should correctly process strings with some targeted escapes', () => { | |
| expect(unescapeStringForGeminiBug('C:\\Users\\name')).toBe( | |
| 'C:\\Users\name', | |
| ); | |
| }); | |
| it('should handle complex cases with mixed slashes and characters', () => { | |
| expect( | |
| unescapeStringForGeminiBug('\\\\\\\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', () => { | |
| 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) => { | |
| // Check if the signal is aborted. If so, throw an error or return a specific response. | |
| if (signal && signal.aborted) { | |
| return Promise.reject(new Error('Aborted')); // Or some other specific error/response | |
| } | |
| 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(); | |
| }); | |
| describe('Scenario Group 1: originalParams.old_string matches currentContent directly', () => { | |
| it('Test 1.1: old_string (no literal \\), new_string (escaped by Gemini) -> new_string unescaped', async () => { | |
| const currentContent = 'This is a test string to find me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find me', | |
| new_string: 'replace with \\"this\\"', | |
| }; | |
| mockResponses.push({ | |
| corrected_new_string_escaping: 'replace with "this"', | |
| }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(1); | |
| expect(result.params.new_string).toBe('replace with "this"'); | |
| expect(result.params.old_string).toBe('find me'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 1.2: old_string (no literal \\), new_string (correctly formatted) -> new_string unchanged', async () => { | |
| const currentContent = 'This is a test string to find me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find me', | |
| new_string: 'replace with this', | |
| }; | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(0); | |
| expect(result.params.new_string).toBe('replace with this'); | |
| expect(result.params.old_string).toBe('find me'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 1.3: old_string (with literal \\), new_string (escaped by Gemini) -> new_string unchanged (still escaped)', async () => { | |
| const currentContent = 'This is a test string to find\\me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find\\me', | |
| new_string: 'replace with \\"this\\"', | |
| }; | |
| mockResponses.push({ | |
| corrected_new_string_escaping: 'replace with "this"', | |
| }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(1); | |
| expect(result.params.new_string).toBe('replace with "this"'); | |
| expect(result.params.old_string).toBe('find\\me'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 1.4: old_string (with literal \\), new_string (correctly formatted) -> new_string unchanged', async () => { | |
| const currentContent = 'This is a test string to find\\me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find\\me', | |
| new_string: 'replace with this', | |
| }; | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(0); | |
| expect(result.params.new_string).toBe('replace with this'); | |
| expect(result.params.old_string).toBe('find\\me'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| }); | |
| describe('Scenario Group 2: originalParams.old_string does NOT match, but unescapeStringForGeminiBug(originalParams.old_string) DOES match', () => { | |
| it('Test 2.1: old_string (over-escaped, no intended literal \\), new_string (escaped by Gemini) -> new_string unescaped', async () => { | |
| const currentContent = 'This is a test string to find "me".'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find \\"me\\"', | |
| new_string: 'replace with \\"this\\"', | |
| }; | |
| mockResponses.push({ corrected_new_string: 'replace with "this"' }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(1); | |
| expect(result.params.new_string).toBe('replace with "this"'); | |
| expect(result.params.old_string).toBe('find "me"'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 2.2: old_string (over-escaped, no intended literal \\), new_string (correctly formatted) -> new_string unescaped (harmlessly)', async () => { | |
| const currentContent = 'This is a test string to find "me".'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find \\"me\\"', | |
| new_string: 'replace with this', | |
| }; | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(0); | |
| expect(result.params.new_string).toBe('replace with this'); | |
| expect(result.params.old_string).toBe('find "me"'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 2.3: old_string (over-escaped, with intended literal \\), new_string (simple) -> new_string corrected', async () => { | |
| const currentContent = 'This is a test string to find \\me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find \\\\me', | |
| new_string: 'replace with foobar', | |
| }; | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(0); | |
| expect(result.params.new_string).toBe('replace with foobar'); | |
| expect(result.params.old_string).toBe('find \\me'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| }); | |
| describe('Scenario Group 3: LLM Correction Path', () => { | |
| it('Test 3.1: old_string (no literal \\), new_string (escaped by Gemini), LLM re-escapes new_string -> final new_string is double unescaped', async () => { | |
| const currentContent = 'This is a test string to corrected find me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find me', | |
| new_string: 'replace with \\\\"this\\\\"', | |
| }; | |
| const llmNewString = 'LLM says replace with "that"'; | |
| mockResponses.push({ corrected_new_string_escaping: llmNewString }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(1); | |
| expect(result.params.new_string).toBe(llmNewString); | |
| expect(result.params.old_string).toBe('find me'); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 3.2: old_string (with literal \\), new_string (escaped by Gemini), LLM re-escapes new_string -> final new_string is unescaped once', async () => { | |
| const currentContent = 'This is a test string to corrected find me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find\\me', | |
| new_string: 'replace with \\\\"this\\\\"', | |
| }; | |
| const llmCorrectedOldString = 'corrected find me'; | |
| const llmNewString = 'LLM says replace with "that"'; | |
| mockResponses.push({ corrected_target_snippet: llmCorrectedOldString }); | |
| mockResponses.push({ corrected_new_string: llmNewString }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(2); | |
| expect(result.params.new_string).toBe(llmNewString); | |
| expect(result.params.old_string).toBe(llmCorrectedOldString); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 3.3: old_string needs LLM, new_string is fine -> old_string corrected, new_string original', async () => { | |
| const currentContent = 'This is a test string to be corrected.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'fiiind me', | |
| new_string: 'replace with "this"', | |
| }; | |
| const llmCorrectedOldString = 'to be corrected'; | |
| mockResponses.push({ corrected_target_snippet: llmCorrectedOldString }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(1); | |
| expect(result.params.new_string).toBe('replace with "this"'); | |
| expect(result.params.old_string).toBe(llmCorrectedOldString); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| it('Test 3.4: LLM correction path, correctNewString returns the originalNewString it was passed (which was unescaped) -> final new_string is unescaped', async () => { | |
| const currentContent = 'This is a test string to corrected find me.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find me', | |
| new_string: 'replace with \\\\"this\\\\"', | |
| }; | |
| const newStringForLLMAndReturnedByLLM = 'replace with "this"'; | |
| mockResponses.push({ | |
| corrected_new_string_escaping: newStringForLLMAndReturnedByLLM, | |
| }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(1); | |
| expect(result.params.new_string).toBe(newStringForLLMAndReturnedByLLM); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| }); | |
| describe('Scenario Group 4: No Match Found / Multiple Matches', () => { | |
| it('Test 4.1: No version of old_string (original, unescaped, LLM-corrected) matches -> returns original params, 0 occurrences', async () => { | |
| const currentContent = 'This content has nothing to find.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'nonexistent string', | |
| new_string: 'some new string', | |
| }; | |
| mockResponses.push({ corrected_target_snippet: 'still nonexistent' }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(1); | |
| expect(result.params).toEqual(originalParams); | |
| expect(result.occurrences).toBe(0); | |
| }); | |
| it('Test 4.2: unescapedOldStringAttempt results in >1 occurrences -> returns original params, count occurrences', async () => { | |
| const currentContent = | |
| 'This content has find "me" and also find "me" again.'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'find "me"', | |
| new_string: 'some new string', | |
| }; | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(0); | |
| expect(result.params).toEqual(originalParams); | |
| expect(result.occurrences).toBe(2); | |
| }); | |
| }); | |
| describe('Scenario Group 5: Specific unescapeStringForGeminiBug checks (integrated into ensureCorrectEdit)', () => { | |
| it('Test 5.1: old_string needs LLM to become currentContent, new_string also needs correction', async () => { | |
| const currentContent = 'const x = "a\\nbc\\\\"def\\\\"'; | |
| const originalParams = { | |
| file_path: '/test/file.txt', | |
| old_string: 'const x = \\\\"a\\\\nbc\\\\\\\\"def\\\\\\\\"', | |
| new_string: 'const y = \\\\"new\\\\nval\\\\\\\\"content\\\\\\\\"', | |
| }; | |
| const expectedFinalNewString = 'const y = "new\\nval\\\\"content\\\\"'; | |
| mockResponses.push({ corrected_target_snippet: currentContent }); | |
| mockResponses.push({ corrected_new_string: expectedFinalNewString }); | |
| const result = await ensureCorrectEdit( | |
| currentContent, | |
| originalParams, | |
| mockGeminiClientInstance, | |
| abortSignal, | |
| ); | |
| expect(mockGenerateJson).toHaveBeenCalledTimes(2); | |
| expect(result.params.old_string).toBe(currentContent); | |
| expect(result.params.new_string).toBe(expectedFinalNewString); | |
| expect(result.occurrences).toBe(1); | |
| }); | |
| }); | |
| }); | |
| 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); | |
| }); | |
| }); | |
| }); | |