Skip to content

Wr/testing updates ii @W-18891538@ #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ You can also use special values to control access to orgs:
// ************************
if (all || enabledToolsets.has('testing')) {
this.logToStderr('Registering testing tools');
testing.registerToolRunApexTest(server);
testing.registerToolRunAgentTest(server);
testing.registerToolTestApex(server);
testing.registerToolTestAgent(server);
}

// ************************
Expand Down
23 changes: 17 additions & 6 deletions src/tools/testing/sf-test-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ import { SfMcpServer } from '../../sf-mcp-server.js';
const runAgentTestsParam = z.object({
agentApiName: z.string().describe(
`Agent test to run
if unsure, list all files matching the pattern *.aiEvaluationDefinition-meta.xml
if unsure, list all files matching the pattern **/aiEvaluationDefinitions/*.aiEvaluationDefinition-meta.xml
only one test can be executed at a time
`
),
usernameOrAlias: usernameOrAliasParam,
directory: directoryParam,
async: z
.boolean()
.default(false)
.describe('Whether to wait for the tests to finish (false) or quickly return only the test id (true)'),
});

export type AgentRunTests = z.infer<typeof runAgentTestsParam>;
Expand All @@ -46,7 +50,7 @@ export type AgentRunTests = z.infer<typeof runAgentTestsParam>;
* Returns:
* - textResponse: Test result.
*/
export const registerToolRunAgentTest = (server: SfMcpServer): void => {
export const registerToolTestAgent = (server: SfMcpServer): void => {
server.tool(
'sf-test-agents',
`Run Agent tests in an org.
Expand All @@ -60,13 +64,14 @@ this should be chosen when a file in the 'aiEvaluationDefinitions' directory is
EXAMPLE USAGE:
Run tests for the X agent
Run this test
start myAgentTest and don't wait for results
`,
runAgentTestsParam.shape,
{
title: 'Run Agent Tests',
openWorldHint: false,
},
async ({ usernameOrAlias, agentApiName, directory }) => {
async ({ usernameOrAlias, agentApiName, directory, async }) => {
if (!usernameOrAlias)
return textResponse(
'The usernameOrAlias parameter is required, if the user did not specify one use the #sf-get-username tool',
Expand All @@ -79,9 +84,15 @@ Run this test

try {
const agentTester = new AgentTester(connection);
const test = await agentTester.start(agentApiName);
const result = await agentTester.poll(test.runId, { timeout: Duration.minutes(10) });
return textResponse(`Test result: ${JSON.stringify(result)}`);

if (async) {
const startResult = await agentTester.start(agentApiName);
return textResponse(`Test Run: ${JSON.stringify(startResult)}`);
} else {
const test = await agentTester.start(agentApiName);
const result = await agentTester.poll(test.runId, { timeout: Duration.minutes(10) });
return textResponse(`Test result: ${JSON.stringify(result)}`);
}
} catch (e) {
return textResponse(`Failed to run Agent Tests: ${e instanceof Error ? e.message : 'Unknown error'}`, true);
}
Expand Down
99 changes: 88 additions & 11 deletions src/tools/testing/sf-test-apex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/

import { z } from 'zod';
import { TestLevel, TestResult, TestService } from '@salesforce/apex-node';
import { TestLevel, TestResult, TestRunIdResult, TestService } from '@salesforce/apex-node';
import { ApexTestResultOutcome } from '@salesforce/apex-node/lib/src/tests/types.js';
import { Duration, ensureArray } from '@salesforce/kit';
import { directoryParam, usernameOrAliasParam } from '../../shared/params.js';
import { textResponse } from '../../shared/utils.js';
import { getConnection } from '../../shared/auth.js';
Expand All @@ -33,12 +35,37 @@ RunAllTestsInOrg="Run all tests in the org, including tests of managed packages"
RunSpecifiedTests="Run the Apex tests I specify, these will be specified in the classNames parameter"
`
),
classNames: z.array(z.string()).describe(
`Apex tests classes to run.
classNames: z
.array(z.string())
.describe(
`Apex tests classes to run.
if Running all tests, all tests should be listed
if unsure, find apex classes matching the pattern **/classes/*.cls, that include the @isTest decorator in the file and then join their test names together with ','
Run the tests, find apex classes matching the pattern **/classes/*.cls, that include the @isTest decorator in the file and then join their test names together with ','
`
),
)
.optional(),
methodNames: z
.array(z.string())
.describe(
'Specific test method names, functions inside of an apex test class, must be joined with the Apex tests name'
)
.optional(),
async: z
.boolean()
.default(false)
.describe(
'Weather to wait for the test to finish (false) or enque the Apex tests and return the test run id (true)'
),
suiteName: z.string().describe('a suite of apex test classes to run').optional(),
testRunId: z.string().default('an id of an in-progress, or completed apex test run').optional(),
verbose: z
.boolean()
.default(false)
.describe('If a user wants more test information in the context, or information about passing tests'),
codeCoverage: z
.boolean()
.default(false)
.describe('set to true if a user wants codecoverage calculated by the server'),
usernameOrAlias: usernameOrAliasParam,
directory: directoryParam,
});
Expand All @@ -57,7 +84,7 @@ export type ApexRunTests = z.infer<typeof runApexTestsParam>;
* Returns:
* - textResponse: Test result.
*/
export const registerToolRunApexTest = (server: SfMcpServer): void => {
export const registerToolTestApex = (server: SfMcpServer): void => {
server.tool(
'sf-test-apex',
`Run Apex tests in an org.
Expand All @@ -70,16 +97,36 @@ this should be chosen when a file in the 'classes' directory is mentioned

EXAMPLE USAGE:
Run tests A, B, C.
Run the tests, find apex classes matching the pattern **/classes/*.cls, that include the @isTest decorator in the file and then join their test names together with ','
Run the myTestMethod in this file
Run this test and include success and failures
Run all tests in the org.
Test the "mySuite" suite asynchronously. I’ll check results later.
Run tests for this file and include coverage
What are the results for 707XXXXXXXXXXXX
`,
runApexTestsParam.shape,
{
title: 'Apex Tests',
openWorldHint: false,
},
async ({ testLevel, usernameOrAlias, classNames, directory }) => {
if (testLevel !== TestLevel.RunSpecifiedTests && classNames?.length && classNames?.length >= 1) {
async ({
testLevel,
usernameOrAlias,
classNames,
directory,
methodNames,
suiteName,
async,
testRunId,
verbose,
codeCoverage,
}) => {
if (
(ensureArray(suiteName).length > 1 ||
ensureArray(methodNames).length > 1 ||
ensureArray(classNames).length > 1) &&
testLevel !== TestLevel.RunSpecifiedTests
) {
return textResponse("You can't specify which tests to run without setting testLevel='RunSpecifiedTests'", true);
}

Expand All @@ -95,9 +142,39 @@ Run all tests in the org.
const connection = await getConnection(usernameOrAlias);
try {
const testService = new TestService(connection);
let result: TestResult | TestRunIdResult;

if (testRunId) {
// we just need to get the test results
result = await testService.reportAsyncResults(testRunId, codeCoverage);
} else {
// we need to run tests
const payload = await testService.buildAsyncPayload(
testLevel,
methodNames?.join(','),
classNames?.join(','),
suiteName
);
result = await testService.runTestAsynchronous(
payload,
codeCoverage,
async,
undefined,
undefined,
Duration.minutes(10)
);
if (async) {
return textResponse(`Test Run Id: ${JSON.stringify(result)}`);
}
// the user waited for the full results, we know they're TestResult
result = result as TestResult;
}

if (!verbose) {
// aka concise, filter out passing tests
result.tests = result.tests.filter((test) => test.outcome === ApexTestResultOutcome.Fail);
}

const payload = await testService.buildAsyncPayload(testLevel, classNames.join(','));
const result = (await testService.runTestAsynchronous(payload, false)) as TestResult;
return textResponse(`Test result: ${JSON.stringify(result)}`);
} catch (e) {
return textResponse(`Failed to run Apex Tests: ${e instanceof Error ? e.message : 'Unknown error'}`, true);
Expand Down
Loading