import * as assert from 'assert'; import * as vscode from 'vscode'; import { AIProvider, ChatOptions } from '../src/ai/providers/base'; import { aiFixReviewIssue } from '../src/fix/customFixEngine'; import type { ReviewIssueInput } from '../src/fix/fixPrompt'; class MockReviewFixProvider 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 ?? '{}'; } } const options: ChatOptions = { model: 'mock', temperature: 0, maxTokens: 100, timeoutMs: 1000 }; function reviewDiag(message = 'bad code', line = 0): ReviewIssueInput { return { ruleId: 'custom:test-rule', line, message, suggestion: 'use GOOD instead' }; } suite('Custom FixEngine Tests', () => { test('aiFixReviewIssue applies AI fix and converges after verify', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const provider = new MockReviewFixProvider([ '{"originalText":"BAD","newText":"GOOD"}', '{"fixed":true}', ]); const result = await aiFixReviewIssue(doc, reviewDiag(), 3, provider, options, true); assert.strictEqual(result.success, true); assert.strictEqual(result.newText, 'const x = GOOD;\n'); assert.strictEqual(result.appliedFixes.length, 1); }); test('aiFixReviewIssue returns ai-match-failed when originalText not found', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const provider = new MockReviewFixProvider(['{"originalText":"NOPE","newText":"GOOD"}']); const result = await aiFixReviewIssue(doc, reviewDiag(), 3, provider, options, true); assert.strictEqual(result.success, false); assert.strictEqual(result.message, 'ai-match-failed'); }); test('aiFixReviewIssue retries until verify passes within maxIterations', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const provider = new MockReviewFixProvider([ '{"originalText":"BAD","newText":"BAD2"}', '{"fixed":false,"reason":"still bad"}', '{"originalText":"BAD2","newText":"GOOD"}', '{"fixed":true}', ]); const result = await aiFixReviewIssue(doc, reviewDiag(), 3, provider, options, true); assert.strictEqual(result.success, true); assert.strictEqual(result.appliedFixes.length, 2); assert.strictEqual(result.newText, 'const x = GOOD;\n'); }); test('aiFixReviewIssue accepts last fix when verify never passes', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const provider = new MockReviewFixProvider([ '{"originalText":"BAD","newText":"GOOD"}', '{"fixed":false,"reason":"still bad"}', ]); const result = await aiFixReviewIssue(doc, reviewDiag(), 1, provider, options, true); assert.strictEqual(result.success, true); assert.strictEqual(result.newText, 'const x = GOOD;\n'); assert.strictEqual(result.appliedFixes.length, 1); }); test('aiFixReviewIssue fails with ai-no-fix when AI provides empty fix', async () => { const doc = await vscode.workspace.openTextDocument({ content: 'const x = BAD;\n', language: 'javascript', }); const provider = new MockReviewFixProvider([ '{"originalText":"","newText":""}', '{"originalText":"","newText":""}', ]); const result = await aiFixReviewIssue(doc, reviewDiag(), 3, provider, options, true); assert.strictEqual(result.success, false); assert.strictEqual(result.message, 'ai-no-fix'); }); });