@@ -14,10 +14,6 @@ import { AnalyzerBot } from '../../AnalyzerBot/AnalyzerBot';
1414
1515const mockCurrentUserId = 123456 ;
1616
17- // Create helper function to generate consistent comment keys
18- const generateCommentKey = ( file : string , line : number | undefined , text : string ) =>
19- `${ file } :${ line } :${ text } ` ;
20-
2117const mockNotes = [
2218 {
2319 id : 1 ,
@@ -163,6 +159,201 @@ describe('VCS: GitLab', () => {
163159 // deleteNote and deleteDiscussion will be called for outdated comments
164160 } ) ;
165161
162+ describe ( 'Error Handling for Comment Deletion' , ( ) => {
163+ it ( 'should handle 404 errors gracefully when deleting notes' , async ( ) => {
164+ const service = new MrServiceMock ( ) ;
165+ const configsWithRemoveOldComment = { ...configs , removeOldComment : true } ;
166+
167+ // Mock a 404 error for note deletion
168+ const notFoundError = new Error ( 'Not Found' ) ;
169+ ( notFoundError as Error & { status : number } ) . status = 404 ;
170+ service . deleteNote . mockRejectedValueOnce ( notFoundError ) ;
171+
172+ // Add an existing comment that should be deleted
173+ service . listAllNotes . mockResolvedValue ( [
174+ {
175+ id : 999 ,
176+ author : { id : mockCurrentUserId } ,
177+ system : false ,
178+ body : 'outdated comment' ,
179+ } ,
180+ ] ) ;
181+
182+ const gitLab = createGitLab ( service , configsWithRemoveOldComment ) ;
183+
184+ // Should not throw error even when note deletion fails with 404
185+ await expect ( gitLab . report ( [ touchFileWarning ] ) ) . resolves . toBe ( true ) ;
186+ expect ( service . deleteNote ) . toHaveBeenCalled ( ) ;
187+ } ) ;
188+
189+ it ( 'should handle 404 errors gracefully when deleting discussions' , async ( ) => {
190+ const service = new MrServiceMock ( ) ;
191+ const configsWithRemoveOldComment = { ...configs , removeOldComment : true } ;
192+
193+ // Mock a 404 error for discussion deletion
194+ const notFoundError = new Error ( 'Not Found' ) ;
195+ ( notFoundError as Error & { status : number } ) . status = 404 ;
196+ service . deleteDiscussion . mockRejectedValueOnce ( notFoundError ) ;
197+
198+ // Add an existing discussion that should be deleted
199+ service . listAllDiscussions . mockResolvedValue ( [
200+ {
201+ id : 'discussion-123' ,
202+ notes : [
203+ {
204+ id : 456 ,
205+ author : { id : mockCurrentUserId } ,
206+ system : false ,
207+ body : 'outdated discussion comment' ,
208+ } ,
209+ ] ,
210+ } ,
211+ ] ) ;
212+
213+ const gitLab = createGitLab ( service , configsWithRemoveOldComment ) ;
214+
215+ // Should not throw error even when discussion deletion fails with 404
216+ await expect ( gitLab . report ( [ touchFileWarning ] ) ) . resolves . toBe ( true ) ;
217+ expect ( service . deleteDiscussion ) . toHaveBeenCalled ( ) ;
218+ } ) ;
219+
220+ it ( 'should continue processing when some comment deletions fail' , async ( ) => {
221+ const service = new MrServiceMock ( ) ;
222+ const configsWithRemoveOldComment = { ...configs , removeOldComment : true } ;
223+
224+ // Mock multiple notes, some will fail to delete
225+ service . listAllNotes . mockResolvedValue ( [
226+ {
227+ id : 100 ,
228+ author : { id : mockCurrentUserId } ,
229+ system : false ,
230+ body : 'comment that will fail' ,
231+ } ,
232+ {
233+ id : 200 ,
234+ author : { id : mockCurrentUserId } ,
235+ system : false ,
236+ body : 'comment that will succeed' ,
237+ } ,
238+ ] ) ;
239+
240+ // First deletion fails, second succeeds
241+ const notFoundError = new Error ( 'Not Found' ) ;
242+ ( notFoundError as Error & { status : number } ) . status = 404 ;
243+ service . deleteNote
244+ . mockRejectedValueOnce ( notFoundError )
245+ . mockResolvedValueOnce ( undefined ) ;
246+
247+ const gitLab = createGitLab ( service , configsWithRemoveOldComment ) ;
248+
249+ // Should complete successfully despite partial failures
250+ await expect ( gitLab . report ( [ touchFileWarning ] ) ) . resolves . toBe ( true ) ;
251+ expect ( service . deleteNote ) . toHaveBeenCalledTimes ( 2 ) ;
252+ } ) ;
253+
254+ it ( 'should handle invalid comment IDs gracefully' , async ( ) => {
255+ const service = new MrServiceMock ( ) ;
256+ const configsWithRemoveOldComment = { ...configs , removeOldComment : true } ;
257+
258+ // Mock notes with invalid IDs
259+ service . listAllNotes . mockResolvedValue ( [
260+ {
261+ id : null , // Invalid ID
262+ author : { id : mockCurrentUserId } ,
263+ system : false ,
264+ body : 'comment with null id' ,
265+ } ,
266+ {
267+ id : 'invalid-string-id' , // Invalid ID type
268+ author : { id : mockCurrentUserId } ,
269+ system : false ,
270+ body : 'comment with string id' ,
271+ } ,
272+ ] ) ;
273+
274+ const gitLab = createGitLab ( service , configsWithRemoveOldComment ) ;
275+
276+ // Should handle invalid IDs without attempting deletion
277+ await expect ( gitLab . report ( [ touchFileWarning ] ) ) . resolves . toBe ( true ) ;
278+ // deleteNote should not be called for invalid IDs
279+ expect ( service . deleteNote ) . not . toHaveBeenCalled ( ) ;
280+ } ) ;
281+
282+ it ( 'should handle discussion deletion with 404 errors on individual notes' , async ( ) => {
283+ const service = new MrServiceMock ( ) ;
284+ const configsWithRemoveOldComment = { ...configs , removeOldComment : true } ;
285+
286+ // Mock a discussion with notes
287+ const mockDiscussion = {
288+ id : 'discussion-456' ,
289+ notes : [
290+ { id : 10 , author : { id : mockCurrentUserId } } ,
291+ { id : 20 , author : { id : mockCurrentUserId } } ,
292+ ] ,
293+ } ;
294+
295+ service . listAllDiscussions . mockResolvedValue ( [ mockDiscussion ] ) ;
296+
297+ // Mock discussion show to return the discussion
298+ const mockMRService = service as MrServiceMock & {
299+ api : {
300+ MergeRequestDiscussions : {
301+ show : jest . Mock ;
302+ } ;
303+ MergeRequestNotes : {
304+ remove : jest . Mock ;
305+ } ;
306+ } ;
307+ } ;
308+ mockMRService . api = {
309+ MergeRequestDiscussions : {
310+ show : jest . fn ( ) . mockResolvedValue ( mockDiscussion ) ,
311+ } ,
312+ MergeRequestNotes : {
313+ remove : jest
314+ . fn ( )
315+ . mockRejectedValueOnce ( new Error ( 'Not Found' ) ) // First note fails
316+ . mockResolvedValueOnce ( undefined ) , // Second note succeeds
317+ } ,
318+ } ;
319+
320+ const gitLab = createGitLab ( service , configsWithRemoveOldComment ) ;
321+
322+ // Should complete successfully despite partial note deletion failures
323+ await expect ( gitLab . report ( [ touchFileWarning ] ) ) . resolves . toBe ( true ) ;
324+ } ) ;
325+
326+ it ( 'should log appropriate warnings for failed deletions' , async ( ) => {
327+ const service = new MrServiceMock ( ) ;
328+ const configsWithRemoveOldComment = { ...configs , removeOldComment : true } ;
329+
330+ // Spy on console to capture log messages
331+ const consoleSpy = jest . spyOn ( console , 'warn' ) . mockImplementation ( ) ;
332+
333+ // Mock a 404 error
334+ const notFoundError = new Error ( 'Not Found' ) ;
335+ ( notFoundError as Error & { status : number } ) . status = 404 ;
336+ service . deleteNote . mockRejectedValueOnce ( notFoundError ) ;
337+
338+ service . listAllNotes . mockResolvedValue ( [
339+ {
340+ id : 123 ,
341+ author : { id : mockCurrentUserId } ,
342+ system : false ,
343+ body : 'comment to delete' ,
344+ } ,
345+ ] ) ;
346+
347+ const gitLab = createGitLab ( service , configsWithRemoveOldComment ) ;
348+ await gitLab . report ( [ touchFileWarning ] ) ;
349+
350+ // Should log warning about failed deletion
351+ // Note: The actual log message will depend on the Winston logger configuration
352+
353+ consoleSpy . mockRestore ( ) ;
354+ } ) ;
355+ } ) ;
356+
166357 describe ( 'when failOnWarnings is true' , ( ) => {
167358 const warningConfigs = { ...configs , failOnWarnings : true } ;
168359
0 commit comments