From 18176bb7a28e5accf5f04267d3bd7410258023e6 Mon Sep 17 00:00:00 2001 From: James Simone <16430727+jamessimone@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:12:02 -0400 Subject: [PATCH 1/9] feat: add support for retrieving invalid classes via new tooling API call in API version 68 --- command-snapshot.json | 11 +- messages/get-invalid-classes.md | 17 + package.json | 2 +- schemas/apex-get-invalid__classes.json | 60 ++++ src/commands/apex/get/invalid-classes.ts | 105 ++++++ .../commands/apex/get/invalid-classes.test.ts | 337 ++++++++++++++++++ tsconfig.json | 3 +- 7 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 messages/get-invalid-classes.md create mode 100644 schemas/apex-get-invalid__classes.json create mode 100644 src/commands/apex/get/invalid-classes.ts create mode 100644 test/commands/apex/get/invalid-classes.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index 8ac0d7a2..74838d21 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,4 +1,12 @@ [ + { + "alias": [], + "command": "apex:get:invalid-classes", + "flagAliases": ["apiversion", "targetusername", "u"], + "flagChars": ["o"], + "flags": ["api-version", "flags-dir", "json", "target-org"], + "plugin": "@salesforce/plugin-apex" + }, { "alias": ["force:apex:log:get"], "command": "apex:get:log", @@ -78,8 +86,8 @@ "flags-dir", "json", "loglevel", - "poll-interval", "output-dir", + "poll-interval", "result-format", "suite-names", "synchronous", @@ -99,6 +107,7 @@ "plugin": "@salesforce/plugin-apex" }, { + "alias": [], "command": "logic:get:test", "flagAliases": [ "apiversion", diff --git a/messages/get-invalid-classes.md b/messages/get-invalid-classes.md new file mode 100644 index 00000000..d5d26de8 --- /dev/null +++ b/messages/get-invalid-classes.md @@ -0,0 +1,17 @@ +# summary + +Fetches Apex classes that are currently invalid. + +# description + +Return compile results for only Apex classes and triggers with validation errors or warnings, instead of recompiling the entire org. + +# examples + +- Fetch invalid Apex classes for a target org + + <%= config.bin %> <%= command.id %> --target-org me@my.org + +# noResultsFound + +No results found diff --git a/package.json b/package.json index 2befe749..4c4951c9 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "description": "Use the apex commands to create Apex classes, execute anonymous blocks, view your logs, run Apex tests, and view Apex test results.", "subtopics": { "get": { - "description": "Get debug logs or test results." + "description": "Get debug logs, invalid Apex classes, or test results." }, "list": { "description": "List debug logs." diff --git a/schemas/apex-get-invalid__classes.json b/schemas/apex-get-invalid__classes.json new file mode 100644 index 00000000..d3fff82d --- /dev/null +++ b/schemas/apex-get-invalid__classes.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/CompilationResult", + "definitions": { + "CompilationResult": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/InvalidApexClassResult" + } + } + }, + "required": ["status", "results"], + "additionalProperties": false + }, + "InvalidApexClassResult": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "success": { + "type": "boolean" + }, + "problems": { + "type": "array", + "items": { + "$ref": "#/definitions/ApexClassProblem" + } + } + }, + "required": ["name", "namespace", "success", "problems"], + "additionalProperties": false + }, + "ApexClassProblem": { + "type": "object", + "properties": { + "line": { + "type": "number" + }, + "column": { + "type": "number" + }, + "message": { + "type": "string" + } + }, + "required": ["line", "column", "message"], + "additionalProperties": false + } + } +} diff --git a/src/commands/apex/get/invalid-classes.ts b/src/commands/apex/get/invalid-classes.ts new file mode 100644 index 00000000..2aacfb9d --- /dev/null +++ b/src/commands/apex/get/invalid-classes.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CancellationTokenSource } from '@salesforce/apex-node'; +import { + orgApiVersionFlagWithDeprecations, + requiredOrgFlagWithDeprecations, + SfCommand, +} from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-apex', 'get-invalid-classes'); + +export type ApexClassProblem = { + line: number; + column: number; + message: string; +}; + +export type InvalidApexClassResult = { + name: string; + namespace: string; + success: boolean; + problems: ApexClassProblem[]; +}; + +export type CompilationResult = { + status: string; + results: InvalidApexClassResult[]; +}; + +export default class GetInvalidClasses extends SfCommand { + public static readonly flags = { + 'target-org': requiredOrgFlagWithDeprecations, + 'api-version': orgApiVersionFlagWithDeprecations, + }; + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + + protected cancellationTokenSource = new CancellationTokenSource(); + + public async run(): Promise { + const { flags } = await this.parse(GetInvalidClasses); + + // graceful shutdown + const exitHandler = async (): Promise => { + await this.cancellationTokenSource.asyncCancel(); + process.exit(); + }; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + process.on('SIGINT', exitHandler); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + process.on('SIGTERM', exitHandler); + + this.spinner.start('Retrieving invalid Apex classes...'); + + const connection = flags['target-org'].getConnection(flags['api-version']); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const invalidApexResponse = (await connection.tooling.request({ + // eslint-disable-next-line no-underscore-dangle + url: `${connection.tooling._baseUrl()}/apexCompileResults`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + })) as CompilationResult; + + // the actual command output is only displayed when --json is passed as a flag + // so we format the results for everyone else + if (!this.jsonEnabled() && invalidApexResponse.results.length > 0) { + if (invalidApexResponse.results.length > 0) { + this.table({ + columns: ['name', 'namespace', 'success', 'problems'], + data: invalidApexResponse.results.map((result) => ({ + ...result, + problems: result.problems.map((problem) => JSON.stringify(problem)), + })), + title: 'Invalid Apex Classes:', + }); + } else { + this.log('No invalid Apex classes found!'); + } + } + + this.spinner.stop(); + return invalidApexResponse; + } +} diff --git a/test/commands/apex/get/invalid-classes.test.ts b/test/commands/apex/get/invalid-classes.test.ts new file mode 100644 index 00000000..ab55110c --- /dev/null +++ b/test/commands/apex/get/invalid-classes.test.ts @@ -0,0 +1,337 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unused-vars */ +import sinon from 'sinon'; +import { expect } from 'chai'; +import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import { Org } from '@salesforce/core'; +import GetInvalidClasses from '../../../../src/commands/apex/get/invalid-classes.js'; +import type { CompilationResult } from '../../../../src/commands/apex/get/invalid-classes.js'; + +describe('apex:get:invalid-classes', () => { + let sandbox: sinon.SinonSandbox; + let uxStub: ReturnType; + + beforeEach(async () => { + sandbox = sinon.createSandbox(); + uxStub = stubSfCommandUx(sandbox); + sandbox.stub(process, 'on').resolves(); + sandbox.stub(process, 'exit'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('returns compilation result with no invalid classes', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + const result = await GetInvalidClasses.run([]); + expect(result).to.deep.equal(mockCompilationResult); + expect(uxStub.log.called).to.be.false; + }); + + it('displays invalid classes in table format without --json flag', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [ + { + name: 'TestClass1', + namespace: '', + success: false, + problems: [ + { + line: 10, + column: 5, + message: 'Unexpected token: }', + }, + ], + }, + { + name: 'TestClass2', + namespace: 'myNamespace', + success: false, + problems: [ + { + line: 25, + column: 1, + message: 'Invalid class declaration', + }, + { + line: 30, + column: 10, + message: 'Syntax error', + }, + ], + }, + ], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + const result = await GetInvalidClasses.run([]); + expect(result).to.deep.equal(mockCompilationResult); + expect(uxStub.table.calledOnce).to.be.true; + + const tableCall = uxStub.table.firstCall; + expect(tableCall.args[0]).to.deep.include({ + columns: ['name', 'namespace', 'success', 'problems'], + title: 'Invalid Apex Classes:', + }); + + const tableData = (tableCall.args[0] as Record).data as Array>; + expect(tableData).to.have.lengthOf(2); + expect(tableData[0].name).to.equal('TestClass1'); + expect((tableData[0].problems as string[])[0]).to.be.a('string'); + expect(JSON.parse((tableData[0].problems as string[])[0])).to.deep.equal({ + line: 10, + column: 5, + message: 'Unexpected token: }', + }); + }); + + it('returns compilation result with --json flag without displaying table', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [ + { + name: 'TestClass1', + namespace: '', + success: false, + problems: [ + { + line: 10, + column: 5, + message: 'Unexpected token: }', + }, + ], + }, + ], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + sandbox.stub(GetInvalidClasses.prototype, 'jsonEnabled').returns(true); + + const result = await GetInvalidClasses.run(['--json']); + expect(result).to.deep.equal(mockCompilationResult); + expect(uxStub.table.called).to.be.false; + }); + + it('makes correct API request to apexCompileResults endpoint', async () => { + const requestStub = sandbox.stub().resolves({ + status: 'success', + results: [], + }); + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: requestStub, + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + await GetInvalidClasses.run([]); + + expect(requestStub.calledOnce).to.be.true; + const callArgs = requestStub.firstCall.args[0]; + expect(callArgs.url).to.include('/apexCompileResults'); + expect(callArgs.method).to.equal('POST'); + expect(callArgs.headers['Content-Type']).to.equal('application/json'); + expect(callArgs.body).to.equal('{}'); + }); + + it('registers signal handlers for graceful shutdown', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + await GetInvalidClasses.run([]); + + expect((process.on as sinon.SinonStub).calledWith('SIGINT')).to.be.true; + expect((process.on as sinon.SinonStub).calledWith('SIGTERM')).to.be.true; + }); + + it('handles multiple problems per class', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [ + { + name: 'ComplexClass', + namespace: 'testNamespace', + success: false, + problems: [ + { + line: 5, + column: 1, + message: 'Error 1', + }, + { + line: 15, + column: 20, + message: 'Error 2', + }, + { + line: 42, + column: 8, + message: 'Error 3', + }, + ], + }, + ], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + const result = await GetInvalidClasses.run([]); + expect(result.results[0].problems).to.have.lengthOf(3); + expect(result.results[0].problems[1].line).to.equal(15); + expect(result.results[0].problems[2].message).to.equal('Error 3'); + }); + + it('handles classes with empty namespace', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [ + { + name: 'LocalClass', + namespace: '', + success: false, + problems: [ + { + line: 1, + column: 1, + message: 'Error', + }, + ], + }, + ], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + const result = await GetInvalidClasses.run([]); + expect(result.results[0].namespace).to.equal(''); + }); + + it('converts problem objects to JSON strings in table display', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [ + { + name: 'TestClass', + namespace: '', + success: false, + problems: [ + { + line: 10, + column: 5, + message: 'Test message', + }, + ], + }, + ], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + await GetInvalidClasses.run([]); + + const tableCall = uxStub.table.firstCall; + const tableData = (tableCall.args[0] as Record).data as Array>; + const problemString = (tableData[0].problems as string[])[0]; + + expect(problemString).to.be.a('string'); + const parsedProblem = JSON.parse(problemString); + expect(parsedProblem.line).to.equal(10); + expect(parsedProblem.column).to.equal(5); + expect(parsedProblem.message).to.equal('Test message'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index e21f437d..63065757 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "outDir": "lib", "rootDir": "src", "skipLibCheck": true, - "strictNullChecks": true + "strictNullChecks": true, + "types": ["mocha"] }, "include": ["./src/**/*.ts"] } From 7a0518ac0b9a4d955bcb49d1cc9ecb4cbcb6d691 Mon Sep 17 00:00:00 2001 From: James Simone <16430727+jamessimone@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:10:29 -0400 Subject: [PATCH 2/9] feat: pre-PR feedback on command name --- README.md | 34 +++++++++++++++++++ command-snapshot.json | 2 +- ...valid-classes.md => get-compile-status.md} | 2 +- package.json | 2 +- ...ses.json => apex-get-compile__status.json} | 0 .../{invalid-classes.ts => compile-status.ts} | 6 ++-- ...classes.test.ts => compile-status.test.ts} | 24 ++++++------- 7 files changed, 52 insertions(+), 18 deletions(-) rename messages/{get-invalid-classes.md => get-compile-status.md} (80%) rename schemas/{apex-get-invalid__classes.json => apex-get-compile__status.json} (100%) rename src/commands/apex/get/{invalid-classes.ts => compile-status.ts} (95%) rename test/commands/apex/get/{invalid-classes.test.ts => compile-status.test.ts} (93%) diff --git a/README.md b/README.md index 6a1c4501..362b6f1c 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Happy debugging! +- [`sf apex get compile-status`](#sf-apex-get-compile-status) - [`sf apex get log`](#sf-apex-get-log) - [`sf apex get test`](#sf-apex-get-test) - [`sf apex list log`](#sf-apex-list-log) @@ -96,6 +97,39 @@ Happy debugging! - [`sf logic get test`](#sf-logic-get-test) - [`sf logic run test`](#sf-logic-run-test) +## `sf apex get compile-status` + +Fetches any Apex classes in your org with compilation status issues. + +``` +USAGE + $ sf apex get compile-status -o [--json] [--flags-dir ] [--api-version ] + +FLAGS + -o, --target-org= (required) Username or alias of the target org. Not required if the `target-org` + configuration variable is already set. + --api-version= Override the api version used for api requests made by this command + +GLOBAL FLAGS + --flags-dir= Import flag values from a directory. + --json Format output as json. + +DESCRIPTION + Fetches invalid Apex classes, including the problems associated with their current compilation status. + + +EXAMPLES + Fetches invalid classes in your default org: + + $ sf apex get compile-status + + Fetch invalid classes in the org with the specified username: + + $ sf apex get compile-status --target-org me@my.org +``` + +_See code: [src/commands/apex/get/log.ts](https://github.com/salesforcecli/plugin-apex/blob/4.1.0/src/commands/apex/get/log.ts)_ + ## `sf apex get log` Fetch the specified log or given number of most recent logs from the org. diff --git a/command-snapshot.json b/command-snapshot.json index 74838d21..e0755202 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,7 +1,7 @@ [ { "alias": [], - "command": "apex:get:invalid-classes", + "command": "apex:get:compile-status", "flagAliases": ["apiversion", "targetusername", "u"], "flagChars": ["o"], "flags": ["api-version", "flags-dir", "json", "target-org"], diff --git a/messages/get-invalid-classes.md b/messages/get-compile-status.md similarity index 80% rename from messages/get-invalid-classes.md rename to messages/get-compile-status.md index d5d26de8..9699d72d 100644 --- a/messages/get-invalid-classes.md +++ b/messages/get-compile-status.md @@ -1,6 +1,6 @@ # summary -Fetches Apex classes that are currently invalid. +Fetches the compilation status for Apex classes that are currently invalid. # description diff --git a/package.json b/package.json index 4c4951c9..6a4de5c4 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "description": "Use the apex commands to create Apex classes, execute anonymous blocks, view your logs, run Apex tests, and view Apex test results.", "subtopics": { "get": { - "description": "Get debug logs, invalid Apex classes, or test results." + "description": "Get debug logs, Apex class compile status, or test results." }, "list": { "description": "List debug logs." diff --git a/schemas/apex-get-invalid__classes.json b/schemas/apex-get-compile__status.json similarity index 100% rename from schemas/apex-get-invalid__classes.json rename to schemas/apex-get-compile__status.json diff --git a/src/commands/apex/get/invalid-classes.ts b/src/commands/apex/get/compile-status.ts similarity index 95% rename from src/commands/apex/get/invalid-classes.ts rename to src/commands/apex/get/compile-status.ts index 2aacfb9d..df99cb00 100644 --- a/src/commands/apex/get/invalid-classes.ts +++ b/src/commands/apex/get/compile-status.ts @@ -23,7 +23,7 @@ import { import { Messages } from '@salesforce/core'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-apex', 'get-invalid-classes'); +const messages = Messages.loadMessages('@salesforce/plugin-apex', 'get-compile-status'); export type ApexClassProblem = { line: number; @@ -43,7 +43,7 @@ export type CompilationResult = { results: InvalidApexClassResult[]; }; -export default class GetInvalidClasses extends SfCommand { +export default class GetCompileStatus extends SfCommand { public static readonly flags = { 'target-org': requiredOrgFlagWithDeprecations, 'api-version': orgApiVersionFlagWithDeprecations, @@ -55,7 +55,7 @@ export default class GetInvalidClasses extends SfCommand { protected cancellationTokenSource = new CancellationTokenSource(); public async run(): Promise { - const { flags } = await this.parse(GetInvalidClasses); + const { flags } = await this.parse(GetCompileStatus); // graceful shutdown const exitHandler = async (): Promise => { diff --git a/test/commands/apex/get/invalid-classes.test.ts b/test/commands/apex/get/compile-status.test.ts similarity index 93% rename from test/commands/apex/get/invalid-classes.test.ts rename to test/commands/apex/get/compile-status.test.ts index ab55110c..f5179f55 100644 --- a/test/commands/apex/get/invalid-classes.test.ts +++ b/test/commands/apex/get/compile-status.test.ts @@ -18,10 +18,10 @@ import sinon from 'sinon'; import { expect } from 'chai'; import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; import { Org } from '@salesforce/core'; -import GetInvalidClasses from '../../../../src/commands/apex/get/invalid-classes.js'; -import type { CompilationResult } from '../../../../src/commands/apex/get/invalid-classes.js'; +import GetCompileStatus from '../../../../src/commands/apex/get/compile-status.js'; +import type { CompilationResult } from '../../../../src/commands/apex/get/compile-status.js'; -describe('apex:get:invalid-classes', () => { +describe('apex:get:compile-status', () => { let sandbox: sinon.SinonSandbox; let uxStub: ReturnType; @@ -53,7 +53,7 @@ describe('apex:get:invalid-classes', () => { sandbox.stub(Org, 'create').resolves(org as unknown as Org); - const result = await GetInvalidClasses.run([]); + const result = await GetCompileStatus.run([]); expect(result).to.deep.equal(mockCompilationResult); expect(uxStub.log.called).to.be.false; }); @@ -105,7 +105,7 @@ describe('apex:get:invalid-classes', () => { sandbox.stub(Org, 'create').resolves(org as unknown as Org); - const result = await GetInvalidClasses.run([]); + const result = await GetCompileStatus.run([]); expect(result).to.deep.equal(mockCompilationResult); expect(uxStub.table.calledOnce).to.be.true; @@ -155,9 +155,9 @@ describe('apex:get:invalid-classes', () => { }; sandbox.stub(Org, 'create').resolves(org as unknown as Org); - sandbox.stub(GetInvalidClasses.prototype, 'jsonEnabled').returns(true); + sandbox.stub(GetCompileStatus.prototype, 'jsonEnabled').returns(true); - const result = await GetInvalidClasses.run(['--json']); + const result = await GetCompileStatus.run(['--json']); expect(result).to.deep.equal(mockCompilationResult); expect(uxStub.table.called).to.be.false; }); @@ -179,7 +179,7 @@ describe('apex:get:invalid-classes', () => { sandbox.stub(Org, 'create').resolves(org as unknown as Org); - await GetInvalidClasses.run([]); + await GetCompileStatus.run([]); expect(requestStub.calledOnce).to.be.true; const callArgs = requestStub.firstCall.args[0]; @@ -206,7 +206,7 @@ describe('apex:get:invalid-classes', () => { sandbox.stub(Org, 'create').resolves(org as unknown as Org); - await GetInvalidClasses.run([]); + await GetCompileStatus.run([]); expect((process.on as sinon.SinonStub).calledWith('SIGINT')).to.be.true; expect((process.on as sinon.SinonStub).calledWith('SIGTERM')).to.be.true; @@ -252,7 +252,7 @@ describe('apex:get:invalid-classes', () => { sandbox.stub(Org, 'create').resolves(org as unknown as Org); - const result = await GetInvalidClasses.run([]); + const result = await GetCompileStatus.run([]); expect(result.results[0].problems).to.have.lengthOf(3); expect(result.results[0].problems[1].line).to.equal(15); expect(result.results[0].problems[2].message).to.equal('Error 3'); @@ -288,7 +288,7 @@ describe('apex:get:invalid-classes', () => { sandbox.stub(Org, 'create').resolves(org as unknown as Org); - const result = await GetInvalidClasses.run([]); + const result = await GetCompileStatus.run([]); expect(result.results[0].namespace).to.equal(''); }); @@ -322,7 +322,7 @@ describe('apex:get:invalid-classes', () => { sandbox.stub(Org, 'create').resolves(org as unknown as Org); - await GetInvalidClasses.run([]); + await GetCompileStatus.run([]); const tableCall = uxStub.table.firstCall; const tableData = (tableCall.args[0] as Record).data as Array>; From d1478a5c550dd0a7881a2addd1028d6100dfdc3f Mon Sep 17 00:00:00 2001 From: James Simone <16430727+jamessimone@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:20:17 -0400 Subject: [PATCH 3/9] feat: add compiler warnings to output --- messages/get-compile-status.md | 4 +-- schemas/apex-get-compile__status.json | 8 ++++- src/commands/apex/get/compile-status.ts | 14 +++++---- test/commands/apex/get/compile-status.test.ts | 30 +++++++++++++++---- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/messages/get-compile-status.md b/messages/get-compile-status.md index 9699d72d..58a9afb3 100644 --- a/messages/get-compile-status.md +++ b/messages/get-compile-status.md @@ -1,6 +1,6 @@ # summary -Fetches the compilation status for Apex classes that are currently invalid. +Fetches the compilation status for Apex classes that are currently invalid or have compilation warnings. # description @@ -8,7 +8,7 @@ Return compile results for only Apex classes and triggers with validation errors # examples -- Fetch invalid Apex classes for a target org +- Fetch invalid Apex classes or classes that have compilation warnings for a target org <%= config.bin %> <%= command.id %> --target-org me@my.org diff --git a/schemas/apex-get-compile__status.json b/schemas/apex-get-compile__status.json index d3fff82d..96e58835 100644 --- a/schemas/apex-get-compile__status.json +++ b/schemas/apex-get-compile__status.json @@ -35,9 +35,15 @@ "items": { "$ref": "#/definitions/ApexClassProblem" } + }, + "warnings": { + "type": "array", + "items": { + "$ref": "#/definitions/ApexClassProblem" + } } }, - "required": ["name", "namespace", "success", "problems"], + "required": ["name", "namespace", "success", "problems", "warnings"], "additionalProperties": false }, "ApexClassProblem": { diff --git a/src/commands/apex/get/compile-status.ts b/src/commands/apex/get/compile-status.ts index df99cb00..9a4240e4 100644 --- a/src/commands/apex/get/compile-status.ts +++ b/src/commands/apex/get/compile-status.ts @@ -36,6 +36,7 @@ export type InvalidApexClassResult = { namespace: string; success: boolean; problems: ApexClassProblem[]; + warnings: ApexClassProblem[]; }; export type CompilationResult = { @@ -68,7 +69,7 @@ export default class GetCompileStatus extends SfCommand { // eslint-disable-next-line @typescript-eslint/no-misused-promises process.on('SIGTERM', exitHandler); - this.spinner.start('Retrieving invalid Apex classes...'); + this.spinner.start('Retrieving Apex compilation results'); const connection = flags['target-org'].getConnection(flags['api-version']); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion @@ -86,16 +87,19 @@ export default class GetCompileStatus extends SfCommand { // so we format the results for everyone else if (!this.jsonEnabled() && invalidApexResponse.results.length > 0) { if (invalidApexResponse.results.length > 0) { + const tableFormatter = (tableItems: ApexClassProblem[]): string => + tableItems.map((tableItem) => JSON.stringify(tableItem)).join('\n'); this.table({ - columns: ['name', 'namespace', 'success', 'problems'], + columns: ['name', 'namespace', 'success', 'problems', 'warnings'], data: invalidApexResponse.results.map((result) => ({ ...result, - problems: result.problems.map((problem) => JSON.stringify(problem)), + problems: tableFormatter(result.problems), + warnings: tableFormatter(result.warnings), })), - title: 'Invalid Apex Classes:', + title: 'Compile Status Results:', }); } else { - this.log('No invalid Apex classes found!'); + this.log('No Apex compiler issues found!'); } } diff --git a/test/commands/apex/get/compile-status.test.ts b/test/commands/apex/get/compile-status.test.ts index f5179f55..9618ba1a 100644 --- a/test/commands/apex/get/compile-status.test.ts +++ b/test/commands/apex/get/compile-status.test.ts @@ -73,6 +73,7 @@ describe('apex:get:compile-status', () => { message: 'Unexpected token: }', }, ], + warnings: [], }, { name: 'TestClass2', @@ -90,6 +91,7 @@ describe('apex:get:compile-status', () => { message: 'Syntax error', }, ], + warnings: [], }, ], }; @@ -111,19 +113,33 @@ describe('apex:get:compile-status', () => { const tableCall = uxStub.table.firstCall; expect(tableCall.args[0]).to.deep.include({ - columns: ['name', 'namespace', 'success', 'problems'], - title: 'Invalid Apex Classes:', + columns: ['name', 'namespace', 'success', 'problems', 'warnings'], + title: 'Compile Status Results:', }); const tableData = (tableCall.args[0] as Record).data as Array>; expect(tableData).to.have.lengthOf(2); expect(tableData[0].name).to.equal('TestClass1'); - expect((tableData[0].problems as string[])[0]).to.be.a('string'); - expect(JSON.parse((tableData[0].problems as string[])[0])).to.deep.equal({ + expect(tableData[0].problems).to.be.a('string'); + expect(JSON.parse(tableData[0].problems as string)).to.deep.equal({ line: 10, column: 5, message: 'Unexpected token: }', }); + + const secondClassProblems = (tableData[1].problems as string).split('\n').map((problem) => JSON.parse(problem)); + expect(secondClassProblems).to.deep.equal([ + { + line: 25, + column: 1, + message: 'Invalid class declaration', + }, + { + line: 30, + column: 10, + message: 'Syntax error', + }, + ]); }); it('returns compilation result with --json flag without displaying table', async () => { @@ -134,6 +150,7 @@ describe('apex:get:compile-status', () => { name: 'TestClass1', namespace: '', success: false, + warnings: [], problems: [ { line: 10, @@ -220,6 +237,7 @@ describe('apex:get:compile-status', () => { name: 'ComplexClass', namespace: 'testNamespace', success: false, + warnings: [], problems: [ { line: 5, @@ -266,6 +284,7 @@ describe('apex:get:compile-status', () => { name: 'LocalClass', namespace: '', success: false, + warnings: [], problems: [ { line: 1, @@ -307,6 +326,7 @@ describe('apex:get:compile-status', () => { message: 'Test message', }, ], + warnings: [], }, ], }; @@ -326,7 +346,7 @@ describe('apex:get:compile-status', () => { const tableCall = uxStub.table.firstCall; const tableData = (tableCall.args[0] as Record).data as Array>; - const problemString = (tableData[0].problems as string[])[0]; + const problemString = tableData[0].problems as string; expect(problemString).to.be.a('string'); const parsedProblem = JSON.parse(problemString); From af84e5510401081c3d11bdbc408f660b9fb1dcac Mon Sep 17 00:00:00 2001 From: James Simone <16430727+jamessimone@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:25:34 -0400 Subject: [PATCH 4/9] feat: update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 05f125e9..d3193650 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Happy debugging! ## `sf apex get compile-status` -Fetches any Apex classes in your org with compilation status issues. +Fetches any Apex in your org with compilation status issues. ``` USAGE @@ -115,15 +115,15 @@ GLOBAL FLAGS --json Format output as json. DESCRIPTION - Fetches invalid Apex classes, including the problems associated with their current compilation status. + Fetches invalid Apex or Apex with compiler warnings, and reports the current compilation status. EXAMPLES - Fetches invalid classes in your default org: + Fetches invalid Apex or Apex with compilation warnings in your default org: $ sf apex get compile-status - Fetch invalid classes in the org with the specified username: + Fetch invalid Apex or Apex with compilation warnings in the org with the specified username: $ sf apex get compile-status --target-org me@my.org ``` From 547ee21ceff86ba5359e9395d81fedaa5f5e1ed2 Mon Sep 17 00:00:00 2001 From: James Simone <16430727+jamessimone@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:29:57 -0400 Subject: [PATCH 5/9] fix: code review feedback --- README.md | 2 +- messages/get-compile-status.md | 6 +++- src/commands/apex/get/compile-status.ts | 30 ++++++------------ test/commands/apex/get/compile-status.test.ts | 31 +++++++++++++++++-- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d3193650..660cf45c 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ EXAMPLES $ sf apex get compile-status --target-org me@my.org ``` -_See code: [src/commands/apex/get/log.ts](https://github.com/salesforcecli/plugin-apex/blob/4.1.0/src/commands/apex/get/log.ts)_ +_See code: [src/commands/apex/get/compile-status.ts](https://github.com/salesforcecli/plugin-apex/blob/4.3.0/src/commands/apex/get/compile-status.ts)_ ## `sf apex get log` diff --git a/messages/get-compile-status.md b/messages/get-compile-status.md index 58a9afb3..e524cb11 100644 --- a/messages/get-compile-status.md +++ b/messages/get-compile-status.md @@ -14,4 +14,8 @@ Return compile results for only Apex classes and triggers with validation errors # noResultsFound -No results found +No Apex compiler issues found. + +# minimumApiVersionNotMet + +Apex compile results available in API versions 68.0 and above diff --git a/src/commands/apex/get/compile-status.ts b/src/commands/apex/get/compile-status.ts index 9a4240e4..23c7b2f1 100644 --- a/src/commands/apex/get/compile-status.ts +++ b/src/commands/apex/get/compile-status.ts @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { CancellationTokenSource } from '@salesforce/apex-node'; import { orgApiVersionFlagWithDeprecations, requiredOrgFlagWithDeprecations, SfCommand, } from '@salesforce/sf-plugins-core'; -import { Messages } from '@salesforce/core'; +import { Messages, SfError } from '@salesforce/core'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-apex', 'get-compile-status'); @@ -53,25 +51,16 @@ export default class GetCompileStatus extends SfCommand { public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); - protected cancellationTokenSource = new CancellationTokenSource(); - public async run(): Promise { const { flags } = await this.parse(GetCompileStatus); - - // graceful shutdown - const exitHandler = async (): Promise => { - await this.cancellationTokenSource.asyncCancel(); - process.exit(); - }; - - // eslint-disable-next-line @typescript-eslint/no-misused-promises - process.on('SIGINT', exitHandler); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - process.on('SIGTERM', exitHandler); + const apiVersion = flags['api-version']; + if (apiVersion && Number(apiVersion.replace('.0', '')) < 68) { + throw new SfError(messages.getMessage('minimumApiVersionNotMet')); + } this.spinner.start('Retrieving Apex compilation results'); - const connection = flags['target-org'].getConnection(flags['api-version']); + const connection = flags['target-org'].getConnection(apiVersion); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const invalidApexResponse = (await connection.tooling.request({ // eslint-disable-next-line no-underscore-dangle @@ -83,9 +72,11 @@ export default class GetCompileStatus extends SfCommand { body: JSON.stringify({}), })) as CompilationResult; + this.spinner.stop(); + // the actual command output is only displayed when --json is passed as a flag // so we format the results for everyone else - if (!this.jsonEnabled() && invalidApexResponse.results.length > 0) { + if (!this.jsonEnabled()) { if (invalidApexResponse.results.length > 0) { const tableFormatter = (tableItems: ApexClassProblem[]): string => tableItems.map((tableItem) => JSON.stringify(tableItem)).join('\n'); @@ -99,11 +90,10 @@ export default class GetCompileStatus extends SfCommand { title: 'Compile Status Results:', }); } else { - this.log('No Apex compiler issues found!'); + this.log(messages.getMessage('noResultsFound')); } } - this.spinner.stop(); return invalidApexResponse; } } diff --git a/test/commands/apex/get/compile-status.test.ts b/test/commands/apex/get/compile-status.test.ts index 9618ba1a..97083802 100644 --- a/test/commands/apex/get/compile-status.test.ts +++ b/test/commands/apex/get/compile-status.test.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */ import sinon from 'sinon'; import { expect } from 'chai'; import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; -import { Org } from '@salesforce/core'; +import { Org, SfError } from '@salesforce/core'; import GetCompileStatus from '../../../../src/commands/apex/get/compile-status.js'; import type { CompilationResult } from '../../../../src/commands/apex/get/compile-status.js'; @@ -55,7 +55,32 @@ describe('apex:get:compile-status', () => { const result = await GetCompileStatus.run([]); expect(result).to.deep.equal(mockCompilationResult); - expect(uxStub.log.called).to.be.false; + expect(uxStub.log.called).to.be.true; + }); + + it('throws when API version beneath 68', async () => { + const mockCompilationResult: CompilationResult = { + status: 'success', + results: [], + }; + + const org = { + getConnection: sandbox.stub().returns({ + tooling: { + request: sandbox.stub().resolves(mockCompilationResult), + _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), + }, + }), + }; + + sandbox.stub(Org, 'create').resolves(org as unknown as Org); + + try { + await GetCompileStatus.run(['--api-version', '67.0']); + } catch (err) { + expect(err).to.be.instanceOf(SfError, 'Should be SfError'); + expect((err as SfError).message).to.eq('Apex compile results available in API versions 68.0 and above'); + } }); it('displays invalid classes in table format without --json flag', async () => { From 06839729f4bfd927fc016cd2277a256bd88368cc Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 5 Oct 2026 10:09:57 -0600 Subject: [PATCH 6/9] fix: improve table output, clean up stale tests, add NUT - Format problems/warnings as readable strings (L:C - message) instead of raw JSON - Set table overflow to wrap so long messages aren't truncated - Remove stale signal handler test (was testing SfCommand base class behavior, not the command) - Remove unnecessary process.on/process.exit stubs from beforeEach - Add expect.fail() to API version test to catch silent passes - Add NUT for compile-status command --- src/commands/apex/get/compile-status.ts | 3 +- test/commands/apex/get/compile-status.nut.ts | 155 ++++++++++++++++++ test/commands/apex/get/compile-status.test.ts | 55 +------ 3 files changed, 163 insertions(+), 50 deletions(-) create mode 100644 test/commands/apex/get/compile-status.nut.ts diff --git a/src/commands/apex/get/compile-status.ts b/src/commands/apex/get/compile-status.ts index 23c7b2f1..02cdccd5 100644 --- a/src/commands/apex/get/compile-status.ts +++ b/src/commands/apex/get/compile-status.ts @@ -79,7 +79,7 @@ export default class GetCompileStatus extends SfCommand { if (!this.jsonEnabled()) { if (invalidApexResponse.results.length > 0) { const tableFormatter = (tableItems: ApexClassProblem[]): string => - tableItems.map((tableItem) => JSON.stringify(tableItem)).join('\n'); + tableItems.map((p) => `L${p.line}:C${p.column} - ${p.message}`).join('\n'); this.table({ columns: ['name', 'namespace', 'success', 'problems', 'warnings'], data: invalidApexResponse.results.map((result) => ({ @@ -88,6 +88,7 @@ export default class GetCompileStatus extends SfCommand { warnings: tableFormatter(result.warnings), })), title: 'Compile Status Results:', + overflow: 'wrap', }); } else { this.log(messages.getMessage('noResultsFound')); diff --git a/test/commands/apex/get/compile-status.nut.ts b/test/commands/apex/get/compile-status.nut.ts new file mode 100644 index 00000000..84aa1300 --- /dev/null +++ b/test/commands/apex/get/compile-status.nut.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { config, expect } from 'chai'; +import type { CompilationResult } from '../../../../src/commands/apex/get/compile-status.js'; + +config.truncateThreshold = 0; + +const APEX_META_XML = [ + '', + '', + ' 62.0', + ' Active', + '', +].join('\n'); + +const HELPER_CLASS = [ + 'public with sharing class CompileStatusHelper {', + " public static final String SOME_CONSTANT = 'hello';", + ' public static void doSomething(String a, String b) {', + ' System.debug(a + b);', + ' }', + ' public CompileStatusHelper(String arg) {', + ' System.debug(arg);', + ' }', + '}', +].join('\n'); + +const BROKEN_CLASS = [ + 'public with sharing class CompileStatusBroken {', + ' public static String getBadVariable() {', + ' return CompileStatusHelper.SOME_CONSTANT;', + ' }', + ' public static void callBadMethod() {', + " CompileStatusHelper.doSomething('param1', 'param2');", + ' }', + ' public static void callBadConstructor() {', + " CompileStatusHelper obj = new CompileStatusHelper('arg1');", + ' }', + '}', +].join('\n'); + +describe('apex get compile-status NUT', () => { + let session: TestSession; + let classesDir: string; + + before(async () => { + session = await TestSession.create({ + project: { + gitClone: 'https://github.com/trailheadapps/dreamhouse-lwc.git', + }, + devhubAuthStrategy: 'AUTO', + }); + + classesDir = path.join(session.project.dir, 'force-app', 'main', 'default', 'classes'); + + // TODO(2026-10-10): remove --release preview after GA release + execCmd( + 'org:create:scratch -f config/project-scratch-def.json --set-default --alias org --release preview --wait 10 --duration-days 1', + { ensureExitCode: 0 } + ); + + execCmd('project:deploy:start -o org --source-dir force-app', { ensureExitCode: 0 }); + }); + + after(async () => { + try { + execCmd('org:delete:scratch -o org --no-prompt'); + } catch { + // best-effort cleanup + } + await session?.zip(undefined, 'artifacts'); + await session?.clean(); + }); + + describe('clean org', () => { + it('reports no compile issues', () => { + const result = execCmd('apex:get:compile-status', { ensureExitCode: 0 }).shellOutput.stdout; + expect(result).to.include('No Apex compiler issues found'); + }); + + it('returns empty results with --json', () => { + const result = execCmd('apex:get:compile-status --json', { ensureExitCode: 0 }).jsonOutput + ?.result; + expect(result?.results).to.be.an('array').with.lengthOf(0); + }); + }); + + describe('org with broken apex references', () => { + before(() => { + fs.writeFileSync(path.join(classesDir, 'CompileStatusHelper.cls'), HELPER_CLASS); + fs.writeFileSync(path.join(classesDir, 'CompileStatusHelper.cls-meta.xml'), APEX_META_XML); + fs.writeFileSync(path.join(classesDir, 'CompileStatusBroken.cls'), BROKEN_CLASS); + fs.writeFileSync(path.join(classesDir, 'CompileStatusBroken.cls-meta.xml'), APEX_META_XML); + + execCmd('project:deploy:start -o org --source-dir force-app/main/default/classes', { + ensureExitCode: 0, + }); + + execCmd('project:delete:source -o org -m ApexClass:CompileStatusHelper --no-prompt', { + ensureExitCode: 0, + }); + }); + + it('shows the broken class in human-readable output', () => { + const result = execCmd('apex:get:compile-status', { ensureExitCode: 0 }).shellOutput.stdout; + expect(result).to.include('CompileStatusBroken'); + expect(result).to.include('CompileStatusHelper'); + }); + + it('returns problems for the broken class with --json', () => { + const result = execCmd('apex:get:compile-status --json', { ensureExitCode: 0 }).jsonOutput + ?.result; + expect(result).to.have.property('status'); + expect(result?.results).to.be.an('array').with.length.greaterThan(0); + + const brokenResult = result?.results.find((r) => r.name === 'CompileStatusBroken'); + expect(brokenResult).to.not.be.undefined; + expect(brokenResult?.success).to.equal(false); + expect(brokenResult?.problems).to.be.an('array').with.length.greaterThan(0); + + const problemMessages = brokenResult?.problems.map((p) => p.message) ?? []; + expect(problemMessages.some((m) => m.includes('CompileStatusHelper'))).to.be.true; + expect(brokenResult?.warnings).to.be.an('array'); + }); + + it('includes line and column info in problems', () => { + const result = execCmd('apex:get:compile-status --json', { ensureExitCode: 0 }).jsonOutput + ?.result; + const brokenResult = result?.results.find((r) => r.name === 'CompileStatusBroken'); + expect(brokenResult).to.not.be.undefined; + expect(brokenResult?.problems).to.be.an('array').that.is.not.empty; + const problem = brokenResult?.problems[0]; + expect(problem).to.have.property('line').that.is.a('number'); + expect(problem).to.have.property('column').that.is.a('number'); + expect(problem).to.have.property('message').that.is.a('string'); + }); + }); +}); diff --git a/test/commands/apex/get/compile-status.test.ts b/test/commands/apex/get/compile-status.test.ts index 97083802..d806ca4d 100644 --- a/test/commands/apex/get/compile-status.test.ts +++ b/test/commands/apex/get/compile-status.test.ts @@ -28,8 +28,6 @@ describe('apex:get:compile-status', () => { beforeEach(async () => { sandbox = sinon.createSandbox(); uxStub = stubSfCommandUx(sandbox); - sandbox.stub(process, 'on').resolves(); - sandbox.stub(process, 'exit'); }); afterEach(() => { @@ -77,6 +75,7 @@ describe('apex:get:compile-status', () => { try { await GetCompileStatus.run(['--api-version', '67.0']); + expect.fail('Expected SfError to be thrown'); } catch (err) { expect(err).to.be.instanceOf(SfError, 'Should be SfError'); expect((err as SfError).message).to.eq('Apex compile results available in API versions 68.0 and above'); @@ -146,25 +145,10 @@ describe('apex:get:compile-status', () => { expect(tableData).to.have.lengthOf(2); expect(tableData[0].name).to.equal('TestClass1'); expect(tableData[0].problems).to.be.a('string'); - expect(JSON.parse(tableData[0].problems as string)).to.deep.equal({ - line: 10, - column: 5, - message: 'Unexpected token: }', - }); + expect(tableData[0].problems).to.equal('L10:C5 - Unexpected token: }'); - const secondClassProblems = (tableData[1].problems as string).split('\n').map((problem) => JSON.parse(problem)); - expect(secondClassProblems).to.deep.equal([ - { - line: 25, - column: 1, - message: 'Invalid class declaration', - }, - { - line: 30, - column: 10, - message: 'Syntax error', - }, - ]); + const secondClassProblems = (tableData[1].problems as string).split('\n'); + expect(secondClassProblems).to.deep.equal(['L25:C1 - Invalid class declaration', 'L30:C10 - Syntax error']); }); it('returns compilation result with --json flag without displaying table', async () => { @@ -231,29 +215,6 @@ describe('apex:get:compile-status', () => { expect(callArgs.body).to.equal('{}'); }); - it('registers signal handlers for graceful shutdown', async () => { - const mockCompilationResult: CompilationResult = { - status: 'success', - results: [], - }; - - const org = { - getConnection: sandbox.stub().returns({ - tooling: { - request: sandbox.stub().resolves(mockCompilationResult), - _baseUrl: sandbox.stub().returns('/services/data/v68.0/tooling'), - }, - }), - }; - - sandbox.stub(Org, 'create').resolves(org as unknown as Org); - - await GetCompileStatus.run([]); - - expect((process.on as sinon.SinonStub).calledWith('SIGINT')).to.be.true; - expect((process.on as sinon.SinonStub).calledWith('SIGTERM')).to.be.true; - }); - it('handles multiple problems per class', async () => { const mockCompilationResult: CompilationResult = { status: 'success', @@ -336,7 +297,7 @@ describe('apex:get:compile-status', () => { expect(result.results[0].namespace).to.equal(''); }); - it('converts problem objects to JSON strings in table display', async () => { + it('formats problem objects as readable strings in table display', async () => { const mockCompilationResult: CompilationResult = { status: 'success', results: [ @@ -373,10 +334,6 @@ describe('apex:get:compile-status', () => { const tableData = (tableCall.args[0] as Record).data as Array>; const problemString = tableData[0].problems as string; - expect(problemString).to.be.a('string'); - const parsedProblem = JSON.parse(problemString); - expect(parsedProblem.line).to.equal(10); - expect(parsedProblem.column).to.equal(5); - expect(parsedProblem.message).to.equal('Test message'); + expect(problemString).to.equal('L10:C5 - Test message'); }); }); From 255bca6a2d4cf1f64738f6994871f089204fcae6 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 5 Oct 2026 10:11:55 -0600 Subject: [PATCH 7/9] chore: remove unused eslint-disable for no-unsafe-return --- test/commands/apex/get/compile-status.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/apex/get/compile-status.test.ts b/test/commands/apex/get/compile-status.test.ts index d806ca4d..52093807 100644 --- a/test/commands/apex/get/compile-status.test.ts +++ b/test/commands/apex/get/compile-status.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import sinon from 'sinon'; import { expect } from 'chai'; import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; From 595850aac597ed432fdd4dc5cea058295c8aa3c9 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 5 Oct 2026 10:49:11 -0600 Subject: [PATCH 8/9] fix: restore cli: 'sf' for non-apex commands in NUT org:create:scratch, project:deploy:start, and org:delete:scratch need cli: 'sf' since they aren't part of plugin-apex and can't run via bin/run. --- test/commands/apex/get/compile-status.nut.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/commands/apex/get/compile-status.nut.ts b/test/commands/apex/get/compile-status.nut.ts index 84aa1300..51875b8e 100644 --- a/test/commands/apex/get/compile-status.nut.ts +++ b/test/commands/apex/get/compile-status.nut.ts @@ -73,15 +73,15 @@ describe('apex get compile-status NUT', () => { // TODO(2026-10-10): remove --release preview after GA release execCmd( 'org:create:scratch -f config/project-scratch-def.json --set-default --alias org --release preview --wait 10 --duration-days 1', - { ensureExitCode: 0 } + { ensureExitCode: 0, cli: 'sf' } ); - execCmd('project:deploy:start -o org --source-dir force-app', { ensureExitCode: 0 }); + execCmd('project:deploy:start -o org --source-dir force-app', { ensureExitCode: 0, cli: 'sf' }); }); after(async () => { try { - execCmd('org:delete:scratch -o org --no-prompt'); + execCmd('org:delete:scratch -o org --no-prompt', { cli: 'sf' }); } catch { // best-effort cleanup } From 4c42e84f1e74445968ffb05a3a01befc24b98f8d Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 5 Oct 2026 10:56:31 -0600 Subject: [PATCH 9/9] fix: add cli: 'sf' to remaining non-apex NUT commands --- test/commands/apex/get/compile-status.nut.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/commands/apex/get/compile-status.nut.ts b/test/commands/apex/get/compile-status.nut.ts index 51875b8e..a21bdb18 100644 --- a/test/commands/apex/get/compile-status.nut.ts +++ b/test/commands/apex/get/compile-status.nut.ts @@ -111,10 +111,12 @@ describe('apex get compile-status NUT', () => { execCmd('project:deploy:start -o org --source-dir force-app/main/default/classes', { ensureExitCode: 0, + cli: 'sf', }); execCmd('project:delete:source -o org -m ApexClass:CompileStatusHelper --no-prompt', { ensureExitCode: 0, + cli: 'sf', }); });