import * as assert from 'assert'; import * as vscode from 'vscode'; import { AIProvider, ChatOptions } from '../src/ai/providers/base'; import { LinterAdapter, LinterDiagnostic } from '../src/types'; import { aiFixDiagnostic } from '../src/fix/aiFixEngine'; class MockFixProvider extends AIProvider { id = 'mock'; name = 'mock'; constructor(private responses: string[]) { super('key', 'url'); } async chat(_system: string, _user: string, _options: ChatOptions): Promise { const next = this.responses.shift(); return next ?? '{}'; } } class MockAdapter implements LinterAdapter { id = 'mock'; supportedLanguages = ['javascript']; isAvailable(): boolean { return true; } async check(document: vscode.TextDocument, _workingDir: string) { const text = document.getText(); const diagnostics: LinterDiagnostic[] = []; if (text.includes('BAD')) { diagnostics.push({ severity: 'error', ruleId: 'eslint:mock-rule', message: 'bad code', range: new vscode.Range(0, 0, 0, 3), }); } return { diagnostics, status: 'ok' as const }; } } const options: ChatOptions = { model: 'mock', temperature: 0, maxTokens: 100, timeoutMs: 1000 }; suite('AI FixEngine Tests', () => { test('aiFixDiagnostic applies AI fix and converges', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const diag: LinterDiagnostic = { severity: 'error', ruleId: 'eslint:mock-rule', message: 'bad code', range: new vscode.Range(0, 8, 0, 11), }; const provider = new MockFixProvider(['{"originalText":"BAD","newText":"GOOD"}']); const result = await aiFixDiagnostic(doc, __dirname, new MockAdapter(), diag, 3, provider, options); assert.strictEqual(result.success, true); assert.strictEqual(result.appliedFixes.length, 1); assert.strictEqual(result.appliedFixes[0].originalText, 'BAD'); assert.strictEqual(result.appliedFixes[0].newText, 'GOOD'); }); test('aiFixDiagnostic returns ai-match-failed when originalText not found', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const diag: LinterDiagnostic = { severity: 'error', ruleId: 'eslint:mock-rule', message: 'bad code', range: new vscode.Range(0, 8, 0, 11), }; const provider = new MockFixProvider(['{"originalText":"NOPE","newText":"GOOD"}']); const result = await aiFixDiagnostic(doc, __dirname, new MockAdapter(), diag, 3, provider, options); assert.strictEqual(result.success, false); assert.strictEqual(result.message, 'ai-match-failed'); }); test('aiFixDiagnostic returns ai-no-fix when AI provides empty fix', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const diag: LinterDiagnostic = { severity: 'error', ruleId: 'eslint:mock-rule', message: 'bad code', range: new vscode.Range(0, 8, 0, 11), }; const provider = new MockFixProvider(['{"originalText":"","newText":""}']); const result = await aiFixDiagnostic(doc, __dirname, new MockAdapter(), diag, 3, provider, options); assert.strictEqual(result.success, false); assert.strictEqual(result.message, 'ai-no-fix'); }); test('aiFixDiagnostic retries until issue resolved within maxIterations', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'BAD BAD\n', language: 'javascript', }); const diag: LinterDiagnostic = { severity: 'error', ruleId: 'eslint:mock-rule', message: 'bad code', range: new vscode.Range(0, 0, 0, 3), }; const provider = new MockFixProvider([ '{"originalText":"BAD BAD","newText":"BAD GOOD"}', '{"originalText":"BAD","newText":"GOOD"}', ]); const result = await aiFixDiagnostic(doc, __dirname, new MockAdapter(), diag, 3, provider, options); assert.strictEqual(result.success, true); assert.strictEqual(result.appliedFixes.length, 2); }); });