import { jest, expect, describe, it } from '@jest/globals' import { splitPatch, patchStartEndLine, parsePatch, checkIfDocumentationOnly, parseReview, extractPatternType, HunkInfo, } from '../utils/patch-utils' describe('patch-utils', () => { describe('splitPatch', () => { it('should split patch with proper @@ headers into hunks', () => { const patch = `@@ -10,5 +10,6 @@ line1 line2 -old line +new line +added line @@ -20,3 +20,4 @@ context -modified +added another` const result = splitPatch(patch) expect(result).toHaveLength(2) expect(result[0]).toContain('line1') expect(result[1]).toContain('context') }) it('should return entire patch when no @@ headers found (new files)', () => { const newFilePatch = `+function updateSettings(user, newTheme) { + const updatedUser = user; + updatedUser.settings.theme = newTheme; + return updatedUser; +}` const result = splitPatch(newFilePatch) expect(result).toHaveLength(1) expect(result[0]).toBe(newFilePatch) }) it('should return empty array for null/undefined patch', () => { expect(splitPatch(null)).toEqual([]) expect(splitPatch(undefined)).toEqual([]) }) it('should handle single hunk patch', () => { const patch = `@@ -1,3 +1,4 @@ const x = 1; -old +new +added` const result = splitPatch(patch) expect(result).toHaveLength(1) }) it('should handle patch with only context lines', () => { const patch = `@@ -5,3 +5,3 @@ return result; } -remove +add` const result = splitPatch(patch) expect(result).toHaveLength(1) }) }) describe('patchStartEndLine', () => { it('should parse standard @@ header correctly', () => { const patch = `@@ -10,5 +10,6 @@ content` const result = patchStartEndLine(patch) expect(result).toEqual({ oldStart: 10, oldLength: 5, newStart: 10, newLength: 6, oldHunk: { startLine: 10, endLine: 14 }, newHunk: { startLine: 10, endLine: 15 }, }) }) it('should handle new file without @@ header (all + lines)', () => { const newFilePatch = `+function test() { + return true; +}` const result = patchStartEndLine(newFilePatch) expect(result).not.toBeNull() expect(result?.newStart).toBe(1) expect(result?.newLength).toBe(3) expect(result?.oldStart).toBe(0) expect(result?.oldLength).toBe(0) expect(result?.oldHunk.startLine).toBe(0) expect(result?.oldHunk.endLine).toBe(0) expect(result?.newHunk.startLine).toBe(1) expect(result?.newHunk.endLine).toBe(3) }) it('should handle single line new file', () => { const singleLinePatch = `+const x = 1;` const result = patchStartEndLine(singleLinePatch) expect(result?.newLength).toBe(1) expect(result?.newHunk.endLine).toBe(1) }) it('should return null for patch with no + lines and no header', () => { const invalidPatch = ` context line another line` const result = patchStartEndLine(invalidPatch) expect(result).toBeNull() }) it('should handle diff with whitespace before +', () => { const patch = `@@ -1,2 +1,3 @@ line1 +added line` const result = patchStartEndLine(patch) expect(result?.newLength).toBe(3) expect(result?.oldLength).toBe(2) }) }) describe('parsePatch', () => { it('should parse standard diff correctly', () => { const patch = `@@ -1,4 +1,5 @@ const x = 1; -old line +new line +added line context line` const result = parsePatch(patch) expect(result).not.toBeNull() expect(result?.oldStart).toBe(1) expect(result?.newStart).toBe(1) expect(result?.oldHunk).toContain('old line') expect(result?.newHunk).toContain('new line') expect(result?.newHunk).toContain('added line') }) it('should handle new file (no old lines)', () => { const newFilePatch = `+function test() { + return 1; +}` const result = parsePatch(newFilePatch) expect(result).not.toBeNull() expect(result?.oldHunk).toBe('') expect(result?.newHunk).toContain('function test()') expect(result?.newStart).toBe(1) }) it('should handle deleted file (no new lines)', () => { const deletedPatch = `@@ -1,3 +1,0 @@ -old line -old line 2 -old line 3` const result = parsePatch(deletedPatch) expect(result).not.toBeNull() expect(result?.oldHunk).toContain('old line') expect(result?.newHunk).toBe('') }) it('should return null for invalid patch without header and no + lines', () => { const invalidPatch = `just some text without diff markers` const result = parsePatch(invalidPatch) expect(result).toBeNull() }) }) describe('checkIfDocumentationOnly', () => { it('should return true for pure comment marker changes (no text content)', () => { const docOnlyDiff = `-// +////` expect(checkIfDocumentationOnly(docOnlyDiff)).toBe(true) }) it('should return false for comment text changes', () => { const codeDiff = `-// Old comment +// New comment` expect(checkIfDocumentationOnly(codeDiff)).toBe(false) }) it('should return false for code changes', () => { const codeDiff = `-const x = 1; +const x = 2;` expect(checkIfDocumentationOnly(codeDiff)).toBe(false) }) it('should return false for Python docstrings', () => { const pythonDocDiff = `-"""Old doc""" +"""New doc"""` expect(checkIfDocumentationOnly(pythonDocDiff)).toBe(false) }) }) describe('parseReview', () => { it('should parse review with line numbers', () => { const response = `[10] | major | bug: Missing null check Null pointer risk here` const patches = [ { newStart: 1, newLength: 20, }, ] const result = parseReview(response, patches, 'test.js') expect(result).toHaveLength(1) expect(result[0].startLine).toBe(10) expect(result[0].endLine).toBe(10) expect(result[0].severity).toBe('major') }) it('should parse review with line range', () => { const response = `[10-15] | critical | security: SQL injection risk` const patches = [ { newStart: 1, newLength: 20, }, ] const result = parseReview(response, patches, 'test.js') expect(result).toHaveLength(1) expect(result[0].startLine).toBe(10) expect(result[0].endLine).toBe(15) expect(result[0].severity).toBe('critical') }) it('should skip reviews outside valid line range', () => { const response = `[100] | major | bug: Out of range` const patches = [ { newStart: 1, newLength: 20, }, ] const result = parseReview(response, patches, 'test.js') expect(result).toHaveLength(0) }) it('should extract remedy from code blocks', () => { const response = `[10] | major | bug: Fix this \`\`\` const fixed = value || defaultValue; \`\`\`` const patches = [ { newStart: 1, newLength: 20, }, ] const result = parseReview(response, patches, 'test.js') expect(result).toHaveLength(1) expect(result[0].remedy).toContain('fixed') }) }) describe('extractPatternType', () => { it('should extract Security pattern', () => { expect(extractPatternType('Security issue found')).toBe('security') }) it('should extract Bug pattern', () => { expect(extractPatternType('Bug in loop')).toBe('bug') }) it('should extract Performance pattern', () => { expect(extractPatternType('Performance bottleneck detected')).toBe('performance') }) it('should return general for unknown patterns', () => { expect(extractPatternType('Some random message')).toBe('general') }) }) describe('integration: real-world patch scenarios', () => { it('should handle the actual e.js patch from PR #52', () => { const actualPatch = `+function updateSettings(user, newTheme) { + + const updatedUser = user; + + updatedUser.settings.theme = newTheme; + + return updatedUser; +} + + +const account = { id: 1, settings: { theme: 'dark' } }; +const newAccount = updateSettings(account, 'light'); +console.log(account.settings.theme);` const split = splitPatch(actualPatch) expect(split).toHaveLength(1) const hunkInfo = patchStartEndLine(split[0]) expect(hunkInfo).not.toBeNull() expect(hunkInfo?.newLength).toBe(13) const parsed = parsePatch(split[0]) expect(parsed).not.toBeNull() expect(parsed?.newHunk).toContain('updateSettings') }) it('should handle multi-file diff with multiple hunks', () => { const multiFileDiff = `diff --git a/calc.py b/calc.py @@ -1,5 +1,6 @@ if __name__ == "__main__": print(f"Addition: {add(10, 5)}") - print(f"Division: {divide(10, 5)}") + print(f"Division: {divide(10, 5)}") + ded \\ No newline at end of file` const split = splitPatch(multiFileDiff) expect(split.length).toBeGreaterThanOrEqual(1) const hunkInfo = patchStartEndLine(split[0]) expect(hunkInfo).not.toBeNull() }) }) })