diff --git a/packages/core/src/utils/checkpointUtils.test.ts b/packages/core/src/utils/checkpointUtils.test.ts index e9567305719..6a4df4c384d 100644 --- a/packages/core/src/utils/checkpointUtils.test.ts +++ b/packages/core/src/utils/checkpointUtils.test.ts @@ -176,6 +176,32 @@ describe('checkpoint utils', () => { expect(fileContent.messageId).toBe('p1'); }); + it('keeps the .json in the checkpoint name when editing a .json file', async () => { + const toolCalls = [ + { + callId: '1', + name: 'replace', + args: { file_path: 'config.json' }, + prompt_id: 'p1', + isClientInitiated: false, + }, + ] as ToolCallRequestInfo[]; + + (mockGitService.createFileSnapshot as Mock).mockResolvedValue('hash123'); + (mockGeminiClient.getHistory as Mock).mockReturnValue([]); + + const { toolCallToCheckpointMap } = await processRestorableToolCalls( + toolCalls, + mockGitService, + mockGeminiClient, + 'history-data', + ); + + expect(toolCallToCheckpointMap.get('1')).toMatch( + /-config\.json-replace$/, + ); + }); + it('should handle git snapshot failure by using current commit hash', async () => { const toolCalls = [ { @@ -280,6 +306,23 @@ describe('checkpoint utils', () => { expect(actual).toEqual(expected); }); + it('strips only the trailing .json when the edited file is itself .json', () => { + const checkpointFiles = new Map([ + [ + '2025-01-01T12-00-00_000Z-config.json-replace.json', + JSON.stringify({ messageId: 'msg1' }), + ], + ]); + + const actual = getCheckpointInfoList(checkpointFiles); + expect(actual).toEqual([ + { + messageId: 'msg1', + checkpoint: '2025-01-01T12-00-00_000Z-config.json-replace', + }, + ]); + }); + it('should ignore files with invalid JSON', () => { const checkpointFiles = new Map([ ['checkpoint1.json', JSON.stringify({ messageId: 'msg1' })], diff --git a/packages/core/src/utils/checkpointUtils.ts b/packages/core/src/utils/checkpointUtils.ts index 0aab0a2a2fb..a1f25b3416b 100644 --- a/packages/core/src/utils/checkpointUtils.ts +++ b/packages/core/src/utils/checkpointUtils.ts @@ -142,7 +142,10 @@ export async function processRestorableToolCalls( checkpointsToWrite.set(fileName, JSON.stringify(checkpointData, null, 2)); toolCallToCheckpointMap.set( toolCall.callId, - fileName.replace('.json', ''), + // Use the base name directly; fileName.replace('.json', '') would strip + // the first ".json" instead of the extension when the edited file is + // itself a .json file (e.g. "...-config.json-replace.json"). + checkpointFileName, ); } catch (error) { errors.push( @@ -176,7 +179,10 @@ export function getCheckpointInfoList( if (result.success) { checkpointInfoList.push({ messageId: result.data.messageId, - checkpoint: file.replace('.json', ''), + // basename(file, '.json') strips only the trailing extension; a + // plain replace('.json', '') would corrupt names from edited .json + // files (e.g. "...-config.json-replace.json"). + checkpoint: path.basename(file, '.json'), }); } } catch {