Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SHA256.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ make sure that their SHA values match the values in the list below.
shasum -a 256 <location_of_the_downloaded_file>

3. Confirm that the SHA in your output matches the value in this list of SHAs.
92d8b8bf23aec05328349ceb9b471fd004fe3d530f584b066d56cca6bf41c009 ./extensions/sfdx-code-analyzer-vscode-1.10.0.vsix
788e642e64081d15b52d4b68e6414ca32d519ae0d16dc2100c742ca9069ddec3 ./extensions/sfdx-code-analyzer-vscode-1.11.0.vsix
4. Change the filename extension for the file that you downloaded from .zip to
.vsix.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"color": "#ECECEC",
"theme": "light"
},
"version": "1.11.0",
"version": "1.12.0",
"publisher": "salesforce",
"license": "BSD-3-Clause",
"engines": {
Expand Down
5 changes: 3 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<SFCAEx


// =================================================================================================================
// == Agentforce for Developers Integration
// == Agentforce Vibes Integration
// =================================================================================================================
const a4dFixAction: A4DFixAction = new A4DFixAction(externalServiceProvider, codeAnalyzer, unifiedDiffService,
diagnosticManager, telemetryService, logger, display);
const a4dFixActionProvider: A4DFixActionProvider = new A4DFixActionProvider(externalServiceProvider, logger);
const a4dFixActionProvider: A4DFixActionProvider = new A4DFixActionProvider(externalServiceProvider, orgConnectionService, logger);

registerCommand(A4DFixAction.COMMAND, async (diagnostic: CodeAnalyzerDiagnostic, document: vscode.TextDocument) => {
await a4dFixAction.run(diagnostic, document);
});
Expand Down
15 changes: 14 additions & 1 deletion src/lib/agentforce/a4d-fix-action-provider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import {messages} from "../messages";
import {LLMServiceProvider} from "../external-services/llm-service";
import {OrgConnectionService} from "../external-services/org-connection-service";
import {Logger} from "../logger";
import {CodeAnalyzerDiagnostic} from "../diagnostics";
import { A4DFixAction } from "./a4d-fix-action";
Expand All @@ -13,11 +14,14 @@ export class A4DFixActionProvider implements vscode.CodeActionProvider {
static readonly providedCodeActionKinds: vscode.CodeActionKind[] = [vscode.CodeActionKind.QuickFix];

private readonly llmServiceProvider: LLMServiceProvider;
private readonly orgConnectionService: OrgConnectionService;
private readonly logger: Logger;
private hasWarnedAboutUnavailableLLMService: boolean = false;
private hasWarnedAboutUnauthenticatedOrg: boolean = false;

constructor(llmServiceProvider: LLMServiceProvider, logger: Logger) {
constructor(llmServiceProvider: LLMServiceProvider, orgConnectionService: OrgConnectionService, logger: Logger) {
this.llmServiceProvider = llmServiceProvider;
this.orgConnectionService = orgConnectionService;
this.logger = logger;
}

Expand All @@ -33,6 +37,15 @@ export class A4DFixActionProvider implements vscode.CodeActionProvider {
return [];
}

// Do not provide quick fix code actions if user is not authenticated to an org
if (!this.orgConnectionService.isAuthed()) {
if (!this.hasWarnedAboutUnauthenticatedOrg) {
this.logger.warn(messages.agentforce.a4dQuickFixUnauthenticatedOrg);
this.hasWarnedAboutUnauthenticatedOrg = true;
}
return [];
}

// Do not provide quick fix code actions if LLM service is not available. We warn once to let user know.
if (!(await this.llmServiceProvider.isLLMServiceAvailable())) {
if (!this.hasWarnedAboutUnavailableLLMService) {
Expand Down
39 changes: 38 additions & 1 deletion src/lib/agentforce/a4d-fix-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,43 @@ export class A4DFixAction extends SuggestFixWithDiffAction {
return Constants.TELEM_A4D_SUGGESTION_FAILED;
}

/**
* Parses JSON from LLM response text that may contain extra formatting or text.
* Handles common cases like:
* - Markdown code blocks (```json ... ```)
* - Extra text before or after the JSON
* - Malformed responses with partial text
* @param responseText The raw response text from the LLM
* @returns Parsed JSON object
* @throws Error if no valid JSON can be extracted
*/
private parseJSON(responseText: string): LLMResponse {
// First, try parsing the response as-is
try {
return JSON.parse(responseText) as LLMResponse;
} catch {
// If that fails, try to extract JSON from the response
}

// Remove leading/trailing whitespace
const cleanedText = responseText.trim();

// Try to find JSON object boundaries in the text
const jsonStartIndex = cleanedText.indexOf('{');
const jsonEndIndex = cleanedText.lastIndexOf('}');

if (jsonStartIndex !== -1 && jsonEndIndex !== -1 && jsonEndIndex > jsonStartIndex) {
const potentialJson = cleanedText.substring(jsonStartIndex, jsonEndIndex + 1);
try {
return JSON.parse(potentialJson) as LLMResponse;
} catch {
// Continue to other methods if this fails
}
}

throw new Error(`Unable to extract valid JSON from response: ${responseText.substring(0, 200)}...`);
}

/**
* Returns suggested replacement code for the entire document that should fix the violation associated with the diagnostic (using A4D).
* @param document
Expand Down Expand Up @@ -102,7 +139,7 @@ export class A4DFixAction extends SuggestFixWithDiffAction {

let llmResponse: LLMResponse;
try {
llmResponse = JSON.parse(llmResponseText) as LLMResponse;
llmResponse = this.parseJSON(llmResponseText);
} catch (error) {
throw new Error(`Response from LLM is not valid JSON: ${getErrorMessage(error)}`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/external-services/llm-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface LLMServiceProvider {


export class LiveLLMService implements LLMService {
// Delegates to the "Agentforce for Developers" LLM service
// Delegates to the "Agentforce Vibes" LLM service
private readonly coreLLMService: LLMServiceInterface;
private readonly logger: Logger;
private uuidGenerator: UUIDGenerator = new RandomUUIDGenerator();
Expand Down
5 changes: 3 additions & 2 deletions src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export const messages = {
processingResults: "Code Analyzer is processing results." // Shared with ApexGuru and CodeAnalyzer
},
agentforce: {
a4dQuickFixUnavailable: "The ability to fix violations with 'Agentforce for Developers' is unavailable since a compatible 'Agentforce for Developers' extension was not found or activated. To enable this functionality, please install the 'Agentforce for Developers' extension and restart VS Code.",
failedA4DResponse: "Unable to receive code fix suggestion from Agentforce for Developers."
a4dQuickFixUnavailable: "The ability to fix violations with 'Agentforce Vibes' is unavailable since a compatible 'Agentforce Vibes' extension was not found or activated. To enable this functionality, please install the 'Agentforce Vibes' extension and restart VS Code.",
a4dQuickFixUnauthenticatedOrg: "The ability to fix violations with 'Agentforce Vibes' is unavailable since you are not authenticated to an org. To enable this functionality, please authenticate to an org.",
failedA4DResponse: "Unable to receive code fix suggestion from Agentforce Vibes."
},
unifiedDiff: {
mustAcceptOrRejectDiffFirst: "You must accept or reject all changes before performing this action.",
Expand Down
138 changes: 126 additions & 12 deletions src/test/unit/lib/agentforce/a4d-fix-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,134 @@ describe('Tests for A4DFixAction', () => {
A4DFixAction.COMMAND);
});

it('When llm response is not valid JSON, then display error message and send exception telemetry event', async () => {
spyLLMService.callLLMReturnValue = 'oops - not json';
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
describe('JSON parsing positive tests', () => {
it('When llm response has JSON with only fixedCode field (explanation is optional), then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = '{"fixedCode": "test code"}';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
expect(display.displayInfoCallHistory).toHaveLength(0); // No explanation provided
});

expect(display.displayErrorCallHistory).toHaveLength(1);
expect(display.displayErrorCallHistory[0].msg).toContain(`Response from LLM is not valid JSON`);
it('When llm response has JSON with additional fields but still has a fixedCode field, then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = '{"fixedCode": "test code", "additionalField": "additional value"}';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(telemetryService.sendExceptionCallHistory).toHaveLength(1);
expect(telemetryService.sendExceptionCallHistory[0].errorMessage).toContain(
`Response from LLM is not valid JSON`);
expect(telemetryService.sendExceptionCallHistory[0].name).toEqual(
'sfdx__eGPT_suggest_failure');
expect(telemetryService.sendExceptionCallHistory[0].properties['executedCommand']).toEqual(
A4DFixAction.COMMAND);
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
expect(display.displayInfoCallHistory).toHaveLength(0); // No explanation provided
});

it('When llm response is JSON in markdown code blocks with json language specifier, then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = '```json\n{"fixedCode": "fixed code", "explanation": "explanation"}\n```';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('fixed code');
expect(display.displayInfoCallHistory).toHaveLength(1);
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: explanation');
});

it('When llm response is JSON in markdown code blocks without language specifier, then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = '```\n{"fixedCode": "fixed code", "explanation": "explanation"}\n```';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('fixed code');
expect(display.displayInfoCallHistory).toHaveLength(1);
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: explanation');
});

it('When llm response has extra text before JSON (like "apist"), then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = 'apist\n{\n "explanation": "Added ApexDoc comment to the class",\n "fixedCode": "/**\\n * This class demonstrates bad practices.\\n */\\npublic class Test {}"\n}';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('This class demonstrates bad practices');
expect(display.displayInfoCallHistory).toHaveLength(1);
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: Added ApexDoc comment to the class');
});

it('When llm response has extra text before and after JSON, then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = 'some extra text here{"fixedCode": "test code", "explanation": "test explanation"}\nsome extra text here';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
expect(display.displayInfoCallHistory).toHaveLength(1);
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: test explanation');
});

it('When llm response is JSON wrapped in single quotes and markdown, then fix is suggested successfully', async () => {
const complexResponse = `' \`\`\`json{ "explanation": "Added ApexDoc comment to the class to satisfy the ApexDoc rule requirement for public classes.", "fixedCode": "/**\\\\n * This class demonstrates bad cryptographic practices.\\\\n */\\\\npublic without sharing class ApexBadCrypto {\\\\n Blob hardCodedIV = Blob.valueOf('Hardcoded IV 123');\\\\n Blob hardCodedKey = Blob.valueOf('0000000000000000');\\\\n Blob data = Blob.valueOf('Data to be encrypted');\\\\n Blob encrypted = Crypto.encrypt('AES128', hardCodedKey, hardCodedIV, data);\\\\n}"}\`\`\`'`;
spyLLMService.callLLMReturnValue = complexResponse;

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('public without sharing class ApexBadCrypto');
expect(display.displayInfoCallHistory).toHaveLength(1);
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: Added ApexDoc comment to the class to satisfy the ApexDoc rule requirement for public classes.');
});

it('When llm response has leading whitespace and newlines, then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = '\n\n \n{"fixedCode": "test code", "explanation": "test explanation"} \n\n';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
expect(display.displayInfoCallHistory).toHaveLength(1);
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: test explanation');
});

it('When llm response has JSON with nested braces and trailing content, then fix is suggested successfully', async () => {
spyLLMService.callLLMReturnValue = '{"fixedCode": "code with {nested} braces", "explanation": "test explanation"} and trailing text here';

await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('code with {nested} braces');
expect(display.displayInfoCallHistory).toHaveLength(1);
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: test explanation');
});
});
describe('JSON parsing negative tests', () => {
it.each([
//no JSON at all
'This is just plain text with no JSON at all',
//multiple JSON objects with text between them
'{"wrong": "object"} some text {"fixedCode": "test code", "explanation": "test explanation"} more text',
//JSON with missing opening brace
'"fixedCode": "test code", "explanation": "test explanation"}',
//JSON with missing closing brace
'{"fixedCode": "test code", "explanation": "test explanation"',
//JSON with missing quote and brace
'{"fixedCode": "test code", "explanation": "missing closing quote and brace',
])('When llm response is not valid, then display error message and send exception telemetry event', async (response: string) => {
spyLLMService.callLLMReturnValue = response;
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);

expect(display.displayErrorCallHistory).toHaveLength(1);
expect(display.displayErrorCallHistory[0].msg).toContain('Response from LLM');
expect(telemetryService.sendExceptionCallHistory).toHaveLength(1);
expect(telemetryService.sendExceptionCallHistory[0].errorMessage).toContain('Unable to extract valid JSON from response');

expect(telemetryService.sendExceptionCallHistory).toHaveLength(1);
expect(telemetryService.sendExceptionCallHistory[0].errorMessage).toContain(
`Response from LLM is not valid JSON`);
expect(telemetryService.sendExceptionCallHistory[0].name).toEqual(
'sfdx__eGPT_suggest_failure');
expect(telemetryService.sendExceptionCallHistory[0].properties['executedCommand']).toEqual(
A4DFixAction.COMMAND);
});
});

it('When fix is suggested, then the diff is displayed, the diagnostic is cleared, and a telemetry event is sent', async () => {
Expand Down
Loading