import * as assert from 'assert'; import { mergeResults, MergedReport } from '../src/merger/merger'; import { CustomRuleResult, AIFinding } from '../src/ai/schema'; import { LinterDiagnostic } from '../src/types'; function range(line: number): any { return new (require('vscode').Range)(line, 0, line, 1); } suite('Merger Tests', () => { test('mergeResults counts correctly', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:no-unused', message: 'x is unused', range: new (require('vscode').Range)(0, 0, 0, 1) }, ]; const customResults: CustomRuleResult[] = [ { ruleId: 'custom:no-console', line: 5, severity: 'warning', message: 'avoid console.log' }, ]; const aiFindings: AIFinding[] = [ { ruleId: 'hardcoded-secret', severity: 'error', category: 'security', title: 'Hardcoded', description: 'Found secret', suggestion: 'Use env', line: 3 }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: customResults, translatedDiagnostics: [], aiFindings, errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.strictEqual(report.linterCount, 1); assert.strictEqual(report.customRuleCount, 1); assert.strictEqual(report.aiCount, 1); assert.strictEqual(report.degraded, false); assert.strictEqual(report.language, 'javascript'); }); test('mergeResults marks degraded when AI fails', () => { const report = mergeResults({ staticDiagnostics: [], customRuleResults: [], translatedDiagnostics: [], aiFindings: [], errors: ['AI 请求超时'], degraded: true, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.strictEqual(report.degraded, true); assert.strictEqual(report.errors.length, 1); }); test('mergeResults sorts diagnostics by severity then line', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'info', ruleId: 'eslint:info-5', message: 'i5', range: range(5) }, { severity: 'error', ruleId: 'eslint:err-10', message: 'e10', range: range(10) }, { severity: 'warning', ruleId: 'eslint:warn-3', message: 'w3', range: range(3) }, { severity: 'error', ruleId: 'eslint:err-1', message: 'e1', range: range(1) }, ]; const customResults: CustomRuleResult[] = [ { ruleId: 'custom:i-9', line: 9, severity: 'info', message: 'i9' }, { ruleId: 'custom:e-2', line: 2, severity: 'error', message: 'e2' }, ]; const aiFindings: AIFinding[] = [ { ruleId: 'ai:w-7', severity: 'warning', category: 'style', title: 'w7', description: 'w7', line: 7, suggestion: '' }, { ruleId: 'ai:e-4', severity: 'error', category: 'bug', title: 'e4', description: 'e4', line: 4, suggestion: '' }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: customResults, translatedDiagnostics: [], aiFindings, errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.deepStrictEqual( report.linterDiagnostics.map(d => `${d.severity}:${d.range.start.line}`), ['error:1', 'error:10', 'warning:3', 'info:5'] ); assert.deepStrictEqual( report.customRuleDiagnostics.map(d => `${d.severity}:${d.range.start.line}`), ['error:1', 'info:8'] ); assert.deepStrictEqual( report.aiFindings.map(f => `${f.severity}:${f.line}`), ['error:3', 'warning:6'] ); assert.deepStrictEqual(report.fixableLinterIndices, []); assert.deepStrictEqual(report.fixableCustomIndices, []); }); test('mergeResults marks fixable only diagnostics with fix object', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:fixable-1', message: 'f1', range: range(1), fix: { range: [0, 5], text: 'x' } }, { severity: 'warning', ruleId: 'eslint:nofix-2', message: 'n2', range: range(2) }, { severity: 'error', ruleId: 'eslint:fixable-3', message: 'f3', range: range(3), fix: { range: [6, 9], text: 'y' } }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); const sorted = report.linterDiagnostics; const fixableIndices = sorted.map((_, i) => (sorted[i].fix ? i : -1)).filter(i => i !== -1); assert.deepStrictEqual(report.fixableLinterIndices, fixableIndices); assert.strictEqual(report.fixableCustomIndices.length, 0); }); test('mergeResults converts aiFindings line to 0-based', () => { const aiFindings: AIFinding[] = [ { ruleId: 'ai:first', severity: 'warning', category: 'style', title: 'first', description: 'd', line: 1, suggestion: '' }, { ruleId: 'ai:last', severity: 'info', category: 'design', title: 'last', description: 'd', line: 8, suggestion: '' }, ]; const report = mergeResults({ staticDiagnostics: [], customRuleResults: [], translatedDiagnostics: [], aiFindings, errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.deepStrictEqual( report.aiFindings.map(f => f.line), [0, 7] ); }); test('mergeResults pairs translations by originalRuleId not index', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:no-unused', message: 'x is unused', range: range(1) }, { severity: 'warning', ruleId: 'eslint:no-console', message: 'console call', range: range(2) }, { severity: 'error', ruleId: 'eslint:no-eval', message: 'eval used', range: range(3) }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [ { originalRuleId: 'eslint:no-eval', translatedMessage: '评估使用', translatedSuggestion: '避免' }, { originalRuleId: 'eslint:no-unused', translatedMessage: '未使用', translatedSuggestion: '删除' }, ], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.strictEqual(report.linterDiagnostics[0].message, '未使用'); assert.strictEqual(report.linterDiagnostics[0].suggestion, '删除'); assert.strictEqual(report.linterDiagnostics[1].message, '评估使用'); assert.strictEqual(report.linterDiagnostics[2].message, 'console call'); }); test('mergeResults handles duplicate ruleId translations in order', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:no-console', message: 'first console', range: range(1) }, { severity: 'error', ruleId: 'eslint:no-console', message: 'second console', range: range(2) }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [ { originalRuleId: 'eslint:no-console', translatedMessage: '首次', translatedSuggestion: '' }, { originalRuleId: 'eslint:no-console', translatedMessage: '二次', translatedSuggestion: '' }, ], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.strictEqual(report.linterDiagnostics[0].message, '首次'); assert.strictEqual(report.linterDiagnostics[1].message, '二次'); }); test('mergeResults matches translations by normalized ruleId when prefix missing', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:no-var', message: 'Unexpected var', range: range(1) }, { severity: 'warning', ruleId: 'eslint:no-console', message: 'console call', range: range(2) }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [ { originalRuleId: 'no-var', translatedMessage: '应使用 let/const', translatedSuggestion: '改写成 let x = 1' }, { originalRuleId: 'no-console', translatedMessage: '避免 console', translatedSuggestion: '' }, ], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.strictEqual(report.linterDiagnostics[0].message, '应使用 let/const'); assert.strictEqual(report.linterDiagnostics[0].suggestion, '改写成 let x = 1'); assert.strictEqual(report.linterDiagnostics[1].message, '避免 console'); }); test('mergeResults matches translations by normalized ruleId with slash variant', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:@typescript-eslint/no-unused-vars', message: 'x unused', range: range(1) }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [ { originalRuleId: '@typescript-eslint/no-unused-vars', translatedMessage: '未使用变量', translatedSuggestion: '删除 x' }, ], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.ts', language: 'typescript', adapterIds: ['eslint'], }); assert.strictEqual(report.linterDiagnostics[0].message, '未使用变量'); assert.strictEqual(report.linterDiagnostics[0].suggestion, '删除 x'); }); test('mergeResults prefers exact ruleId match over normalized', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:no-shadow', message: 'shadow', range: range(1) }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [ { originalRuleId: 'no-shadow', translatedMessage: '归一化命中', translatedSuggestion: '' }, { originalRuleId: 'eslint:no-shadow', translatedMessage: '精确命中', translatedSuggestion: '' }, ], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.strictEqual(report.linterDiagnostics[0].message, '精确命中'); }); test('mergeResults computes aiFixableLinterIndices excluding native fix and sqlfluff', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:no-var', message: 'v', range: range(1), fix: { range: [0, 3], text: 'let' } }, { severity: 'error', ruleId: 'eslint:no-undef', message: 'u', range: range(2) }, { severity: 'error', ruleId: 'sqlfluff:AL01', message: 's', range: range(3) }, { severity: 'error', ruleId: 'pmd:UnusedLocalVariable', message: 'p', range: range(4) }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/a.java', language: 'java', adapterIds: ['pmd'], aiFixAvailable: true, }); assert.strictEqual(report.aiFixAvailable, true); assert.deepStrictEqual(report.fixableLinterIndices, [0]); assert.deepStrictEqual(report.aiFixableLinterIndices, [1, 3]); }); test('mergeResults aiFixableLinterIndices empty when AI unavailable', () => { const staticDiags: LinterDiagnostic[] = [ { severity: 'error', ruleId: 'eslint:no-undef', message: 'u', range: range(2) }, ]; const report = mergeResults({ staticDiagnostics: staticDiags, customRuleResults: [], translatedDiagnostics: [], aiFindings: [], errors: [], degraded: false, startTime: Date.now(), filePath: '/test/sample.js', language: 'javascript', adapterIds: ['eslint'], }); assert.deepStrictEqual(report.aiFixableLinterIndices, []); assert.strictEqual(report.aiFixAvailable, false); }); });