diff --git a/command-snapshot.json b/command-snapshot.json index 050e1343..b815bd34 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -182,7 +182,17 @@ "command": "agent:test:results", "flagAliases": [], "flagChars": ["d", "i", "o"], - "flags": ["api-version", "flags-dir", "job-id", "json", "output-dir", "result-format", "target-org", "verbose"], + "flags": [ + "api-version", + "flags-dir", + "job-id", + "json", + "output-dir", + "result-format", + "target-org", + "test-runner", + "verbose" + ], "plugin": "@salesforce/plugin-agent" }, { @@ -198,6 +208,7 @@ "output-dir", "result-format", "target-org", + "test-runner", "use-most-recent", "verbose", "wait" @@ -217,6 +228,7 @@ "output-dir", "result-format", "target-org", + "test-runner", "verbose", "wait" ], diff --git a/messages/shared.md b/messages/shared.md index e007fadc..173e30cf 100644 --- a/messages/shared.md +++ b/messages/shared.md @@ -20,6 +20,14 @@ When enabled, includes detailed generated data (such as invoked actions) in the The generated data is in JSON format and includes the Apex classes or Flows that were invoked, the Salesforce objects that were touched, and so on. Use the JSON structure of this information to build the test case JSONPath expression when using custom evaluations. +# flags.test-runner.summary + +Explicitly specify which test runner to use (agentforce-studio or testing-center). + +# flags.test-runner.description + +By default, the command automatically detects which test runner to use based on the test definition metadata type in your org. Use this flag to explicitly specify the runner type. 'agentforce-studio' uses AiTestingDefinition metadata. 'testing-center' uses AiEvaluationDefinition metadata. + # error.invalidAgentType agentType must be either "customer" or "internal". Found: [%s] diff --git a/schemas/agent-test-results.json b/schemas/agent-test-results.json index 50332090..a176db01 100644 --- a/schemas/agent-test-results.json +++ b/schemas/agent-test-results.json @@ -3,7 +3,14 @@ "$ref": "#/definitions/AgentTestResultsResult", "definitions": { "AgentTestResultsResult": { - "$ref": "#/definitions/AgentTestResultsResponse" + "anyOf": [ + { + "$ref": "#/definitions/AgentTestResultsResponse" + }, + { + "$ref": "#/definitions/AgentTestNGTResultsResponse" + } + ] }, "AgentTestResultsResponse": { "type": "object", @@ -148,6 +155,54 @@ }, "required": ["status", "startTime", "inputs", "generatedData", "testResults", "testNumber"], "additionalProperties": false + }, + "AgentTestNGTResultsResponse": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "testCases": { + "type": "array", + "items": { + "$ref": "#/definitions/NGTTestCaseResult" + } + } + }, + "required": ["status", "testCases"], + "additionalProperties": false + }, + "NGTTestCaseResult": { + "type": "object", + "properties": { + "subjectResponse": { + "type": "string" + }, + "testNumber": { + "type": "number" + }, + "testScorerResults": { + "type": "array", + "items": { + "$ref": "#/definitions/TestScorerResult" + } + } + }, + "required": ["subjectResponse", "testNumber", "testScorerResults"], + "additionalProperties": false + }, + "TestScorerResult": { + "type": "object", + "properties": { + "scorerName": { + "type": "string" + }, + "scorerResponse": { + "type": "string" + } + }, + "required": ["scorerName", "scorerResponse"], + "additionalProperties": false } } } diff --git a/schemas/agent-test-resume.json b/schemas/agent-test-resume.json index 3b73ce7d..5f195569 100644 --- a/schemas/agent-test-resume.json +++ b/schemas/agent-test-resume.json @@ -65,6 +65,26 @@ } }, "required": ["runId", "startTime", "status", "subjectName", "testCases"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "type": "string", + "const": "COMPLETED" + }, + "runId": { + "type": "string" + }, + "testCases": { + "type": "array", + "items": { + "$ref": "#/definitions/NGTTestCaseResult" + } + } + }, + "required": ["runId", "status", "testCases"] } ] }, @@ -183,6 +203,38 @@ }, "required": ["status", "startTime", "inputs", "generatedData", "testResults", "testNumber"], "additionalProperties": false + }, + "NGTTestCaseResult": { + "type": "object", + "properties": { + "subjectResponse": { + "type": "string" + }, + "testNumber": { + "type": "number" + }, + "testScorerResults": { + "type": "array", + "items": { + "$ref": "#/definitions/TestScorerResult" + } + } + }, + "required": ["subjectResponse", "testNumber", "testScorerResults"], + "additionalProperties": false + }, + "TestScorerResult": { + "type": "object", + "properties": { + "scorerName": { + "type": "string" + }, + "scorerResponse": { + "type": "string" + } + }, + "required": ["scorerName", "scorerResponse"], + "additionalProperties": false } } } diff --git a/schemas/agent-test-run.json b/schemas/agent-test-run.json index 3b73ce7d..5f195569 100644 --- a/schemas/agent-test-run.json +++ b/schemas/agent-test-run.json @@ -65,6 +65,26 @@ } }, "required": ["runId", "startTime", "status", "subjectName", "testCases"] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "type": "string", + "const": "COMPLETED" + }, + "runId": { + "type": "string" + }, + "testCases": { + "type": "array", + "items": { + "$ref": "#/definitions/NGTTestCaseResult" + } + } + }, + "required": ["runId", "status", "testCases"] } ] }, @@ -183,6 +203,38 @@ }, "required": ["status", "startTime", "inputs", "generatedData", "testResults", "testNumber"], "additionalProperties": false + }, + "NGTTestCaseResult": { + "type": "object", + "properties": { + "subjectResponse": { + "type": "string" + }, + "testNumber": { + "type": "number" + }, + "testScorerResults": { + "type": "array", + "items": { + "$ref": "#/definitions/TestScorerResult" + } + } + }, + "required": ["subjectResponse", "testNumber", "testScorerResults"], + "additionalProperties": false + }, + "TestScorerResult": { + "type": "object", + "properties": { + "scorerName": { + "type": "string" + }, + "scorerResponse": { + "type": "string" + } + }, + "required": ["scorerName", "scorerResponse"], + "additionalProperties": false } } } diff --git a/src/agentTestCache.ts b/src/agentTestCache.ts index bdb17c1d..62d7ca28 100644 --- a/src/agentTestCache.ts +++ b/src/agentTestCache.ts @@ -16,6 +16,7 @@ import { Global, SfError, TTLConfig } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; +import type { TestRunnerType } from '@salesforce/agents'; type ResultFormat = 'json' | 'human' | 'junit' | 'tap'; @@ -24,6 +25,7 @@ type CacheContents = { name: string; outputDir?: string; resultFormat?: ResultFormat; + runnerType?: TestRunnerType; }; export class AgentTestCache extends TTLConfig { @@ -45,11 +47,12 @@ export class AgentTestCache extends TTLConfig runId: string, name: string, outputDir?: string, - resultFormat?: ResultFormat + resultFormat?: ResultFormat, + runnerType?: TestRunnerType ): Promise { if (!runId) throw new SfError('runId is required to create a cache entry'); - this.set(runId, { runId, name, outputDir, resultFormat }); + this.set(runId, { runId, name, outputDir, resultFormat, runnerType }); await this.write(); } @@ -70,7 +73,7 @@ export class AgentTestCache extends TTLConfig public useIdOrMostRecent( runId: string | undefined, useMostRecent: boolean - ): { runId: string; name?: string; outputDir?: string; resultFormat?: ResultFormat } { + ): { runId: string; name?: string; outputDir?: string; resultFormat?: ResultFormat; runnerType?: TestRunnerType } { if (runId && useMostRecent) { throw new SfError('Cannot specify both a runId and use most recent flag'); } diff --git a/src/commands/agent/test/results.ts b/src/commands/agent/test/results.ts index 69cfe77e..a3392ea5 100644 --- a/src/commands/agent/test/results.ts +++ b/src/commands/agent/test/results.ts @@ -16,14 +16,15 @@ import { SfCommand, Flags, toHelpSection } from '@salesforce/sf-plugins-core'; import { EnvironmentVariable, Messages, SfError } from '@salesforce/core'; -import { AgentTester, AgentTestResultsResponse } from '@salesforce/agents'; -import { resultFormatFlag, testOutputDirFlag, verboseFlag } from '../../../flags.js'; +import { AgentTestResultsResponse, AgentTestNGTResultsResponse } from '@salesforce/agents'; +import { resultFormatFlag, testOutputDirFlag, testRunnerFlag, verboseFlag } from '../../../flags.js'; import { handleTestResults } from '../../../handleTestResults.js'; +import { createTestRunner } from '../../../testRunnerFactory.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.test.results'); -export type AgentTestResultsResult = AgentTestResultsResponse; +export type AgentTestResultsResult = AgentTestResultsResponse | AgentTestNGTResultsResponse; export default class AgentTestResults extends SfCommand { public static readonly summary = messages.getMessage('summary'); @@ -51,13 +52,20 @@ export default class AgentTestResults extends SfCommand }), 'result-format': resultFormatFlag(), 'output-dir': testOutputDirFlag(), + 'test-runner': testRunnerFlag, verbose: verboseFlag, }; public async run(): Promise { const { flags } = await this.parse(AgentTestResults); - const agentTester = new AgentTester(flags['target-org'].getConnection(flags['api-version'])); + const connection = flags['target-org'].getConnection(flags['api-version']); + const { runner: agentTester } = await createTestRunner( + connection, + flags['test-runner'], + undefined, + flags['job-id'] + ); let response; try { diff --git a/src/commands/agent/test/resume.ts b/src/commands/agent/test/resume.ts index bca01e77..256241b1 100644 --- a/src/commands/agent/test/resume.ts +++ b/src/commands/agent/test/resume.ts @@ -16,12 +16,18 @@ import { SfCommand, Flags, toHelpSection } from '@salesforce/sf-plugins-core'; import { EnvironmentVariable, Messages, SfError } from '@salesforce/core'; -import { AgentTester } from '@salesforce/agents'; import { CLIError } from '@oclif/core/errors'; import { AgentTestCache } from '../../../agentTestCache.js'; import { TestStages } from '../../../testStages.js'; -import { AgentTestRunResult, resultFormatFlag, testOutputDirFlag, verboseFlag } from '../../../flags.js'; +import { + AgentTestRunResult, + resultFormatFlag, + testOutputDirFlag, + testRunnerFlag, + verboseFlag, +} from '../../../flags.js'; import { handleTestResults } from '../../../handleTestResults.js'; +import { createTestRunner } from '../../../testRunnerFactory.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.test.resume'); @@ -65,6 +71,7 @@ export default class AgentTestResume extends SfCommand { }), 'result-format': resultFormatFlag(), 'output-dir': testOutputDirFlag(), + 'test-runner': testRunnerFlag, verbose: verboseFlag, }; @@ -78,6 +85,7 @@ export default class AgentTestResume extends SfCommand { let runId; let outputDir; let resultFormat; + let cachedRunnerType; try { const cacheEntry = agentTestCache.useIdOrMostRecent(flags['job-id'], flags['use-most-recent']); @@ -85,6 +93,7 @@ export default class AgentTestResume extends SfCommand { runId = cacheEntry.runId; outputDir = cacheEntry.outputDir; resultFormat = cacheEntry.resultFormat; + cachedRunnerType = cacheEntry.runnerType; } catch (e) { const wrapped = SfError.wrap(e); @@ -105,7 +114,15 @@ export default class AgentTestResume extends SfCommand { jsonEnabled: this.jsonEnabled(), }); this.mso.start({ id: runId }); - const agentTester = new AgentTester(flags['target-org'].getConnection(flags['api-version'])); + + // Use explicit flag > cached runner type > ID prefix detection > org metadata query + const connection = flags['target-org'].getConnection(flags['api-version']); + const { runner: agentTester } = await createTestRunner( + connection, + flags['test-runner'] ?? cachedRunnerType, + name, + runId + ); let completed; let response; @@ -139,12 +156,17 @@ export default class AgentTestResume extends SfCommand { // Set exit code to 1 only for execution errors (tests couldn't run properly) // Test assertion failures are business logic and should not affect exit code - if (response?.testCases.some((tc) => tc.status === 'ERROR')) { + // Only applicable to legacy responses (NGT doesn't have test case status) + if ( + response && + 'subjectName' in response && + response.testCases.some((tc) => 'status' in tc && tc.status === 'ERROR') + ) { process.exitCode = 1; } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return { ...response!, runId, status: 'COMPLETED' }; + return { ...response!, runId, status: 'COMPLETED' } as AgentTestRunResult; } protected catch(error: Error | SfError | CLIError): Promise { diff --git a/src/commands/agent/test/run.ts b/src/commands/agent/test/run.ts index 330223c6..4dbbb968 100644 --- a/src/commands/agent/test/run.ts +++ b/src/commands/agent/test/run.ts @@ -16,21 +16,23 @@ import { SfCommand, Flags, toHelpSection } from '@salesforce/sf-plugins-core'; import { EnvironmentVariable, Messages, SfError } from '@salesforce/core'; -import { AgentTester, AgentTestStartResponse } from '@salesforce/agents'; +import { AgentTestStartResponse, AgentTestNGTStartResponse } from '@salesforce/agents'; import { colorize } from '@oclif/core/ux'; import { CLIError } from '@oclif/core/errors'; import { AgentTestRunResult, FlaggablePrompt, makeFlags, - promptForAiEvaluationDefinitionApiName, + promptForTestDefinitionApiName, resultFormatFlag, testOutputDirFlag, + testRunnerFlag, verboseFlag, } from '../../../flags.js'; import { AgentTestCache } from '../../../agentTestCache.js'; import { TestStages } from '../../../testStages.js'; import { handleTestResults } from '../../../handleTestResults.js'; +import { createTestRunner } from '../../../testRunnerFactory.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.test.run'); @@ -88,6 +90,7 @@ export default class AgentTestRun extends SfCommand { }), 'result-format': resultFormatFlag(), 'output-dir': testOutputDirFlag(), + 'test-runner': testRunnerFlag, verbose: verboseFlag, }; @@ -102,13 +105,14 @@ export default class AgentTestRun extends SfCommand { } const apiName = - flags['api-name'] ?? (await promptForAiEvaluationDefinitionApiName(FLAGGABLE_PROMPTS['api-name'], connection)); + flags['api-name'] ?? (await promptForTestDefinitionApiName(FLAGGABLE_PROMPTS['api-name'], connection)); this.mso = new TestStages({ title: `Agent Test Run: ${apiName}`, jsonEnabled: this.jsonEnabled() }); this.mso.start(); - const agentTester = new AgentTester(connection); - let response: AgentTestStartResponse; + const { runner: agentTester, type: runnerType } = await createTestRunner(connection, flags['test-runner'], apiName); + + let response: AgentTestStartResponse | AgentTestNGTStartResponse; try { response = await agentTester.start(apiName); } catch (e) { @@ -135,7 +139,13 @@ export default class AgentTestRun extends SfCommand { this.mso.update({ id: response.runId }); const agentTestCache = await AgentTestCache.create(); - await agentTestCache.createCacheEntry(response.runId, apiName, flags['output-dir'], flags['result-format']); + await agentTestCache.createCacheEntry( + response.runId, + apiName, + flags['output-dir'], + flags['result-format'], + runnerType + ); if (flags.wait?.minutes) { let completed; @@ -170,12 +180,16 @@ export default class AgentTestRun extends SfCommand { // Set exit code to 1 only for execution errors (tests couldn't run properly) // Test assertion failures are business logic and should not affect exit code - if (detailsResponse?.testCases.some((tc) => tc.status === 'ERROR')) { + // Only applicable to legacy responses (NGT doesn't have test case status) + if ( + detailsResponse && + 'subjectName' in detailsResponse && + detailsResponse.testCases.some((tc) => 'status' in tc && tc.status === 'ERROR') + ) { process.exitCode = 1; } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return { ...detailsResponse!, status: 'COMPLETED', runId: response.runId }; + return { ...detailsResponse, status: 'COMPLETED', runId: response.runId } as AgentTestRunResult; } else { this.mso.stop(); this.log( diff --git a/src/flags.ts b/src/flags.ts index bc627118..b6902276 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -68,6 +68,12 @@ export const verboseFlag = Flags.boolean({ description: messages.getMessage('flags.verbose.description'), }); +export const testRunnerFlag = Flags.custom<'agentforce-studio' | 'testing-center'>({ + options: ['agentforce-studio', 'testing-center'], + summary: messages.getMessage('flags.test-runner.summary'), + description: messages.getMessage('flags.test-runner.description'), +})(); + function validateInput(input: string, validate: (input: string) => boolean | string): never | string { const result = validate(input); if (typeof result === 'string') throw new Error(result); @@ -128,7 +134,7 @@ export function traverseForFiles(dirOrDirs: string | string[], suffixes: string[ return results; } -export const promptForAiEvaluationDefinitionApiName = async ( +export const promptForTestDefinitionApiName = async ( flagDef: FlaggablePrompt, connection: Connection ): Promise => { diff --git a/src/handleTestResults.ts b/src/handleTestResults.ts index e859037c..bb9e66fb 100644 --- a/src/handleTestResults.ts +++ b/src/handleTestResults.ts @@ -16,11 +16,24 @@ import { join } from 'node:path'; import { stripVTControlCharacters } from 'node:util'; import { writeFile, mkdir } from 'node:fs/promises'; -import { AgentTestResultsResponse, convertTestResultsToFormat, humanFriendlyName, metric } from '@salesforce/agents'; +import { + AgentTestResultsResponse, + AgentTestNGTResultsResponse, + convertTestResultsToFormat, + humanFriendlyName, + metric, +} from '@salesforce/agents'; +import { XMLBuilder } from 'fast-xml-parser'; import { Ux } from '@salesforce/sf-plugins-core/Ux'; import { ux as ocux } from '@oclif/core'; import ansis from 'ansis'; +type TestResultsResponse = AgentTestResultsResponse | AgentTestNGTResultsResponse; + +function isLegacyResponse(response: TestResultsResponse): response is AgentTestResultsResponse { + return 'subjectName' in response; +} + async function writeFileToDir(outputDir: string, fileName: string, content: string): Promise { // if directory doesn't exist, create it await mkdir(outputDir, { recursive: true }); @@ -55,11 +68,6 @@ export function readableTime(time: number, decimalPlaces = 2): string { return '< 1s'; } - // if time < 1000ms, return time in ms - if (time < 1000) { - return `${time}ms`; - } - // if time < 60s, return time in seconds if (time < 60_000) { return `${truncate(time / 1000, decimalPlaces)}s`; @@ -78,6 +86,148 @@ export function readableTime(time: number, decimalPlaces = 2): string { return `${hours}h ${minutes}m`; } +type ParsedScorerResponse = { + status?: string; + score?: number; + reasoning?: string; + actualValue?: string; + expectedValue?: string; +}; + +function parseScorerResponse(raw: string): ParsedScorerResponse { + try { + return JSON.parse(raw) as ParsedScorerResponse; + } catch { + return {}; + } +} + +function humanFormatNGT(results: AgentTestNGTResultsResponse): string { + const ux = new Ux(); + const tables: string[] = []; + + for (const testCase of results.testCases) { + let userInput = ''; + try { + const parsed = JSON.parse(testCase.subjectResponse) as { userInput?: string }; + userInput = parsed.userInput ?? ''; + } catch { + // ignore + } + + const scorerRows = testCase.testScorerResults.map((scorer) => { + const parsed = parseScorerResponse(scorer.scorerResponse); + return { + scorer: scorer.scorerName, + result: parsed.status === 'PASS' ? ansis.green('Pass') : ansis.red('Fail'), + expected: parsed.expectedValue ?? '', + actual: parsed.actualValue ?? '', + reasoning: parsed.reasoning ?? '', + }; + }); + + tables.push( + ux.makeTable({ + title: `${ansis.bold(`Test Case #${testCase.testNumber}`)}\n${ansis.dim('User Input')}: ${userInput}`, + overflow: 'wrap', + columns: [ + { key: 'scorer', name: 'Scorer' }, + { key: 'result', name: 'Result' }, + { key: 'expected', name: 'Expected', width: '25%' }, + { key: 'actual', name: 'Actual', width: '25%' }, + { key: 'reasoning', name: 'Reasoning', width: '35%' }, + ], + data: scorerRows, + width: '100%', + }) + ); + tables.push('\n'); + } + + const totalCases = results.testCases.length; + const passCases = results.testCases.filter((tc) => + tc.testScorerResults.every((s) => parseScorerResponse(s.scorerResponse).status === 'PASS') + ).length; + + const summary = makeSimpleTable( + { + Status: results.status, + 'Total Test Cases': String(totalCases), + 'Passing Test Cases': String(passCases), + 'Failing Test Cases': String(totalCases - passCases), + }, + ansis.bold.blue('Test Results') + ); + + return tables.join('') + `\n${summary}\n`; +} + +function junitFormatNGT(results: AgentTestNGTResultsResponse): string { + const builder = new XMLBuilder({ format: true, attributeNamePrefix: '$', ignoreAttributes: false }); + const testCount = results.testCases.length; + const failureCount = results.testCases.filter((tc) => + tc.testScorerResults.some((s) => parseScorerResponse(s.scorerResponse).status !== 'PASS') + ).length; + + const suites = builder.build({ + testsuites: { + $name: 'AgentTestNGT', + $tests: testCount, + $failures: failureCount, + property: [{ $name: 'status', $value: results.status }], + testsuite: results.testCases.map((tc) => ({ + $name: tc.testNumber, + $assertions: tc.testScorerResults.length, + failure: tc.testScorerResults + .map((s) => { + const parsed = parseScorerResponse(s.scorerResponse); + if (parsed.status !== 'PASS') { + return { $message: parsed.reasoning ?? 'Unknown error', $name: s.scorerName }; + } + }) + .filter(Boolean), + })), + }, + }); + + return `\n${suites}`.trim(); +} + +function tapFormatNGT(results: AgentTestNGTResultsResponse): string { + const lines: string[] = []; + let expectationCount = 0; + + for (const tc of results.testCases) { + for (const scorer of tc.testScorerResults) { + const parsed = parseScorerResponse(scorer.scorerResponse); + const pass = parsed.status === 'PASS'; + expectationCount++; + lines.push(`${pass ? 'ok' : 'not ok'} ${expectationCount} ${tc.testNumber}.${scorer.scorerName}`); + if (!pass) { + lines.push(' ---'); + lines.push(` message: ${parsed.reasoning ?? 'Unknown error'}`); + lines.push(` scorer: ${scorer.scorerName}`); + lines.push(` actual: ${parsed.actualValue ?? ''}`); + lines.push(` expected: ${parsed.expectedValue ?? ''}`); + lines.push(' ...'); + } + } + } + + return `TAP version 13\n1..${expectationCount}\n${lines.join('\n')}`; +} + +function convertNGTTestResultsToFormat(results: AgentTestNGTResultsResponse, format: 'json' | 'junit' | 'tap'): string { + switch (format) { + case 'json': + return JSON.stringify(results, null, 2); + case 'junit': + return junitFormatNGT(results); + case 'tap': + return tapFormatNGT(results); + } +} + export function humanFormat(results: AgentTestResultsResponse, verbose = false): string { const ux = new Ux(); @@ -227,7 +377,7 @@ export async function handleTestResults({ }: { id: string; format: 'human' | 'json' | 'junit' | 'tap'; - results: AgentTestResultsResponse | undefined; + results: TestResultsResponse | undefined; jsonEnabled: boolean; outputDir?: string; verbose?: boolean; @@ -239,6 +389,26 @@ export async function handleTestResults({ const ux = new Ux({ jsonEnabled }); + if (!isLegacyResponse(results)) { + const ngtFormatConfig = { + human: { ext: 'txt', label: 'human-readable', get: () => humanFormatNGT(results), strip: true }, + json: { ext: 'json', label: 'JSON', get: () => convertNGTTestResultsToFormat(results, 'json'), strip: false }, + junit: { ext: 'xml', label: 'JUnit', get: () => convertNGTTestResultsToFormat(results, 'junit'), strip: false }, + tap: { ext: 'txt', label: 'TAP', get: () => convertNGTTestResultsToFormat(results, 'tap'), strip: false }, + } as const; + const cfg = ngtFormatConfig[format]; + const formatted = cfg.get(); + if (outputDir) { + const file = `test-result-${id}.${cfg.ext}`; + await writeFileToDir(outputDir, file, cfg.strip ? stripVTControlCharacters(formatted) : formatted); + ux.log(`Created ${cfg.label} file at ${join(outputDir, file)}`); + } else { + ux.log(formatted); + } + return; + } + + // Legacy response formatting if (format === 'human') { const formatted = humanFormat(results, verbose); if (outputDir) { diff --git a/src/testRunnerFactory.ts b/src/testRunnerFactory.ts new file mode 100644 index 00000000..6e799ccc --- /dev/null +++ b/src/testRunnerFactory.ts @@ -0,0 +1,43 @@ +/* + * 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 { Connection, SfError } from '@salesforce/core'; +import { createAgentTester, AgentTester, AgentTesterNGT, type TestRunnerType } from '@salesforce/agents'; + +export type TestRunnerInstance = AgentTester | AgentTesterNGT; + +export async function createTestRunner( + connection: Connection, + explicitType?: TestRunnerType, + testDefinitionName?: string, + runId?: string +): Promise<{ runner: TestRunnerInstance; type: TestRunnerType }> { + try { + return await createAgentTester(connection, { explicitType, runId, testDefinitionName }); + } catch (e) { + const wrapped = SfError.wrap(e); + if (wrapped.name === 'AmbiguousTestDefinition') { + throw new SfError( + wrapped.message, + wrapped.name, + ['Use --test-runner to explicitly specify the runner type (agentforce-studio or testing-center)'], + undefined, + wrapped + ); + } + throw wrapped; + } +} diff --git a/src/testStages.ts b/src/testStages.ts index 5eff2def..81d985d4 100644 --- a/src/testStages.ts +++ b/src/testStages.ts @@ -16,10 +16,11 @@ import { colorize } from '@oclif/core/ux'; import { MultiStageOutput } from '@oclif/multi-stage-output'; -import { AgentTestResultsResponse, AgentTester } from '@salesforce/agents'; +import { AgentTestResultsResponse, AgentTestNGTResultsResponse } from '@salesforce/agents'; import { Lifecycle } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; import { Ux } from '@salesforce/sf-plugins-core'; +import type { TestRunnerInstance } from './testRunnerFactory.js'; type Data = { id: string; @@ -86,10 +87,10 @@ export class TestStages { } public async poll( - agentTester: AgentTester, + agentTester: TestRunnerInstance, id: string, wait: Duration - ): Promise<{ completed: boolean; response?: AgentTestResultsResponse }> { + ): Promise<{ completed: boolean; response?: AgentTestResultsResponse | AgentTestNGTResultsResponse }> { this.mso.skipTo('Polling for Test Results'); const lifecycle = Lifecycle.getInstance(); lifecycle.on( diff --git a/test/common.test.ts b/test/common.test.ts new file mode 100644 index 00000000..25ec52bb --- /dev/null +++ b/test/common.test.ts @@ -0,0 +1,141 @@ +/* + * 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 { expect } from 'chai'; +import { SfError } from '@salesforce/core'; +import type { CompilationError } from '@salesforce/agents'; +import { throwAgentCompilationError, COMPILATION_API_EXIT_CODES } from '../src/common.js'; + +describe('common', () => { + describe('COMPILATION_API_EXIT_CODES', () => { + it('should re-export COMPILATION_API_EXIT_CODES from @salesforce/agents', () => { + expect(COMPILATION_API_EXIT_CODES).to.be.an('object'); + }); + }); + + describe('throwAgentCompilationError', () => { + it('should throw SfError with unknown error message when given empty array', () => { + try { + throwAgentCompilationError([]); + expect.fail('Expected error to be thrown'); + } catch (e) { + expect(e).to.be.instanceOf(SfError); + expect((e as SfError).name).to.equal('CompileAgentScriptError'); + expect((e as SfError).message).to.equal('Unknown compilation error occurred'); + expect((e as SfError).exitCode).to.equal(1); + } + }); + + it('should throw SfError with formatted error message for single error', () => { + const errors: CompilationError[] = [ + { + errorType: 'SyntaxError', + description: 'Unexpected token', + lineStart: 5, + colStart: 10, + lineEnd: 5, + colEnd: 15, + }, + ]; + + try { + throwAgentCompilationError(errors); + expect.fail('Expected error to be thrown'); + } catch (e) { + expect(e).to.be.instanceOf(SfError); + expect((e as SfError).message).to.equal('SyntaxError: Unexpected token [Ln 5, Col 10]'); + expect((e as SfError).exitCode).to.equal(1); + } + }); + + it('should join multiple errors with EOL separator', () => { + const errors: CompilationError[] = [ + { + errorType: 'SyntaxError', + description: 'Unexpected token', + lineStart: 5, + colStart: 10, + lineEnd: 5, + colEnd: 15, + }, + { + errorType: 'TypeError', + description: 'Cannot read property', + lineStart: 12, + colStart: 3, + lineEnd: 12, + colEnd: 20, + }, + ]; + + try { + throwAgentCompilationError(errors); + expect.fail('Expected error to be thrown'); + } catch (e) { + expect(e).to.be.instanceOf(SfError); + const msg = (e as SfError).message; + expect(msg).to.include('SyntaxError: Unexpected token [Ln 5, Col 10]'); + expect(msg).to.include('TypeError: Cannot read property [Ln 12, Col 3]'); + } + }); + + it('should always set exitCode to 1', () => { + const errors: CompilationError[] = [ + { errorType: 'AnyError', description: 'Something failed', lineStart: 1, colStart: 1, lineEnd: 1, colEnd: 5 }, + ]; + + try { + throwAgentCompilationError(errors); + expect.fail('Expected error to be thrown'); + } catch (e) { + expect((e as SfError).exitCode).to.equal(1); + } + }); + + it('should always set error name to CompileAgentScriptError', () => { + try { + throwAgentCompilationError([]); + expect.fail('Expected error to be thrown'); + } catch (e) { + expect((e as SfError).name).to.equal('CompileAgentScriptError'); + } + }); + + it('should include errors array in data for non-empty input', () => { + const errors: CompilationError[] = [ + { errorType: 'SyntaxError', description: 'Bad token', lineStart: 1, colStart: 1, lineEnd: 1, colEnd: 5 }, + ]; + + try { + throwAgentCompilationError(errors); + expect.fail('Expected error to be thrown'); + } catch (e) { + const sfErr = e as SfError; + expect(sfErr.data).to.deep.equal({ errors }); + } + }); + + it('should include empty array in data for empty input', () => { + try { + throwAgentCompilationError([]); + expect.fail('Expected error to be thrown'); + } catch (e) { + const sfErr = e as SfError; + expect(sfErr.data).to.deep.equal([]); + } + }); + }); +}); diff --git a/test/nuts/z4.agent.test.ngt.nut.ts b/test/nuts/z4.agent.test.ngt.nut.ts new file mode 100644 index 00000000..161927c7 --- /dev/null +++ b/test/nuts/z4.agent.test.ngt.nut.ts @@ -0,0 +1,245 @@ +/* + * 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 { writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { expect } from 'chai'; +import { execCmd, Duration, TestSession } from '@salesforce/cli-plugins-testkit'; +import { ComponentSetBuilder } from '@salesforce/source-deploy-retrieve'; +import { Agent } from '@salesforce/agents'; +import { Org } from '@salesforce/core'; +import { AgentTestCache } from '../../src/agentTestCache.js'; +import type { AgentTestListResult } from '../../src/commands/agent/test/list.js'; +import type { AgentTestResultsResult } from '../../src/commands/agent/test/results.js'; +import type { AgentTestRunResult } from '../../src/flags.js'; +import { getTestSession, getUsername } from './shared-setup.js'; + +/* eslint-disable no-console */ + +// Agentforce Studio (AiTestingDefinition) NUTs. +// Depends on z2 having published a Test_Agent_* agent. The before() hook discovers that +// agent, writes an AiTestingDefinition with the correct subjectName, and deploys it. +describe('agent test (agentforce-studio / NGT)', function () { + this.timeout(30 * 60 * 1000); + + let session: TestSession; + let ngtTestName: string; + + before(async function () { + this.timeout(30 * 60 * 1000); + session = await getTestSession(); + + const org = await Org.create({ aliasOrUsername: getUsername() }); + const connection = org.getConnection(); + + // Find the agent published in z2 + const publishedAgent = (await Agent.listRemote(connection)).find((a) => a.DeveloperName?.startsWith('Test_Agent_')); + if (!publishedAgent?.DeveloperName) { + throw new Error('No published Test_Agent_* found — ensure z2.agent.publish.nut runs first'); + } + + const agentName = publishedAgent.DeveloperName; + ngtTestName = `${agentName}_NGT_Test`; + console.log(`Using agent '${agentName}', test definition '${ngtTestName}'`); + + // Write AiTestingDefinition metadata file + const metaDir = join(session.project.dir, 'force-app', 'main', 'default', 'aiTestingDefinitions'); + mkdirSync(metaDir, { recursive: true }); + + const metaXml = ` + + NGT NUT test for ${agentName} + ${ngtTestName} + ${agentName} + AGENT + v1 + + + Hi, can you tell me what your return or refund policy is? Please include citations in the answer, and use this citations URL: https://help.example.com/citations. + + 1 + + GeneralFAQ + topic_sequence_match + + + ['AnswerQuestionsWithKnowledge'] + action_sequence_match + + + I can help with that. Here's what I found in our knowledge base about the return/refund policy. + bot_response_rating + + + conciseness + + + coherence + + + output_latency_milliseconds + + + completeness + + + + + Hey, I need help with something important—can you take care of it for me? + + 2 + + ambiguous_question + topic_sequence_match + + + + action_sequence_match + + + conciseness + + + coherence + + + output_latency_milliseconds + + + completeness + + + +`; + writeFileSync(join(metaDir, `${ngtTestName}.aiTestingDefinition-meta.xml`), metaXml, 'utf8'); + console.log(`Wrote AiTestingDefinition metadata to ${metaDir}`); + + // Deploy the definition + const cs = await ComponentSetBuilder.build({ + sourcepath: [metaDir], + }); + const deploy = await cs.deploy({ usernameOrConnection: getUsername() }); + await deploy.pollStatus({ frequency: Duration.seconds(10), timeout: Duration.minutes(10) }); + console.log(`Deployed AiTestingDefinition '${ngtTestName}'`); + }); + + // Set by the run test, consumed by the results tests (Mocha runs describes sequentially) + let completedRunId: string; + + describe('agent test list', () => { + it('should include the NGT test definition in list', async () => { + const result = execCmd(`agent test list --target-org ${getUsername()} --json`, { + ensureExitCode: 0, + }).jsonOutput?.result; + expect(result).to.be.ok; + const ngtDefs = result?.filter((r) => r.type?.includes('AiTestingDefinition')); + expect(ngtDefs?.length).to.be.greaterThanOrEqual(1); + expect(ngtDefs?.some((r) => r.fullName === ngtTestName)).to.be.true; + }); + }); + + describe('agent test run', () => { + it('should run with --wait, auto-detect agentforce-studio, and return NGT result shape', function () { + this.timeout(30 * 60 * 1000); + const output = execCmd( + `agent test run --api-name ${ngtTestName} --target-org ${getUsername()} --wait 10 --json`, + { ensureExitCode: 0 } + ).jsonOutput; + + expect(output?.result.status).to.equal('COMPLETED'); + expect(output?.result.runId.startsWith('3A2')).to.be.true; + const result = output?.result as AgentTestRunResult & { testCases?: unknown[] }; + expect(result?.testCases).to.be.an('array').with.length.greaterThan(0); + expect(result).to.not.have.property('subjectName'); + + completedRunId = output!.result.runId; + }); + }); + + describe('agent test results', () => { + it('should fetch NGT results by job ID (json)', async () => { + const output = execCmd( + `agent test results --job-id ${completedRunId} --target-org ${getUsername()} --json`, + { ensureExitCode: 0 } + ).jsonOutput; + + const result = output?.result as { status: string; testCases?: unknown[] }; + expect(result?.status).to.be.a('string'); + expect(result?.testCases).to.be.an('array').with.length.greaterThan(0); + expect(output?.result).to.not.have.property('subjectName'); + }); + + it('should support human result format', () => { + const output = execCmd( + `agent test results --job-id ${completedRunId} --result-format human --target-org ${getUsername()}`, + { ensureExitCode: 0 } + ); + expect(output.shellOutput.stdout).to.be.a('string').with.length.greaterThan(0); + }); + + it('should support junit result format', () => { + const output = execCmd( + `agent test results --job-id ${completedRunId} --result-format junit --target-org ${getUsername()}`, + { ensureExitCode: 0 } + ); + expect(output.shellOutput.stdout).to.include(' { + const output = execCmd( + `agent test results --job-id ${completedRunId} --result-format tap --target-org ${getUsername()}`, + { ensureExitCode: 0 } + ); + expect(output.shellOutput.stdout).to.include('TAP version 13'); + }); + }); + + describe('agent test resume', () => { + it('should start async then resume by job ID, and support --use-most-recent', async () => { + const cache = await AgentTestCache.create(); + cache.clear(); + + // One async start covers both resume paths + const runResult = execCmd( + `agent test run --api-name ${ngtTestName} --target-org ${getUsername()} --json`, + { ensureExitCode: 0 } + ).jsonOutput; + + expect(runResult?.result.runId.startsWith('3A2')).to.be.true; + expect(runResult?.result.status).to.equal('NEW'); + expect(cache.resolveFromCache().runnerType).to.equal('agentforce-studio'); + + const output = execCmd( + `agent test resume --job-id ${runResult?.result.runId} --target-org ${getUsername()} --json`, + { ensureExitCode: 0 } + ).jsonOutput; + + expect(output?.result.status).to.equal('COMPLETED'); + expect(output?.result.runId.startsWith('3A2')).to.be.true; + expect(() => cache.resolveFromCache()).to.throw('Could not find a runId to resume'); + }); + }); + + describe('error handling', () => { + it('should return exit code 2 for a non-existent NGT test definition', () => { + execCmd( + `agent test run --api-name NonExistent_NGT_Test_XYZ --test-runner agentforce-studio --target-org ${getUsername()} --json`, + { ensureExitCode: 2 } + ); + }); + }); +}); diff --git a/test/testRunnerFactory.test.ts b/test/testRunnerFactory.test.ts new file mode 100644 index 00000000..1c1ebb96 --- /dev/null +++ b/test/testRunnerFactory.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { expect } from 'chai'; +import sinon from 'sinon'; +import esmock from 'esmock'; +import { SfError } from '@salesforce/core'; +import type { Connection } from '@salesforce/core'; +import type { TestRunnerType } from '@salesforce/agents'; +import type { createTestRunner as CreateTestRunnerFn } from '../src/testRunnerFactory.js'; + +type MockConnection = Pick; +const makeMockConnection = (): MockConnection => ({ instanceUrl: 'https://test.salesforce.com' }); + +describe('testRunnerFactory', () => { + let createAgentTesterStub: sinon.SinonStub; + let createTestRunner: typeof CreateTestRunnerFn; + + beforeEach(async () => { + createAgentTesterStub = sinon.stub(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const { createTestRunner: fn } = await esmock('../src/testRunnerFactory.js', { + '@salesforce/agents': { + createAgentTester: createAgentTesterStub, + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + createTestRunner = fn as typeof CreateTestRunnerFn; + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('argument passthrough', () => { + it('passes explicitType, runId, and testDefinitionName to createAgentTester', async () => { + const mockResult = { runner: {}, type: 'agentforce-studio' as TestRunnerType }; + createAgentTesterStub.resolves(mockResult); + const connection = makeMockConnection() as Connection; + + await createTestRunner(connection, 'agentforce-studio', 'myTest', '3A2xxx'); + + expect( + createAgentTesterStub.calledOnceWith(connection, { + explicitType: 'agentforce-studio', + runId: '3A2xxx', + testDefinitionName: 'myTest', + }) + ).to.be.true; + }); + + it('passes undefined fields when not provided', async () => { + createAgentTesterStub.resolves({ runner: {}, type: 'testing-center' as TestRunnerType }); + const connection = makeMockConnection() as Connection; + + await createTestRunner(connection); + + expect( + createAgentTesterStub.calledOnceWith(connection, { + explicitType: undefined, + runId: undefined, + testDefinitionName: undefined, + }) + ).to.be.true; + }); + + it('returns the result from createAgentTester', async () => { + const mockRunner = { poll: sinon.stub() }; + const mockResult = { runner: mockRunner, type: 'agentforce-studio' as TestRunnerType }; + createAgentTesterStub.resolves(mockResult); + const connection = makeMockConnection() as Connection; + + const result = await createTestRunner(connection, 'agentforce-studio'); + + expect(result.runner).to.equal(mockRunner); + expect(result.type).to.equal('agentforce-studio'); + }); + }); + + describe('AmbiguousTestDefinition error handling', () => { + it('re-throws with --test-runner action hint', async () => { + const original = new SfError('MySuite exists in both metadata types', 'AmbiguousTestDefinition'); + createAgentTesterStub.rejects(original); + const connection = makeMockConnection() as Connection; + + try { + await createTestRunner(connection, undefined, 'MySuite'); + expect.fail('Expected error was not thrown'); + } catch (err) { + expect(err).to.be.instanceOf(SfError); + const sfErr = err as SfError; + expect(sfErr.name).to.equal('AmbiguousTestDefinition'); + expect(sfErr.actions).to.include( + 'Use --test-runner to explicitly specify the runner type (agentforce-studio or testing-center)' + ); + expect(sfErr.cause).to.equal(original); + } + }); + + it('passes through non-AmbiguousTestDefinition errors unchanged', async () => { + const original = new SfError('Network error', 'NetworkError'); + createAgentTesterStub.rejects(original); + const connection = makeMockConnection() as Connection; + + try { + await createTestRunner(connection, undefined, 'MySuite'); + expect.fail('Expected error was not thrown'); + } catch (err) { + expect(err).to.equal(original); + } + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 45c4afb8..2e814bf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -594,9 +594,9 @@ tslib "^2.6.2" "@aws-sdk/xml-builder@^3.972.20": - version "3.972.20" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.20.tgz#d5f41188072ff6ae9e3e794a3ec62f3c82b9ff28" - integrity sha512-MDcUfroaMAnDAHn29vN781t0wudR8zjfgg+r3s5otx8TJXFWg01NZB7HvHkBbOf7UUmKEwIZf5kHxiaVUgwjlQ== + version "3.972.21" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.21.tgz#8c6a0f17eed26d45e9d30546cd32d56b200dbb22" + integrity sha512-qxNiHUtlrsjTeSlrPWiFkWps7uD6YB4eKzg7eLAFH8jbiHTlt0ePNlo2Xu+WlftP38JIcMaIX4jTUjOlE2ySWw== dependencies: "@nodable/entities" "2.1.0" "@smithy/types" "^4.14.1" @@ -3124,9 +3124,9 @@ baseline-browser-mapping@^2.10.12: integrity sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g== basic-ftp@^5.0.2: - version "5.3.0" - resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.3.0.tgz#88f057d1ba8442643c505c4c83bbaa4442b15cfd" - integrity sha512-5K9eNNn7ywHPsYnFwjKgYH8Hf8B5emh7JKcPaVjjrMJFQQwGpwowEnZNEtHs7DfR7hCZsmaK3VA4HUK0YarT+w== + version "5.3.1" + resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.3.1.tgz#3148ee9af43c0522514a4f973fecb1d3cbb6d71e" + integrity sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw== binary-extensions@^2.0.0: version "2.3.0" @@ -5466,7 +5466,7 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -ip-address@^10.0.1: +ip-address@^10.1.1: version "10.1.1" resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.1.tgz#a7614252413e3751b841aaffba939090d2c4c37b" integrity sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw== @@ -8032,11 +8032,11 @@ socks-proxy-agent@^8.0.5: socks "^2.8.3" socks@^2.8.3: - version "2.8.7" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea" - integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A== + version "2.8.8" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.8.tgz#23bef6d02748eac847ad75610deb6c472554c67a" + integrity sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog== dependencies: - ip-address "^10.0.1" + ip-address "^10.1.1" smart-buffer "^4.2.0" sonic-boom@^4.0.1: @@ -8432,17 +8432,17 @@ tinyglobby@^0.2.14, tinyglobby@^0.2.9: fdir "^6.5.0" picomatch "^4.0.4" -tldts-core@^7.0.28: - version "7.0.28" - resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.0.28.tgz#28c256edae2ed177b2a8338a51caf81d41580ecf" - integrity sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ== +tldts-core@^7.0.29: + version "7.0.29" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.0.29.tgz#c3806f5af57b0351ed9415899be2a8dafa3f56dc" + integrity sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q== tldts@^7.0.5: - version "7.0.28" - resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.0.28.tgz#5a5bb26ef3f70008d88c6e53ff58cd59ed8d4c68" - integrity sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw== + version "7.0.29" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.0.29.tgz#5a246d4ffcdf8b34cd9cc2dea424162a653f69d1" + integrity sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg== dependencies: - tldts-core "^7.0.28" + tldts-core "^7.0.29" to-regex-range@^5.0.1: version "5.0.1"