Skip to content

Commit 10522a9

Browse files
authored
Merge pull request #296 from forcedotcom/release-1.12.0
RELEASE @W-19785605@ Releasing v1.12.0
2 parents 5c883db + b109123 commit 10522a9

11 files changed

Lines changed: 256 additions & 28 deletions

File tree

SHA256.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ make sure that their SHA values match the values in the list below.
1515
shasum -a 256 <location_of_the_downloaded_file>
1616

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

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"color": "#ECECEC",
1616
"theme": "light"
1717
},
18-
"version": "1.11.0",
18+
"version": "1.12.0",
1919
"publisher": "salesforce",
2020
"license": "BSD-3-Clause",
2121
"engines": {

src/extension.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<SFCAEx
273273

274274

275275
// =================================================================================================================
276-
// == Agentforce for Developers Integration
276+
// == Agentforce Vibes Integration
277277
// =================================================================================================================
278278
const a4dFixAction: A4DFixAction = new A4DFixAction(externalServiceProvider, codeAnalyzer, unifiedDiffService,
279279
diagnosticManager, telemetryService, logger, display);
280-
const a4dFixActionProvider: A4DFixActionProvider = new A4DFixActionProvider(externalServiceProvider, logger);
280+
const a4dFixActionProvider: A4DFixActionProvider = new A4DFixActionProvider(externalServiceProvider, orgConnectionService, logger);
281+
281282
registerCommand(A4DFixAction.COMMAND, async (diagnostic: CodeAnalyzerDiagnostic, document: vscode.TextDocument) => {
282283
await a4dFixAction.run(diagnostic, document);
283284
});

src/lib/agentforce/a4d-fix-action-provider.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode";
22
import {messages} from "../messages";
33
import {LLMServiceProvider} from "../external-services/llm-service";
4+
import {OrgConnectionService} from "../external-services/org-connection-service";
45
import {Logger} from "../logger";
56
import {CodeAnalyzerDiagnostic} from "../diagnostics";
67
import { A4DFixAction } from "./a4d-fix-action";
@@ -13,11 +14,14 @@ export class A4DFixActionProvider implements vscode.CodeActionProvider {
1314
static readonly providedCodeActionKinds: vscode.CodeActionKind[] = [vscode.CodeActionKind.QuickFix];
1415

1516
private readonly llmServiceProvider: LLMServiceProvider;
17+
private readonly orgConnectionService: OrgConnectionService;
1618
private readonly logger: Logger;
1719
private hasWarnedAboutUnavailableLLMService: boolean = false;
20+
private hasWarnedAboutUnauthenticatedOrg: boolean = false;
1821

19-
constructor(llmServiceProvider: LLMServiceProvider, logger: Logger) {
22+
constructor(llmServiceProvider: LLMServiceProvider, orgConnectionService: OrgConnectionService, logger: Logger) {
2023
this.llmServiceProvider = llmServiceProvider;
24+
this.orgConnectionService = orgConnectionService;
2125
this.logger = logger;
2226
}
2327

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

40+
// Do not provide quick fix code actions if user is not authenticated to an org
41+
if (!this.orgConnectionService.isAuthed()) {
42+
if (!this.hasWarnedAboutUnauthenticatedOrg) {
43+
this.logger.warn(messages.agentforce.a4dQuickFixUnauthenticatedOrg);
44+
this.hasWarnedAboutUnauthenticatedOrg = true;
45+
}
46+
return [];
47+
}
48+
3649
// Do not provide quick fix code actions if LLM service is not available. We warn once to let user know.
3750
if (!(await this.llmServiceProvider.isLLMServiceAvailable())) {
3851
if (!this.hasWarnedAboutUnavailableLLMService) {

src/lib/agentforce/a4d-fix-action.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,43 @@ export class A4DFixAction extends SuggestFixWithDiffAction {
5252
return Constants.TELEM_A4D_SUGGESTION_FAILED;
5353
}
5454

55+
/**
56+
* Parses JSON from LLM response text that may contain extra formatting or text.
57+
* Handles common cases like:
58+
* - Markdown code blocks (```json ... ```)
59+
* - Extra text before or after the JSON
60+
* - Malformed responses with partial text
61+
* @param responseText The raw response text from the LLM
62+
* @returns Parsed JSON object
63+
* @throws Error if no valid JSON can be extracted
64+
*/
65+
private parseJSON(responseText: string): LLMResponse {
66+
// First, try parsing the response as-is
67+
try {
68+
return JSON.parse(responseText) as LLMResponse;
69+
} catch {
70+
// If that fails, try to extract JSON from the response
71+
}
72+
73+
// Remove leading/trailing whitespace
74+
const cleanedText = responseText.trim();
75+
76+
// Try to find JSON object boundaries in the text
77+
const jsonStartIndex = cleanedText.indexOf('{');
78+
const jsonEndIndex = cleanedText.lastIndexOf('}');
79+
80+
if (jsonStartIndex !== -1 && jsonEndIndex !== -1 && jsonEndIndex > jsonStartIndex) {
81+
const potentialJson = cleanedText.substring(jsonStartIndex, jsonEndIndex + 1);
82+
try {
83+
return JSON.parse(potentialJson) as LLMResponse;
84+
} catch {
85+
// Continue to other methods if this fails
86+
}
87+
}
88+
89+
throw new Error(`Unable to extract valid JSON from response: ${responseText.substring(0, 200)}...`);
90+
}
91+
5592
/**
5693
* Returns suggested replacement code for the entire document that should fix the violation associated with the diagnostic (using A4D).
5794
* @param document
@@ -102,7 +139,7 @@ export class A4DFixAction extends SuggestFixWithDiffAction {
102139

103140
let llmResponse: LLMResponse;
104141
try {
105-
llmResponse = JSON.parse(llmResponseText) as LLMResponse;
142+
llmResponse = this.parseJSON(llmResponseText);
106143
} catch (error) {
107144
throw new Error(`Response from LLM is not valid JSON: ${getErrorMessage(error)}`);
108145
}

src/lib/external-services/llm-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface LLMServiceProvider {
1717

1818

1919
export class LiveLLMService implements LLMService {
20-
// Delegates to the "Agentforce for Developers" LLM service
20+
// Delegates to the "Agentforce Vibes" LLM service
2121
private readonly coreLLMService: LLMServiceInterface;
2222
private readonly logger: Logger;
2323
private uuidGenerator: UUIDGenerator = new RandomUUIDGenerator();

src/lib/messages.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ export const messages = {
1414
processingResults: "Code Analyzer is processing results." // Shared with ApexGuru and CodeAnalyzer
1515
},
1616
agentforce: {
17-
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.",
18-
failedA4DResponse: "Unable to receive code fix suggestion from Agentforce for Developers."
17+
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.",
18+
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.",
19+
failedA4DResponse: "Unable to receive code fix suggestion from Agentforce Vibes."
1920
},
2021
unifiedDiff: {
2122
mustAcceptOrRejectDiffFirst: "You must accept or reject all changes before performing this action.",

src/test/unit/lib/agentforce/a4d-fix-action.test.ts

Lines changed: 126 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -159,20 +159,134 @@ describe('Tests for A4DFixAction', () => {
159159
A4DFixAction.COMMAND);
160160
});
161161

162-
it('When llm response is not valid JSON, then display error message and send exception telemetry event', async () => {
163-
spyLLMService.callLLMReturnValue = 'oops - not json';
164-
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
162+
describe('JSON parsing positive tests', () => {
163+
it('When llm response has JSON with only fixedCode field (explanation is optional), then fix is suggested successfully', async () => {
164+
spyLLMService.callLLMReturnValue = '{"fixedCode": "test code"}';
165+
166+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
167+
168+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
169+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
170+
expect(display.displayInfoCallHistory).toHaveLength(0); // No explanation provided
171+
});
165172

166-
expect(display.displayErrorCallHistory).toHaveLength(1);
167-
expect(display.displayErrorCallHistory[0].msg).toContain(`Response from LLM is not valid JSON`);
173+
it('When llm response has JSON with additional fields but still has a fixedCode field, then fix is suggested successfully', async () => {
174+
spyLLMService.callLLMReturnValue = '{"fixedCode": "test code", "additionalField": "additional value"}';
175+
176+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
168177

169-
expect(telemetryService.sendExceptionCallHistory).toHaveLength(1);
170-
expect(telemetryService.sendExceptionCallHistory[0].errorMessage).toContain(
171-
`Response from LLM is not valid JSON`);
172-
expect(telemetryService.sendExceptionCallHistory[0].name).toEqual(
173-
'sfdx__eGPT_suggest_failure');
174-
expect(telemetryService.sendExceptionCallHistory[0].properties['executedCommand']).toEqual(
175-
A4DFixAction.COMMAND);
178+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
179+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
180+
expect(display.displayInfoCallHistory).toHaveLength(0); // No explanation provided
181+
});
182+
183+
it('When llm response is JSON in markdown code blocks with json language specifier, then fix is suggested successfully', async () => {
184+
spyLLMService.callLLMReturnValue = '```json\n{"fixedCode": "fixed code", "explanation": "explanation"}\n```';
185+
186+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
187+
188+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
189+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('fixed code');
190+
expect(display.displayInfoCallHistory).toHaveLength(1);
191+
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: explanation');
192+
});
193+
194+
it('When llm response is JSON in markdown code blocks without language specifier, then fix is suggested successfully', async () => {
195+
spyLLMService.callLLMReturnValue = '```\n{"fixedCode": "fixed code", "explanation": "explanation"}\n```';
196+
197+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
198+
199+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
200+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('fixed code');
201+
expect(display.displayInfoCallHistory).toHaveLength(1);
202+
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: explanation');
203+
});
204+
205+
it('When llm response has extra text before JSON (like "apist"), then fix is suggested successfully', async () => {
206+
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}';
207+
208+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
209+
210+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
211+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('This class demonstrates bad practices');
212+
expect(display.displayInfoCallHistory).toHaveLength(1);
213+
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: Added ApexDoc comment to the class');
214+
});
215+
216+
it('When llm response has extra text before and after JSON, then fix is suggested successfully', async () => {
217+
spyLLMService.callLLMReturnValue = 'some extra text here{"fixedCode": "test code", "explanation": "test explanation"}\nsome extra text here';
218+
219+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
220+
221+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
222+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
223+
expect(display.displayInfoCallHistory).toHaveLength(1);
224+
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: test explanation');
225+
});
226+
227+
it('When llm response is JSON wrapped in single quotes and markdown, then fix is suggested successfully', async () => {
228+
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}"}\`\`\`'`;
229+
spyLLMService.callLLMReturnValue = complexResponse;
230+
231+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
232+
233+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
234+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('public without sharing class ApexBadCrypto');
235+
expect(display.displayInfoCallHistory).toHaveLength(1);
236+
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: Added ApexDoc comment to the class to satisfy the ApexDoc rule requirement for public classes.');
237+
});
238+
239+
it('When llm response has leading whitespace and newlines, then fix is suggested successfully', async () => {
240+
spyLLMService.callLLMReturnValue = '\n\n \n{"fixedCode": "test code", "explanation": "test explanation"} \n\n';
241+
242+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
243+
244+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
245+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('test code');
246+
expect(display.displayInfoCallHistory).toHaveLength(1);
247+
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: test explanation');
248+
});
249+
250+
it('When llm response has JSON with nested braces and trailing content, then fix is suggested successfully', async () => {
251+
spyLLMService.callLLMReturnValue = '{"fixedCode": "code with {nested} braces", "explanation": "test explanation"} and trailing text here';
252+
253+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
254+
255+
expect(unifiedDiffService.showDiffCallHistory).toHaveLength(1);
256+
expect(unifiedDiffService.showDiffCallHistory[0].newCode).toContain('code with {nested} braces');
257+
expect(display.displayInfoCallHistory).toHaveLength(1);
258+
expect(display.displayInfoCallHistory[0].msg).toEqual('Fix Explanation: test explanation');
259+
});
260+
});
261+
describe('JSON parsing negative tests', () => {
262+
it.each([
263+
//no JSON at all
264+
'This is just plain text with no JSON at all',
265+
//multiple JSON objects with text between them
266+
'{"wrong": "object"} some text {"fixedCode": "test code", "explanation": "test explanation"} more text',
267+
//JSON with missing opening brace
268+
'"fixedCode": "test code", "explanation": "test explanation"}',
269+
//JSON with missing closing brace
270+
'{"fixedCode": "test code", "explanation": "test explanation"',
271+
//JSON with missing quote and brace
272+
'{"fixedCode": "test code", "explanation": "missing closing quote and brace',
273+
])('When llm response is not valid, then display error message and send exception telemetry event', async (response: string) => {
274+
spyLLMService.callLLMReturnValue = response;
275+
await a4dFixAction.run(sampleDiagForSingleLine, sampleDocument);
276+
277+
expect(display.displayErrorCallHistory).toHaveLength(1);
278+
expect(display.displayErrorCallHistory[0].msg).toContain('Response from LLM');
279+
expect(telemetryService.sendExceptionCallHistory).toHaveLength(1);
280+
expect(telemetryService.sendExceptionCallHistory[0].errorMessage).toContain('Unable to extract valid JSON from response');
281+
282+
expect(telemetryService.sendExceptionCallHistory).toHaveLength(1);
283+
expect(telemetryService.sendExceptionCallHistory[0].errorMessage).toContain(
284+
`Response from LLM is not valid JSON`);
285+
expect(telemetryService.sendExceptionCallHistory[0].name).toEqual(
286+
'sfdx__eGPT_suggest_failure');
287+
expect(telemetryService.sendExceptionCallHistory[0].properties['executedCommand']).toEqual(
288+
A4DFixAction.COMMAND);
289+
});
176290
});
177291

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

0 commit comments

Comments
 (0)