From 8df61a9fba8005d0823bba5ce5f14d3ab5a5c12e Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 13 Nov 2024 09:34:31 -0700 Subject: [PATCH 01/15] feat: mock agent testing --- src/agent.ts | 55 ++++++++---------------------------- src/agentTester.ts | 61 ++++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/mockDir.ts | 69 +++++++++++++++++++++++++++++++++++++++------- 4 files changed, 132 insertions(+), 54 deletions(-) create mode 100644 src/agentTester.ts diff --git a/src/agent.ts b/src/agent.ts index 01deaa3..cc43d1e 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -5,12 +5,10 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { join } from 'node:path'; -import { readFileSync, statSync } from 'node:fs'; import { inspect } from 'node:util'; import { Connection, Logger, SfError, SfProject } from '@salesforce/core'; import { Duration, sleep } from '@salesforce/kit'; -import { getMockDir } from './mockDir'; +import { mockOrRequest } from './mockDir'; import { type SfAgent, type AgentCreateConfig, @@ -22,11 +20,9 @@ import { export class Agent implements SfAgent { private logger: Logger; - private mockDir?: string; public constructor(private connection: Connection, private project: SfProject) { this.logger = Logger.childFromRoot(this.constructor.name); - this.mockDir = getMockDir(); } public async create(config: AgentCreateConfig): Promise { @@ -50,48 +46,19 @@ export class Agent implements SfAgent { this.verifyAgentSpecConfig(config); let agentSpec: AgentJobSpec; + const response = await mockOrRequest( + this.connection, + 'GET', + this.buildAgentJobSpecUrl(config) + ); - if (this.mockDir) { - const specFileName = `${config.name}.json`; - const specFilePath = join(this.mockDir, `${specFileName}`); - try { - this.logger.debug(`Using mock directory: ${this.mockDir} for agent job spec creation`); - statSync(specFilePath); - } catch (err) { - throw SfError.create({ - name: 'MissingMockFile', - message: `SF_MOCK_DIR [${this.mockDir}] must contain a spec file with name ${specFileName}`, - cause: err, - }); - } - try { - this.logger.debug(`Returning mock agent spec file: ${specFilePath}`); - agentSpec = JSON.parse(readFileSync(specFilePath, 'utf8')) as AgentJobSpec; - } catch (err) { - throw SfError.create({ - name: 'InvalidMockFile', - message: `SF_MOCK_DIR [${this.mockDir}] must contain a valid spec file with name ${specFileName}`, - cause: err, - actions: [ - 'Check that the file is readable', - 'Check that the file is a valid JSON array of jobTitle and jobDescription objects', - ], - }); - } + if (response.isSuccess && response.jobSpecs) { + agentSpec = response.jobSpecs; } else { - // TODO: We'll probably want to wrap this for better error handling but let's see - // what it looks like first. - const response = await this.connection.requestGet(this.buildAgentJobSpecUrl(config), { - retry: { maxRetries: 3 }, + throw SfError.create({ + name: 'AgentJobSpecCreateError', + message: response.errorMessage ?? 'unknown', }); - if (response.isSuccess) { - agentSpec = response?.jobSpecs as AgentJobSpec; - } else { - throw SfError.create({ - name: 'AgentJobSpecCreateError', - message: response.errorMessage ?? 'unknown', - }); - } } return agentSpec; diff --git a/src/agentTester.ts b/src/agentTester.ts new file mode 100644 index 0000000..aa8af0f --- /dev/null +++ b/src/agentTester.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Connection } from '@salesforce/core'; +import { mockOrRequest } from './mockDir'; + +type AgentTestStartResponse = { + id: string; +}; + +type AgentTestStatusResponse = { + status: 'NEW' | 'IN_PROGRESS' | 'COMPLETED' | 'ERROR'; + startTime: string; + endTime?: string; + errorMessage?: string; +}; + +type AgentTestDetailsResponse = { + AiEvaluationSuiteDefinition: string; + tests: Array<{ + AiEvaluationDefinition: string; + results: Array<{ + test_number: number; + results: Array<{ + name: string; + actual: string[]; + is_pass: boolean; + execution_time_ms: number; + error?: string; + }>; + }>; + }>; +}; + +export class AgentTester { + public constructor(private connection: Connection) {} + + public async start(suiteId: string): Promise<{ id: string }> { + const url = `/services/data/${this.connection.getApiVersion()}/einstein/ai-evaluations/runs`; + + return mockOrRequest(this.connection, 'POST', url, { + aiEvaluationSuiteDefinition: suiteId, + }); + } + + public async status(jobId: string): Promise { + const url = `/services/data/${this.connection.getApiVersion()}/einstein/ai-evaluations/runs/${jobId}`; + + return mockOrRequest(this.connection, 'GET', url); + } + + public async details(jobId: string): Promise { + const url = `/services/data/${this.connection.getApiVersion()}/einstein/ai-evaluations/runs/${jobId}/details`; + + return mockOrRequest(this.connection, 'GET', url); + } +} diff --git a/src/index.ts b/src/index.ts index f3c04d3..2faba92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,3 +14,4 @@ export { SfAgent, } from './types'; export { Agent } from './agent'; +export { AgentTester } from './agentTester'; diff --git a/src/mockDir.ts b/src/mockDir.ts index cef8ea5..3f0ebfe 100644 --- a/src/mockDir.ts +++ b/src/mockDir.ts @@ -5,17 +5,17 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { resolve } from 'node:path'; -import { type Stats, statSync } from 'node:fs'; -import { SfError } from '@salesforce/core'; +import { join, resolve } from 'node:path'; +import { readFileSync, type Stats, statSync } from 'node:fs'; +import { Connection, Logger, SfError } from '@salesforce/core'; import { env } from '@salesforce/kit'; /** - * If the `SF_MOCK_DIR` environment variable is set, resolve to an absolue path + * If the `SF_MOCK_DIR` environment variable is set, resolve to an absolute path * and ensure the directory exits, then return the path. - * + * * NOTE: THIS SHOULD BE MOVED TO SOME OTHER LIBRARY LIKE `@salesforce/kit`. - * + * * @returns the absolute path to an existing directory used for mocking behavior */ export const getMockDir = (): string | undefined => { @@ -29,17 +29,66 @@ export const getMockDir = (): string | undefined => { name: 'InvalidMockDir', message: `SF_MOCK_DIR [${mockDir}] not found`, cause: err, - actions: ['If you\'re trying to mock agent behavior you must create the mock directory and add expected mock files to it.'] + actions: [ + "If you're trying to mock agent behavior you must create the mock directory and add expected mock files to it.", + ], }); } - + if (!mockDirStat.isDirectory()) { throw SfError.create({ name: 'InvalidMockDir', message: `SF_MOCK_DIR [${mockDir}] is not a directory`, - actions: ['If you\'re trying to mock agent behavior you must create the mock directory and add expected mock files to it.'] + actions: [ + "If you're trying to mock agent behavior you must create the mock directory and add expected mock files to it.", + ], }); } return mockDir; } -} \ No newline at end of file +}; + +export function mockOrRequest( + connection: Connection, + method: 'GET' | 'POST', + url: string, + body?: Record +): Promise { + const mockDir = getMockDir(); + const logger = Logger.childFromRoot('mockOrRequest'); + if (mockDir) { + logger.debug(`Mocking ${method} request to ${url} using ${mockDir}`); + const mockResponseFileName = url.replace(/\//g, '_').replace(/^_/, '').split('?')[0] + '.json'; + const mockResponseFilePath = join(mockDir, mockResponseFileName); + logger.debug(`Using mock file: ${mockResponseFilePath} for ${url}`); + try { + return Promise.resolve(JSON.parse(readFileSync(mockResponseFilePath, 'utf-8')) as T); + } catch (err) { + throw SfError.create({ + name: 'MissingMockFile', + message: `SF_MOCK_DIR [${mockDir}] must contain a spec file with name ${mockResponseFileName}`, + cause: err, + }); + } + } else { + logger.debug(`Making ${method} request to ${url}`); + switch (method) { + case 'GET': + return connection.requestGet(url, { retry: { maxRetries: 3 } }); + case 'POST': + if (!body) { + throw SfError.create({ + name: 'InvalidBody', + message: 'POST requests must include a body', + }); + } + return connection.requestPost(url, body, { retry: { maxRetries: 3 } }); + default: + throw SfError.create({ + name: 'InvalidMethod', + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + message: `Invalid method: ${method}`, + }); + } + } +} From 334988d753f942fbfecdaa776e2285c51b81ebf5 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 13 Nov 2024 12:49:54 -0700 Subject: [PATCH 02/15] feat: mocked agent testing --- package.json | 4 +- src/agent.ts | 12 +- src/agentTester.ts | 164 +++++++++++++++- src/mockDir.ts | 135 +++++++++---- yarn.lock | 459 +++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 685 insertions(+), 89 deletions(-) diff --git a/package.json b/package.json index 316b8d5..8474f14 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,10 @@ "url": "https://github.com/forcedotcom/agents.git" }, "dependencies": { + "@oclif/table": "^0.3.3", "@salesforce/core": "^8.5.2", - "@salesforce/kit": "^3.2.3" + "@salesforce/kit": "^3.2.3", + "nock": "^13.5.6" }, "devDependencies": { "@salesforce/cli-plugins-testkit": "^5.3.20", diff --git a/src/agent.ts b/src/agent.ts index cc43d1e..8d25774 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -8,7 +8,7 @@ import { inspect } from 'node:util'; import { Connection, Logger, SfError, SfProject } from '@salesforce/core'; import { Duration, sleep } from '@salesforce/kit'; -import { mockOrRequest } from './mockDir'; +import { MaybeMock } from './mockDir'; import { type SfAgent, type AgentCreateConfig, @@ -20,9 +20,11 @@ import { export class Agent implements SfAgent { private logger: Logger; + private maybeMock: MaybeMock; - public constructor(private connection: Connection, private project: SfProject) { + public constructor(connection: Connection, private project: SfProject) { this.logger = Logger.childFromRoot(this.constructor.name); + this.maybeMock = new MaybeMock(connection); } public async create(config: AgentCreateConfig): Promise { @@ -46,11 +48,7 @@ export class Agent implements SfAgent { this.verifyAgentSpecConfig(config); let agentSpec: AgentJobSpec; - const response = await mockOrRequest( - this.connection, - 'GET', - this.buildAgentJobSpecUrl(config) - ); + const response = await this.maybeMock.request('GET', this.buildAgentJobSpecUrl(config)); if (response.isSuccess && response.jobSpecs) { agentSpec = response.jobSpecs; diff --git a/src/agentTester.ts b/src/agentTester.ts index aa8af0f..532e2cf 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -4,9 +4,11 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import { Connection, PollingClient, StatusResult } from '@salesforce/core'; +import { Duration } from '@salesforce/kit'; +import { MaybeMock } from './mockDir'; -import { Connection } from '@salesforce/core'; -import { mockOrRequest } from './mockDir'; +type Format = 'human' | 'tap' | 'junit' | 'json'; type AgentTestStartResponse = { id: string; @@ -37,25 +39,167 @@ type AgentTestDetailsResponse = { }; export class AgentTester { - public constructor(private connection: Connection) {} + private maybeMock: MaybeMock; + public constructor(connection: Connection) { + this.maybeMock = new MaybeMock(connection); + } public async start(suiteId: string): Promise<{ id: string }> { - const url = `/services/data/${this.connection.getApiVersion()}/einstein/ai-evaluations/runs`; + const url = '/einstein/ai-evaluations/runs'; - return mockOrRequest(this.connection, 'POST', url, { + return this.maybeMock.request('POST', url, { aiEvaluationSuiteDefinition: suiteId, }); } public async status(jobId: string): Promise { - const url = `/services/data/${this.connection.getApiVersion()}/einstein/ai-evaluations/runs/${jobId}`; + const url = `/einstein/ai-evaluations/runs/${jobId}`; + + return this.maybeMock.request('GET', url); + } + + public async poll( + jobId: string, + { + format = 'human', + timeout = Duration.minutes(5), + }: { + format?: Format; + timeout?: Duration; + } + ): Promise<{ response: AgentTestDetailsResponse; formatted: string }> { + const client = await PollingClient.create({ + poll: async (): Promise => { + const { status } = await this.status(jobId); + if (status === 'COMPLETED') { + return { payload: await this.details(jobId, format), completed: true }; + } - return mockOrRequest(this.connection, 'GET', url); + return { completed: false }; + }, + frequency: Duration.seconds(1), + timeout, + }); + + const result = await client.subscribe<{ response: AgentTestDetailsResponse; formatted: string }>(); + return result; } - public async details(jobId: string): Promise { - const url = `/services/data/${this.connection.getApiVersion()}/einstein/ai-evaluations/runs/${jobId}/details`; + public async details( + jobId: string, + format: Format = 'human' + ): Promise<{ response: AgentTestDetailsResponse; formatted: string }> { + const url = `/einstein/ai-evaluations/runs/${jobId}/details`; + + const response = await this.maybeMock.request('GET', url); + return { + response, + formatted: + format === 'human' + ? await humanFormat(response) + : format === 'tap' + ? await tapFormat(response) + : format === 'junit' + ? await junitFormat(response) + : await jsonFormat(response), + }; + } +} - return mockOrRequest(this.connection, 'GET', url); +export async function humanFormat(details: AgentTestDetailsResponse): Promise { + // TODO: these tables need to follow the same defaults that sf-plugins-core uses + // TODO: the api response isn't finalized so this is just a POC + const { makeTable } = await import('@oclif/table'); + const tables: string[] = []; + for (const aiEvalDef of details.tests) { + for (const result of aiEvalDef.results) { + const table = makeTable({ + title: `Test Results for ${aiEvalDef.AiEvaluationDefinition} (#${result.test_number})`, + data: result.results.map((r) => ({ + 'TEST NAME': r.name, + OUTCOME: r.is_pass ? 'Pass' : 'Fail', + MESSAGE: r.error ?? '', + 'RUNTIME (MS)': r.execution_time_ms, + })), + }); + tables.push(table); + } } + + return tables.join('\n'); +} + +export async function junitFormat(details: AgentTestDetailsResponse): Promise { + // APEX EXAMPLE + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + await Promise.reject(new Error('Not implemented')); + return JSON.stringify(details, null, 2); +} + +export async function tapFormat(details: AgentTestDetailsResponse): Promise { + // APEX EXAMPLE + // 1..11 + // ok 1 TestPropertyController.testGetPagedPropertyList + // ok 2 TestPropertyController.testGetPicturesNoResults + // ok 3 TestPropertyController.testGetPicturesWithResults + // ok 4 FileUtilitiesTest.createFileFailsWhenIncorrectBase64Data + // ok 5 FileUtilitiesTest.createFileFailsWhenIncorrectFilename + // ok 6 FileUtilitiesTest.createFileFailsWhenIncorrectRecordId + // ok 7 FileUtilitiesTest.createFileSucceedsWhenCorrectInput + // ok 8 TestSampleDataController.importSampleData + // ok 9 GeocodingServiceTest.blankAddress + // ok 10 GeocodingServiceTest.errorResponse + // ok 11 GeocodingServiceTest.successResponse + // # Run "sf apex get test -i 707Ei00000dUJry -o test-mgoe8ogsltwe@example.com --result-format " to retrieve test results in a different format. + await Promise.reject(new Error('Not implemented')); + return JSON.stringify(details, null, 2); +} + +export async function jsonFormat(details: AgentTestDetailsResponse): Promise { + return Promise.resolve(JSON.stringify(details, null, 2)); } diff --git a/src/mockDir.ts b/src/mockDir.ts index 3f0ebfe..68156fd 100644 --- a/src/mockDir.ts +++ b/src/mockDir.ts @@ -6,9 +6,11 @@ */ import { join, resolve } from 'node:path'; -import { readFileSync, type Stats, statSync } from 'node:fs'; +import { type Stats, statSync } from 'node:fs'; +import { readdir, readFile } from 'node:fs/promises'; import { Connection, Logger, SfError } from '@salesforce/core'; import { env } from '@salesforce/kit'; +import nock from 'nock'; /** * If the `SF_MOCK_DIR` environment variable is set, resolve to an absolute path @@ -18,7 +20,7 @@ import { env } from '@salesforce/kit'; * * @returns the absolute path to an existing directory used for mocking behavior */ -export const getMockDir = (): string | undefined => { +const getMockDir = (): string | undefined => { const mockDir = env.getString('SF_MOCK_DIR'); if (mockDir) { let mockDirStat: Stats; @@ -48,33 +50,106 @@ export const getMockDir = (): string | undefined => { } }; -export function mockOrRequest( - connection: Connection, - method: 'GET' | 'POST', - url: string, - body?: Record -): Promise { - const mockDir = getMockDir(); - const logger = Logger.childFromRoot('mockOrRequest'); - if (mockDir) { - logger.debug(`Mocking ${method} request to ${url} using ${mockDir}`); - const mockResponseFileName = url.replace(/\//g, '_').replace(/^_/, '').split('?')[0] + '.json'; - const mockResponseFilePath = join(mockDir, mockResponseFileName); - logger.debug(`Using mock file: ${mockResponseFilePath} for ${url}`); - try { - return Promise.resolve(JSON.parse(readFileSync(mockResponseFilePath, 'utf-8')) as T); - } catch (err) { - throw SfError.create({ - name: 'MissingMockFile', - message: `SF_MOCK_DIR [${mockDir}] must contain a spec file with name ${mockResponseFileName}`, - cause: err, - }); +async function readJson(path: string): Promise { + return JSON.parse(await readFile(path, 'utf-8')) as T; +} + +async function readPlainText(path: string): Promise { + return readFile(path, 'utf-8'); +} + +async function readDirectory(path: string): Promise { + const files = await readdir(path); + const promises = files.map((file) => { + if (file.endsWith('.json')) { + return readJson(join(path, file)); + } else { + return readPlainText(join(path, file)); + } + }); + return (await Promise.all(promises)).filter((r): r is T => !!r); +} + +async function readResponses(mockDir: string, url: string, logger: Logger): Promise { + const mockResponseName = url.replace(/\//g, '_').replace(/^_/, '').split('?')[0]; + const mockResponsePath = join(mockDir, mockResponseName); + + // Try all possibilities for the mock response file + const responses = ( + await Promise.all([ + readJson(`${mockResponsePath}.json`) + .then((r) => { + logger.debug(`Found JSON mock file: ${mockResponsePath}.json`); + return r; + }) + .catch(() => undefined), + readPlainText(mockResponsePath) + .then((r) => { + logger.debug(`Found plain text mock file: ${mockResponsePath}`); + return r; + }) + .catch(() => undefined), + readDirectory(mockResponsePath) + .then((r) => { + logger.debug(`Found directory of mock files: ${mockResponsePath}`); + return r; + }) + .catch(() => undefined), + ]) + ) + .filter((r): r is T[] => !!r) + .flat(); + if (responses.length === 0) { + throw SfError.create({ + name: 'MissingMockFile', + message: `SF_MOCK_DIR [${mockDir}] must contain a spec file with name ${mockResponsePath} or ${mockResponsePath}.json`, + }); + } + + logger.debug(`Using responses: ${responses.map((r) => JSON.stringify(r)).join(', ')}`); + + return responses; +} + +export class MaybeMock { + private mockDir = getMockDir(); + private scopes = new Map(); + private logger: Logger; + + public constructor(private connection: Connection) { + this.logger = Logger.childFromRoot(this.constructor.name); + } + + public async request( + method: 'GET' | 'POST', + url: string, + body?: nock.RequestBodyMatcher + ): Promise { + if (this.mockDir) { + this.logger.debug(`Mocking ${method} request to ${url} using ${this.mockDir}`); + const responses = await readResponses(this.mockDir, url, this.logger); + if (!this.scopes.has(url)) { + const scope = nock(this.connection.baseUrl()); + this.scopes.set(url, scope); + switch (method) { + case 'GET': + for (const response of responses) { + scope.get(url).reply(200, response); + } + break; + case 'POST': + for (const response of responses) { + scope.post(url, body).reply(200, response); + } + break; + } + } } - } else { - logger.debug(`Making ${method} request to ${url}`); + + this.logger.debug(`Making ${method} request to ${url}`); switch (method) { case 'GET': - return connection.requestGet(url, { retry: { maxRetries: 3 } }); + return this.connection.requestGet(url, { retry: { maxRetries: 3 } }); case 'POST': if (!body) { throw SfError.create({ @@ -82,13 +157,7 @@ export function mockOrRequest( message: 'POST requests must include a body', }); } - return connection.requestPost(url, body, { retry: { maxRetries: 3 } }); - default: - throw SfError.create({ - name: 'InvalidMethod', - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - message: `Invalid method: ${method}`, - }); + return this.connection.requestPost(url, body, { retry: { maxRetries: 3 } }); } } } diff --git a/yarn.lock b/yarn.lock index b52db24..29cb98b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,14 @@ resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@alcalzone/ansi-tokenize@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz#9f89839561325a8e9a0c32360b8d17e48489993f" + integrity sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^4.0.0" + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" @@ -500,6 +508,46 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@oclif/core@^4": + version "4.0.32" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.32.tgz#0e8078c53b079549d685798893b9f9534ca69bf6" + integrity sha512-O3jfIAhqaJxXI2dzF81PLTMhKpFFA0Nyz8kfBnc9WYDJnvdmXK0fVAOSpwpi2mHTow/9FXxY6Kww8+Kbe7/sag== + dependencies: + ansi-escapes "^4.3.2" + ansis "^3.3.2" + clean-stack "^3.0.1" + cli-spinners "^2.9.2" + debug "^4.3.7" + ejs "^3.1.10" + get-package-type "^0.1.0" + globby "^11.1.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + lilconfig "^3.1.2" + minimatch "^9.0.5" + semver "^7.6.3" + string-width "^4.2.3" + supports-color "^8" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + +"@oclif/table@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@oclif/table/-/table-0.3.3.tgz#5dc1c98cfa5415b131d77c85048df187fc241d12" + integrity sha512-sz6gGT1JAPP743vxl1491hxboIu0ZFHaP3gyvhz5Prgsuljp2NGyyu7JPEMeVImCnZ9N3K9cy3VXxRFEwRH/ig== + dependencies: + "@oclif/core" "^4" + "@types/react" "^18.3.12" + change-case "^5.4.4" + cli-truncate "^4.0.0" + ink "^5.0.1" + natural-orderby "^3.0.2" + object-hash "^3.0.0" + react "^18.3.1" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" @@ -782,6 +830,19 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/prop-types@*": + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + +"@types/react@^18.3.12": + version "18.3.12" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60" + integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/semver@^7.5.0": version "7.5.8" resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz" @@ -980,6 +1041,20 @@ ansi-colors@^4.1.3: resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== +ansi-escapes@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" @@ -1004,11 +1079,16 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.0: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +ansis@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.3.2.tgz#15adc36fea112da95c74d309706e593618accac3" + integrity sha512-cFthbBlt+Oi0i9Pv/j6YdVWJh54CtjGACaMPCIrEV4Ha7HWsIjXDwseYV79TIL0B4+KfSwD5S70PeQDkPUd1rA== + anymatch@~3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" @@ -1139,6 +1219,11 @@ assertion-error@^1.1.0: resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -1149,6 +1234,11 @@ atomic-sleep@^1.0.0: resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +auto-bind@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-5.0.1.tgz#50d8e63ea5a1dddcb5e5e36451c1a8266ffbb2ae" + integrity sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg== + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" @@ -1333,7 +1423,7 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1341,7 +1431,7 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.0.0: +chalk@^5.0.0, chalk@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== @@ -1364,6 +1454,11 @@ change-case@^4.1.2: snake-case "^3.0.4" tslib "^2.0.3" +change-case@^5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" + integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== + check-error@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz" @@ -1403,6 +1498,38 @@ clean-stack@^2.0.0: resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-stack@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-3.0.1.tgz#155bf0b2221bf5f4fba89528d24c5953f17fe3a8" + integrity sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg== + dependencies: + escape-string-regexp "4.0.0" + +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + +cli-spinners@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" @@ -1421,6 +1548,13 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +code-excerpt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-4.0.0.tgz#2de7d46e98514385cb01f7b3b741320115f4c95e" + integrity sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA== + dependencies: + convert-to-spaces "^2.0.1" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -1528,6 +1662,11 @@ convert-source-map@^2.0.0: resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +convert-to-spaces@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz#61a6c98f8aa626c16b296b862a91412a33bceb6b" + integrity sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ== + core-js-compat@^3.34.0: version "3.36.0" resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz" @@ -1576,6 +1715,11 @@ csprng@*: dependencies: sequin "*" +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + csv-parse@^5.5.2: version "5.5.5" resolved "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.5.tgz" @@ -1757,11 +1901,23 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" +ejs@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.5.4: version "1.5.4" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz" integrity sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA== +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -1784,6 +1940,11 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" @@ -1876,15 +2037,20 @@ escape-html@^1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== eslint-config-prettier@^9.1.0: version "9.1.0" @@ -2229,6 +2395,13 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" @@ -2375,6 +2548,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz" @@ -2709,6 +2887,11 @@ indent-string@^4.0.0: resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -2727,6 +2910,36 @@ ini@^1.3.4: resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ink@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ink/-/ink-5.0.1.tgz#f2ef9796a3911830c3995dedd227ec84ae27de4b" + integrity sha512-ae4AW/t8jlkj/6Ou21H2av0wxTk8vrGzXv+v2v7j4in+bl1M5XRMVbfNghzhBokV++FjF8RBDJvYo+ttR9YVRg== + dependencies: + "@alcalzone/ansi-tokenize" "^0.1.3" + ansi-escapes "^7.0.0" + ansi-styles "^6.2.1" + auto-bind "^5.0.1" + chalk "^5.3.0" + cli-boxes "^3.0.0" + cli-cursor "^4.0.0" + cli-truncate "^4.0.0" + code-excerpt "^4.0.0" + indent-string "^5.0.0" + is-in-ci "^0.1.0" + lodash "^4.17.21" + patch-console "^2.0.0" + react-reconciler "^0.29.0" + scheduler "^0.23.0" + signal-exit "^3.0.7" + slice-ansi "^7.1.0" + stack-utils "^2.0.6" + string-width "^7.0.0" + type-fest "^4.8.3" + widest-line "^5.0.0" + wrap-ansi "^9.0.0" + ws "^8.15.0" + yoga-wasm-web "~0.3.3" + internal-slot@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz" @@ -2803,6 +3016,11 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -2813,6 +3031,18 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -2820,6 +3050,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-in-ci@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-0.1.0.tgz#5e07d6a02ec3a8292d3f590973357efa3fceb0d3" + integrity sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" @@ -2927,6 +3162,13 @@ is-windows@^1.0.2: resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" @@ -3017,12 +3259,22 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + joycon@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -3089,6 +3341,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + json5@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" @@ -3193,6 +3450,11 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +lilconfig@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" @@ -3325,7 +3587,7 @@ lodash.upperfirst@^4.3.1: resolved "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz" integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== -lodash@^4.17.15: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3338,6 +3600,13 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loupe@^2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz" @@ -3583,6 +3852,11 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +natural-orderby@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-3.0.2.tgz#1b874d685fbd68beab2c6e7d14f298e03d631ec3" + integrity sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g== + nise@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz" @@ -3613,6 +3887,15 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +nock@^13.5.6: + version "13.5.6" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.6.tgz#5e693ec2300bbf603b61dae6df0225673e6c4997" + integrity sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + propagate "^2.0.0" + node-fetch@^2.6.1, node-fetch@^2.6.9: version "2.7.0" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" @@ -3697,6 +3980,11 @@ nyc@^17.0.0: test-exclude "^6.0.0" yargs "^15.0.2" +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.13.1, object-inspect@^1.9.0: version "1.13.1" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz" @@ -3864,6 +4152,11 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" +patch-console@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/patch-console/-/patch-console-2.0.0.tgz#9023f4665840e66f40e9ce774f904a63167433bb" + integrity sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA== + path-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz" @@ -4044,6 +4337,11 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + proper-lockfile@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz" @@ -4108,6 +4406,21 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +react-reconciler@^0.29.0: + version "0.29.2" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.29.2.tgz#8ecfafca63549a4f4f3e4c1e049dd5ad9ac3a54f" + integrity sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" @@ -4261,6 +4574,14 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.22.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@^0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" @@ -4324,6 +4645,13 @@ sax@>=0.6.0: resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +scheduler@^0.23.0, scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + secure-json-parse@^2.4.0: version "2.7.0" resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz" @@ -4444,7 +4772,7 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -4483,6 +4811,22 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz" @@ -4579,16 +4923,14 @@ srcset@^5.0.0: resolved "https://registry.npmjs.org/srcset/-/srcset-5.0.0.tgz" integrity sha512-SqEZaAEhe0A6ETEa9O1IhSPC7MdvehZtCnTR0AftXk3QhY2UNgb+NApFOUPZILXk/YTDfFxMTNJOBpzrJsEdIA== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +stack-utils@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" + escape-string-regexp "^2.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4606,6 +4948,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string.prototype.trim@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz" @@ -4647,21 +4998,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -4709,7 +5053,7 @@ supports-color@^7, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.1.1: +supports-color@^8, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -4864,6 +5208,11 @@ type-fest@^0.20.2: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz" @@ -4874,6 +5223,11 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^4.8.3: + version "4.26.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" + integrity sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg== + typed-array-buffer@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz" @@ -5102,6 +5456,20 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +widest-line@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-5.0.0.tgz#b74826a1e480783345f0cd9061b49753c9da70d0" + integrity sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA== + dependencies: + string-width "^7.0.0" + wireit@^0.14.5: version "0.14.5" resolved "https://registry.npmjs.org/wireit/-/wireit-0.14.5.tgz" @@ -5113,12 +5481,17 @@ wireit@^0.14.5: jsonc-parser "^3.0.0" proper-lockfile "^4.1.2" +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + workerpool@^6.5.1: version "6.5.1" resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5136,15 +5509,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -5154,6 +5518,15 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" @@ -5169,6 +5542,11 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@^8.15.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xml2js@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz" @@ -5292,3 +5670,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yoga-wasm-web@~0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba" + integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== From 50b1615c5f0077d109deec302b7e05f8b5b054a9 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 13 Nov 2024 12:55:39 -0700 Subject: [PATCH 03/15] chore: rename --- src/agent.ts | 2 +- src/agentTester.ts | 2 +- src/{mockDir.ts => maybe-mock.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{mockDir.ts => maybe-mock.ts} (100%) diff --git a/src/agent.ts b/src/agent.ts index 8d25774..82030ed 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -8,7 +8,7 @@ import { inspect } from 'node:util'; import { Connection, Logger, SfError, SfProject } from '@salesforce/core'; import { Duration, sleep } from '@salesforce/kit'; -import { MaybeMock } from './mockDir'; +import { MaybeMock } from './maybe-mock'; import { type SfAgent, type AgentCreateConfig, diff --git a/src/agentTester.ts b/src/agentTester.ts index 532e2cf..e9ec0e1 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -6,7 +6,7 @@ */ import { Connection, PollingClient, StatusResult } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; -import { MaybeMock } from './mockDir'; +import { MaybeMock } from './maybe-mock'; type Format = 'human' | 'tap' | 'junit' | 'json'; diff --git a/src/mockDir.ts b/src/maybe-mock.ts similarity index 100% rename from src/mockDir.ts rename to src/maybe-mock.ts From 8ef2616e43bdd9fe064459dd21fd397520749b9f Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 13 Nov 2024 13:53:11 -0700 Subject: [PATCH 04/15] chore: tests --- src/agent.ts | 1 - src/maybe-mock.ts | 29 ++++----- test/agentJobSpecCreate.test.ts | 31 ++++++++- test/mocks/connect_agent-job-spec.json | 90 ++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 test/mocks/connect_agent-job-spec.json diff --git a/src/agent.ts b/src/agent.ts index 82030ed..09c533e 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -49,7 +49,6 @@ export class Agent implements SfAgent { let agentSpec: AgentJobSpec; const response = await this.maybeMock.request('GET', this.buildAgentJobSpecUrl(config)); - if (response.isSuccess && response.jobSpecs) { agentSpec = response.jobSpecs; } else { diff --git a/src/maybe-mock.ts b/src/maybe-mock.ts index 68156fd..bc76375 100644 --- a/src/maybe-mock.ts +++ b/src/maybe-mock.ts @@ -128,21 +128,20 @@ export class MaybeMock { if (this.mockDir) { this.logger.debug(`Mocking ${method} request to ${url} using ${this.mockDir}`); const responses = await readResponses(this.mockDir, url, this.logger); - if (!this.scopes.has(url)) { - const scope = nock(this.connection.baseUrl()); - this.scopes.set(url, scope); - switch (method) { - case 'GET': - for (const response of responses) { - scope.get(url).reply(200, response); - } - break; - case 'POST': - for (const response of responses) { - scope.post(url, body).reply(200, response); - } - break; - } + const baseUrl = this.connection.baseUrl(); + const scope = this.scopes.get(baseUrl) ?? nock(baseUrl); + this.scopes.set(baseUrl, scope); + switch (method) { + case 'GET': + for (const response of responses) { + scope.get(url).reply(200, response); + } + break; + case 'POST': + for (const response of responses) { + scope.post(url, body).reply(200, response); + } + break; } } diff --git a/test/agentJobSpecCreate.test.ts b/test/agentJobSpecCreate.test.ts index 8c242a3..c04e289 100644 --- a/test/agentJobSpecCreate.test.ts +++ b/test/agentJobSpecCreate.test.ts @@ -8,22 +8,51 @@ import { expect } from 'chai'; import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; import { SfProject } from '@salesforce/core'; import { Agent } from '../src/agent'; +import { AgentJobSpecCreateConfig } from '../src/types'; describe('agent job spec create test', () => { const $$ = new TestContext(); const testOrg = new MockTestOrgData(); $$.inProject(true); + process.env.SF_MOCK_DIR = 'test/mocks'; + it('runs agent run test', async () => { const connection = await testOrg.getConnection(); + connection.instanceUrl = 'https://mydomain.salesforce.com'; + const sfProject = SfProject.getInstance(); + $$.SANDBOXES.CONNECTION.restore(); + const agent = new Agent(connection, sfProject); + const output = await agent.createSpec({ + name: 'MyFirstAgent', + type: 'customer_facing', + role: 'answer questions about vacation_rentals', + companyName: 'Coral Cloud Enterprises', + companyDescription: 'Provide vacation rentals and activities', + }); + + // TODO: make this assertion more meaningful + expect(output).to.be.ok; + }); + + it('creates an agent', async () => { + const connection = await testOrg.getConnection(); + connection.instanceUrl = 'https://mydomain.salesforce.com'; const sfProject = SfProject.getInstance(); + $$.SANDBOXES.CONNECTION.restore(); const agent = new Agent(connection, sfProject); - const output = agent.createSpec({ + const opts: AgentJobSpecCreateConfig = { name: 'MyFirstAgent', type: 'customer_facing', role: 'answer questions about vacation rentals', companyName: 'Coral Cloud Enterprises', companyDescription: 'Provide vacation rentals and activities', + }; + const jobSpecs = await agent.createSpec(opts); + expect(jobSpecs).to.be.ok; + const output = agent.create({ + ...opts, + jobSpecs, }); expect(output).to.be.ok; }); diff --git a/test/mocks/connect_agent-job-spec.json b/test/mocks/connect_agent-job-spec.json new file mode 100644 index 0000000..5f32c3b --- /dev/null +++ b/test/mocks/connect_agent-job-spec.json @@ -0,0 +1,90 @@ +{ + "isSuccess": true, + "type": "customer_facing", + "role": "replace me", + "companyName": "replace me", + "companyDescription": "replace me", + "companyWebsite": "replace me", + "jobSpecs": [ + { + "jobTitle": "Guest_Experience_Enhancement", + "jobDescription": "Develop and implement entertainment programs to enhance guest experience." + }, + { + "jobTitle": "Event_Planning_and_Execution", + "jobDescription": "Plan, organize, and execute resort events and activities." + }, + { + "jobTitle": "Vendor_Management", + "jobDescription": "Coordinate with external vendors for event supplies and services." + }, + { + "jobTitle": "Staff_Training_and_Development", + "jobDescription": "Train and develop staff to deliver exceptional entertainment services." + }, + { + "jobTitle": "Budget_Management", + "jobDescription": "Manage budgets for entertainment activities and events." + }, + { + "jobTitle": "Guest_Feedback_Analysis", + "jobDescription": "Collect and analyze guest feedback to improve entertainment offerings." + }, + { + "jobTitle": "Marketing_Collaboration", + "jobDescription": "Work with marketing to promote events and entertainment activities." + }, + { + "jobTitle": "Technology_Integration", + "jobDescription": "Utilize technology to enhance guest engagement and streamline operations." + }, + { + "jobTitle": "Safety_and_Compliance", + "jobDescription": "Ensure all entertainment activities comply with safety regulations." + }, + { + "jobTitle": "Performance_Monitoring", + "jobDescription": "Monitor and evaluate the performance of entertainment programs." + }, + { + "jobTitle": "Community_Partnerships", + "jobDescription": "Build partnerships with local artists and performers." + }, + { + "jobTitle": "Inventory_Management", + "jobDescription": "Manage inventory of entertainment equipment and supplies." + }, + { + "jobTitle": "Custom_Experience_Creation", + "jobDescription": "Design personalized entertainment experiences for VIP guests." + }, + { + "jobTitle": "Data_Reporting", + "jobDescription": "Generate reports on entertainment program performance and guest satisfaction." + }, + { + "jobTitle": "Crisis_Management", + "jobDescription": "Develop plans to handle emergencies during entertainment events." + }, + { + "jobTitle": "Digital_Engagement", + "jobDescription": "Enhance online presence and engagement through social media." + }, + { + "jobTitle": "Salesforce_Integration", + "jobDescription": "Utilize Salesforce to track guest preferences and tailor entertainment." + }, + { + "jobTitle": "Trend_Analysis", + "jobDescription": "Stay updated on industry trends to keep entertainment offerings fresh." + }, + { + "jobTitle": "Cross_Department_Coordination", + "jobDescription": "Collaborate with other departments to ensure seamless guest experience." + }, + { + "jobTitle": "Resource_Optimization", + "jobDescription": "Optimize the use of resources to maximize guest satisfaction." + } + ] +} From 3b1f6217a30db08b877854f3f022b325d8e0419d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 14 Nov 2024 09:30:02 -0700 Subject: [PATCH 05/15] test: cleanup --- test/{agentJobSpecCreate.test.ts => agents.test.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename test/{agentJobSpecCreate.test.ts => agents.test.ts} (93%) diff --git a/test/agentJobSpecCreate.test.ts b/test/agents.test.ts similarity index 93% rename from test/agentJobSpecCreate.test.ts rename to test/agents.test.ts index c04e289..a451e6a 100644 --- a/test/agentJobSpecCreate.test.ts +++ b/test/agents.test.ts @@ -10,14 +10,14 @@ import { SfProject } from '@salesforce/core'; import { Agent } from '../src/agent'; import { AgentJobSpecCreateConfig } from '../src/types'; -describe('agent job spec create test', () => { +describe('Agents', () => { const $$ = new TestContext(); const testOrg = new MockTestOrgData(); $$.inProject(true); process.env.SF_MOCK_DIR = 'test/mocks'; - it('runs agent run test', async () => { + it('createSpec', async () => { const connection = await testOrg.getConnection(); connection.instanceUrl = 'https://mydomain.salesforce.com'; const sfProject = SfProject.getInstance(); @@ -35,7 +35,7 @@ describe('agent job spec create test', () => { expect(output).to.be.ok; }); - it('creates an agent', async () => { + it('create', async () => { const connection = await testOrg.getConnection(); connection.instanceUrl = 'https://mydomain.salesforce.com'; const sfProject = SfProject.getInstance(); From 4e16014ca8255931e9667085d80ea57d3f346417 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 14 Nov 2024 09:54:16 -0700 Subject: [PATCH 06/15] test: agentTester tests --- src/agentTester.ts | 3 + test/agentTester.test.ts | 85 +++++++++++++++++++ test/agents.test.ts | 27 +++--- test/mocks/einstein_ai-evaluations_runs.json | 3 + .../1.json | 4 + .../2.json | 4 + .../3.json | 4 + ...tions_runs_4KBSM000000003F4AQ_details.json | 41 +++++++++ 8 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 test/agentTester.test.ts create mode 100644 test/mocks/einstein_ai-evaluations_runs.json create mode 100644 test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/1.json create mode 100644 test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/2.json create mode 100644 test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/3.json create mode 100644 test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json diff --git a/src/agentTester.ts b/src/agentTester.ts index e9ec0e1..1119bab 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -66,6 +66,9 @@ export class AgentTester { }: { format?: Format; timeout?: Duration; + } = { + format: 'human', + timeout: Duration.minutes(5), } ): Promise<{ response: AgentTestDetailsResponse; formatted: string }> { const client = await PollingClient.create({ diff --git a/test/agentTester.test.ts b/test/agentTester.test.ts new file mode 100644 index 0000000..042ff14 --- /dev/null +++ b/test/agentTester.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; +import { Connection } from '@salesforce/core'; +import { AgentTester } from '../src/agentTester'; + +describe('AgentTester', () => { + const $$ = new TestContext(); + + let testOrg: MockTestOrgData; + let connection: Connection; + + beforeEach(async () => { + $$.inProject(true); + testOrg = new MockTestOrgData(); + process.env.SF_MOCK_DIR = 'test/mocks'; + connection = await testOrg.getConnection(); + connection.instanceUrl = 'https://mydomain.salesforce.com'; + // restore the connection sandbox so that it doesn't override the builtin mocking (MaybeMock) + $$.SANDBOXES.CONNECTION.restore(); + }); + + afterEach(() => { + delete process.env.SF_MOCK_DIR; + }); + + describe('start', () => { + it('should start test run', async () => { + const tester = new AgentTester(connection); + const output = await tester.start('suiteId'); + // TODO: make this assertion more meaningful + expect(output).to.be.ok; + }); + }); + + describe('status', () => { + it('should return status of test run', async () => { + const tester = new AgentTester(connection); + await tester.start('suiteId'); + const output = await tester.status('4KBSM000000003F4AQ'); + expect(output).to.be.ok; + expect(output).to.deep.equal({ + status: 'IN_PROGRESS', + startTime: '2024-11-13T15:00:00.000Z', + }); + }); + }); + + describe('poll', () => { + it('should poll until test run is complete (human format)', async () => { + const tester = new AgentTester(connection); + await tester.start('suiteId'); + const output = await tester.poll('4KBSM000000003F4AQ'); + expect(output).to.be.ok; + // TODO: make these assertions more meaningful + expect(output.formatted).to.include('Test Results for my first test'); + expect(output.response.tests[0].results[0].results[0].is_pass).to.be.true; + }); + + it('should poll until test run is complete (json format)', async () => { + const tester = new AgentTester(connection); + await tester.start('suiteId'); + const output = await tester.poll('4KBSM000000003F4AQ', { format: 'json' }); + expect(output).to.be.ok; + // TODO: make these assertions more meaningful + expect(JSON.parse(output.formatted)).to.deep.equal(output.response); + expect(output.response.tests[0].results[0].results[0].is_pass).to.be.true; + }); + }); + + describe('details', () => { + it('should return details of completed test run', async () => { + const tester = new AgentTester(connection); + await tester.start('suiteId'); + const output = await tester.details('4KBSM000000003F4AQ'); + // TODO: make this assertion more meaningful + expect(output).to.be.ok; + }); + }); +}); diff --git a/test/agents.test.ts b/test/agents.test.ts index a451e6a..8885516 100644 --- a/test/agents.test.ts +++ b/test/agents.test.ts @@ -6,22 +6,31 @@ */ import { expect } from 'chai'; import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; -import { SfProject } from '@salesforce/core'; +import { Connection, SfProject } from '@salesforce/core'; import { Agent } from '../src/agent'; import { AgentJobSpecCreateConfig } from '../src/types'; describe('Agents', () => { const $$ = new TestContext(); - const testOrg = new MockTestOrgData(); - $$.inProject(true); + let testOrg: MockTestOrgData; + let connection: Connection; - process.env.SF_MOCK_DIR = 'test/mocks'; + beforeEach(async () => { + $$.inProject(true); + testOrg = new MockTestOrgData(); + process.env.SF_MOCK_DIR = 'test/mocks'; + connection = await testOrg.getConnection(); + connection.instanceUrl = 'https://mydomain.salesforce.com'; + // restore the connection sandbox so that it doesn't override the builtin mocking (MaybeMock) + $$.SANDBOXES.CONNECTION.restore(); + }); + + afterEach(() => { + delete process.env.SF_MOCK_DIR; + }); it('createSpec', async () => { - const connection = await testOrg.getConnection(); - connection.instanceUrl = 'https://mydomain.salesforce.com'; const sfProject = SfProject.getInstance(); - $$.SANDBOXES.CONNECTION.restore(); const agent = new Agent(connection, sfProject); const output = await agent.createSpec({ name: 'MyFirstAgent', @@ -36,10 +45,7 @@ describe('Agents', () => { }); it('create', async () => { - const connection = await testOrg.getConnection(); - connection.instanceUrl = 'https://mydomain.salesforce.com'; const sfProject = SfProject.getInstance(); - $$.SANDBOXES.CONNECTION.restore(); const agent = new Agent(connection, sfProject); const opts: AgentJobSpecCreateConfig = { name: 'MyFirstAgent', @@ -54,6 +60,7 @@ describe('Agents', () => { ...opts, jobSpecs, }); + // TODO: make this assertion more meaningful expect(output).to.be.ok; }); }); diff --git a/test/mocks/einstein_ai-evaluations_runs.json b/test/mocks/einstein_ai-evaluations_runs.json new file mode 100644 index 0000000..3c37def --- /dev/null +++ b/test/mocks/einstein_ai-evaluations_runs.json @@ -0,0 +1,3 @@ +{ + "id": "4KBSM000000003F4AQ" +} diff --git a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/1.json b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/1.json new file mode 100644 index 0000000..daf2bbc --- /dev/null +++ b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/1.json @@ -0,0 +1,4 @@ +{ + "status": "IN_PROGRESS", + "startTime": "2024-11-13T15:00:00.000Z" +} diff --git a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/2.json b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/2.json new file mode 100644 index 0000000..daf2bbc --- /dev/null +++ b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/2.json @@ -0,0 +1,4 @@ +{ + "status": "IN_PROGRESS", + "startTime": "2024-11-13T15:00:00.000Z" +} diff --git a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/3.json b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/3.json new file mode 100644 index 0000000..d4f6503 --- /dev/null +++ b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ/3.json @@ -0,0 +1,4 @@ +{ + "status": "COMPLETED", + "startTime": "2024-11-13T15:00:00.000Z" +} diff --git a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json new file mode 100644 index 0000000..d0a8a06 --- /dev/null +++ b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json @@ -0,0 +1,41 @@ +{ + "AiEvaluationSuiteDefinition": "", + "tests": [ + { + "AiEvaluationDefinition": "my first test", + "results": [ + { + "test_number": 1, + "results": [ + { + "name": "action_assertion", + "actual": ["Identify Record by Name", "Get Record Details"], + "is_pass": true, + "execution_time_ms": 3000, + "error": "" + }, + { + "name": "action_assertion", + "actual": ["Identify Record by Name", "Get Record Details"], + "is_pass": false, + "execution_time_ms": 3000, + "error": "assertion failed" + } + ] + }, + { + "test_number": 2, + "results": [ + { + "name": "action_assertion", + "actual": ["Identify Record by Name", "Get Record Details"], + "is_pass": true, + "execution_time_ms": 3000, + "error": "" + } + ] + } + ] + } + ] +} From 3fc0db28b9a5c06730a656e44537af89da465c4d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 14 Nov 2024 09:54:44 -0700 Subject: [PATCH 07/15] chore: clean up --- test/agentTester.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/agentTester.test.ts b/test/agentTester.test.ts index 042ff14..15064b5 100644 --- a/test/agentTester.test.ts +++ b/test/agentTester.test.ts @@ -11,7 +11,6 @@ import { AgentTester } from '../src/agentTester'; describe('AgentTester', () => { const $$ = new TestContext(); - let testOrg: MockTestOrgData; let connection: Connection; From 695fd086865c60850d53aa2753686ea5aeef2d4a Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 14 Nov 2024 13:22:42 -0700 Subject: [PATCH 08/15] fix: add polling lifecycle events --- src/agentTester.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/agentTester.ts b/src/agentTester.ts index 1119bab..bcce2bd 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -4,7 +4,7 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { Connection, PollingClient, StatusResult } from '@salesforce/core'; +import { Connection, Lifecycle, PollingClient, StatusResult } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; import { MaybeMock } from './maybe-mock'; @@ -71,13 +71,16 @@ export class AgentTester { timeout: Duration.minutes(5), } ): Promise<{ response: AgentTestDetailsResponse; formatted: string }> { + const lifecycle = Lifecycle.getInstance(); const client = await PollingClient.create({ poll: async (): Promise => { const { status } = await this.status(jobId); if (status === 'COMPLETED') { + await lifecycle.emit('AGENT_TEST_POLLING_EVENT', { jobId, status }); return { payload: await this.details(jobId, format), completed: true }; } + await lifecycle.emit('AGENT_TEST_POLLING_EVENT', { jobId, status }); return { completed: false }; }, frequency: Duration.seconds(1), From 8371f9fd735bfd7e12dd4a95419e321bb34cf465 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 20 Nov 2024 13:57:29 -0700 Subject: [PATCH 09/15] feat: add cancel method --- src/agentTester.ts | 8 +++++++- src/maybe-mock.ts | 2 +- test/agentTester.test.ts | 9 +++++++++ ...in_ai-evaluations_runs_4KBSM000000003F4AQ_cancel.json | 3 +++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_cancel.json diff --git a/src/agentTester.ts b/src/agentTester.ts index bcce2bd..c0e5963 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -110,6 +110,12 @@ export class AgentTester { : await jsonFormat(response), }; } + + public async cancel(jobId: string): Promise<{ success: boolean }> { + const url = `/einstein/ai-evaluations/runs/${jobId}/cancel`; + + return this.maybeMock.request<{ success: boolean }>('POST', url); + } } export async function humanFormat(details: AgentTestDetailsResponse): Promise { @@ -188,7 +194,7 @@ export async function junitFormat(details: AgentTestDetailsResponse): Promise { - // APEX EXAMPLE + // APEX EXAMPLE (these are streamed in chunks) // 1..11 // ok 1 TestPropertyController.testGetPagedPropertyList // ok 2 TestPropertyController.testGetPicturesNoResults diff --git a/src/maybe-mock.ts b/src/maybe-mock.ts index bc76375..6adc684 100644 --- a/src/maybe-mock.ts +++ b/src/maybe-mock.ts @@ -123,7 +123,7 @@ export class MaybeMock { public async request( method: 'GET' | 'POST', url: string, - body?: nock.RequestBodyMatcher + body: nock.RequestBodyMatcher = {} ): Promise { if (this.mockDir) { this.logger.debug(`Mocking ${method} request to ${url} using ${this.mockDir}`); diff --git a/test/agentTester.test.ts b/test/agentTester.test.ts index 15064b5..1281163 100644 --- a/test/agentTester.test.ts +++ b/test/agentTester.test.ts @@ -81,4 +81,13 @@ describe('AgentTester', () => { expect(output).to.be.ok; }); }); + + describe('cancel', () => { + it('should cancel test run', async () => { + const tester = new AgentTester(connection); + await tester.start('suiteId'); + const output = await tester.cancel('4KBSM000000003F4AQ'); + expect(output.success).to.be.true; + }); + }); }); diff --git a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_cancel.json b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_cancel.json new file mode 100644 index 0000000..5550c6d --- /dev/null +++ b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_cancel.json @@ -0,0 +1,3 @@ +{ + "success": true +} From 97eaa633fd739c214029ce0fb1dbd521f220c5ae Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 20 Nov 2024 15:29:17 -0700 Subject: [PATCH 10/15] fix: use sf-plugins-core for making table --- package.json | 1 + src/agentTester.ts | 6 +- yarn.lock | 187 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 5a27c73..a51b078 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@oclif/table": "^0.3.3", "@salesforce/core": "^8.8.0", "@salesforce/kit": "^3.2.3", + "@salesforce/sf-plugins-core": "^12.0.13", "nock": "^13.5.6" }, "devDependencies": { diff --git a/src/agentTester.ts b/src/agentTester.ts index c0e5963..8b8dc17 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -119,13 +119,13 @@ export class AgentTester { } export async function humanFormat(details: AgentTestDetailsResponse): Promise { - // TODO: these tables need to follow the same defaults that sf-plugins-core uses // TODO: the api response isn't finalized so this is just a POC - const { makeTable } = await import('@oclif/table'); + const { Ux } = await import('@salesforce/sf-plugins-core'); + const ux = new Ux(); const tables: string[] = []; for (const aiEvalDef of details.tests) { for (const result of aiEvalDef.results) { - const table = makeTable({ + const table = ux.makeTable({ title: `Test Results for ${aiEvalDef.AiEvaluationDefinition} (#${result.test_number})`, data: result.results.map((r) => ({ 'TEST NAME': r.name, diff --git a/yarn.lock b/yarn.lock index df3dbf5..ea0e7a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -403,6 +403,60 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz" integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== +"@inquirer/confirm@^3.1.22": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-3.2.0.tgz#6af1284670ea7c7d95e3f1253684cfbd7228ad6a" + integrity sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw== + dependencies: + "@inquirer/core" "^9.1.0" + "@inquirer/type" "^1.5.3" + +"@inquirer/core@^9.1.0": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-9.2.1.tgz#677c49dee399c9063f31e0c93f0f37bddc67add1" + integrity sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg== + dependencies: + "@inquirer/figures" "^1.0.6" + "@inquirer/type" "^2.0.0" + "@types/mute-stream" "^0.0.4" + "@types/node" "^22.5.5" + "@types/wrap-ansi" "^3.0.0" + ansi-escapes "^4.3.2" + cli-width "^4.1.0" + mute-stream "^1.0.0" + signal-exit "^4.1.0" + strip-ansi "^6.0.1" + wrap-ansi "^6.2.0" + yoctocolors-cjs "^2.1.2" + +"@inquirer/figures@^1.0.6": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.8.tgz#d9e414a1376a331a0e71b151fea27c48845788b0" + integrity sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg== + +"@inquirer/password@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-2.2.0.tgz#0b6f26336c259c8a9e5f5a3f2e1a761564f764ba" + integrity sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg== + dependencies: + "@inquirer/core" "^9.1.0" + "@inquirer/type" "^1.5.3" + ansi-escapes "^4.3.2" + +"@inquirer/type@^1.5.3": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-1.5.5.tgz#303ea04ce7ad2e585b921b662b3be36ef7b4f09b" + integrity sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA== + dependencies: + mute-stream "^1.0.0" + +"@inquirer/type@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-2.0.0.tgz#08fa513dca2cb6264fe1b0a2fabade051444e3f6" + integrity sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag== + dependencies: + mute-stream "^1.0.0" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -532,6 +586,46 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" +"@oclif/core@^4.0.32": + version "4.0.33" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-4.0.33.tgz#fcaf3dd2850c5999de20459a1445d31a230cd24b" + integrity sha512-NoTDwJ2L/ywpsSjcN7jAAHf3m70Px4Yim2SJrm16r70XpnfbNOdlj1x0HEJ0t95gfD+p/y5uy+qPT/VXTh/1gw== + dependencies: + ansi-escapes "^4.3.2" + ansis "^3.3.2" + clean-stack "^3.0.1" + cli-spinners "^2.9.2" + debug "^4.3.7" + ejs "^3.1.10" + get-package-type "^0.1.0" + globby "^11.1.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + lilconfig "^3.1.2" + minimatch "^9.0.5" + semver "^7.6.3" + string-width "^4.2.3" + supports-color "^8" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + +"@oclif/table@^0.3.2": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@oclif/table/-/table-0.3.5.tgz#118149eab364f3485eab5c9fd0d717c56082bacb" + integrity sha512-1IjoVz7WAdUdBW5vYIRc6wt9N7Ezwll6AtdmeqLQ8lUmB9gQJVyeb7dqXtUaUvIG7bZMvryfPe6Xibeo5FTCWA== + dependencies: + "@oclif/core" "^4" + "@types/react" "^18.3.12" + change-case "^5.4.4" + cli-truncate "^4.0.0" + ink "^5.0.1" + natural-orderby "^3.0.2" + object-hash "^3.0.0" + react "^18.3.1" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + "@oclif/table@^0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@oclif/table/-/table-0.3.3.tgz#5dc1c98cfa5415b131d77c85048df187fc241d12" @@ -569,7 +663,7 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/core@^8.6.4", "@salesforce/core@^8.8.0": +"@salesforce/core@^8.5.1", "@salesforce/core@^8.6.4", "@salesforce/core@^8.8.0": version "8.8.0" resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.8.0.tgz#849c07ea3a2548ca201fc0fe8baef9b36a462194" integrity sha512-HWGdRiy/MPCJ2KHz+W+cnqx0O9xhx9+QYvwP8bn9PE27wj0A/NjTi4xrqIWk1M+fE4dXHycE+8qPf4b540euvg== @@ -647,6 +741,22 @@ resolved "https://registry.npmjs.org/@salesforce/schemas/-/schemas-1.9.0.tgz" integrity sha512-LiN37zG5ODT6z70sL1fxF7BQwtCX9JOWofSU8iliSNIM+WDEeinnoFtVqPInRSNt8I0RiJxIKCrqstsmQRBNvA== +"@salesforce/sf-plugins-core@^12.0.13": + version "12.0.13" + resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-12.0.13.tgz#29bb68e8876dbd248fddb0ef6c9d340b748c87f4" + integrity sha512-ze13xyK8RisH2//1iXhG63lmtwzJCKVcy2WMjYs9WR1XRxuQe8vn1kyluYc8ZQEbl+HcNnVi5JuIDIvbCTHuFg== + dependencies: + "@inquirer/confirm" "^3.1.22" + "@inquirer/password" "^2.2.0" + "@oclif/core" "^4.0.32" + "@oclif/table" "^0.3.2" + "@salesforce/core" "^8.5.1" + "@salesforce/kit" "^3.2.3" + "@salesforce/ts-types" "^2.0.12" + ansis "^3.3.2" + cli-progress "^3.12.0" + terminal-link "^3.0.0" + "@salesforce/ts-types@^2.0.10", "@salesforce/ts-types@^2.0.11", "@salesforce/ts-types@^2.0.12": version "2.0.12" resolved "https://registry.npmjs.org/@salesforce/ts-types/-/ts-types-2.0.12.tgz" @@ -787,6 +897,13 @@ resolved "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz" integrity sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw== +"@types/mute-stream@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478" + integrity sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow== + dependencies: + "@types/node" "*" + "@types/node@*": version "22.5.5" resolved "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz" @@ -801,6 +918,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^22.5.5": + version "22.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.1.tgz#bdf91c36e0e7ecfb7257b2d75bf1b206b308ca71" + integrity sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg== + dependencies: + undici-types "~6.19.8" + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" @@ -849,6 +973,11 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz" integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== +"@types/wrap-ansi@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" + integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== + "@typescript-eslint/eslint-plugin@^6.21.0": version "6.21.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz" @@ -1024,6 +1153,13 @@ ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" +ansi-escapes@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-5.0.0.tgz#b6a0caf0eef0c41af190e9a749e0c00ec04bb2a6" + integrity sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA== + dependencies: + type-fest "^1.0.2" + ansi-escapes@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" @@ -1493,6 +1629,13 @@ cli-cursor@^4.0.0: dependencies: restore-cursor "^4.0.0" +cli-progress@^3.12.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942" + integrity sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A== + dependencies: + string-width "^4.2.3" + cli-spinners@^2.9.2: version "2.9.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" @@ -1506,6 +1649,11 @@ cli-truncate@^4.0.0: slice-ansi "^5.0.0" string-width "^7.0.0" +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + cliui@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" @@ -3823,6 +3971,11 @@ multistream@^3.1.0: inherits "^2.0.1" readable-stream "^3.4.0" +mute-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" + integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -4753,7 +4906,7 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -5022,7 +5175,7 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7, supports-color@^7.1.0: +supports-color@^7, supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -5036,11 +5189,27 @@ supports-color@^8, supports-color@^8.1.1: dependencies: has-flag "^4.0.0" +supports-hyperlinks@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +terminal-link@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-3.0.0.tgz#91c82a66b52fc1684123297ce384429faf72ac5c" + integrity sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg== + dependencies: + ansi-escapes "^5.0.0" + supports-hyperlinks "^2.2.0" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" @@ -5199,6 +5368,11 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.0.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + type-fest@^4.8.3: version "4.26.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.26.1.tgz#a4a17fa314f976dd3e6d6675ef6c775c16d7955e" @@ -5296,7 +5470,7 @@ undici-types@~5.26.4: resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~6.19.2: +undici-types@~6.19.2, undici-types@~6.19.8: version "6.19.8" resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== @@ -5647,6 +5821,11 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yoctocolors-cjs@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" + integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== + yoga-wasm-web@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba" From adb44b76e5e9236e00e5e412196883f05b77aef3 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 26 Nov 2024 09:51:17 -0700 Subject: [PATCH 11/15] chore: bump sf-plugins-core --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a51b078..7591935 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@oclif/table": "^0.3.3", "@salesforce/core": "^8.8.0", "@salesforce/kit": "^3.2.3", - "@salesforce/sf-plugins-core": "^12.0.13", + "@salesforce/sf-plugins-core": "^12.1.0", "nock": "^13.5.6" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index ea0e7a8..9202212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -741,10 +741,10 @@ resolved "https://registry.npmjs.org/@salesforce/schemas/-/schemas-1.9.0.tgz" integrity sha512-LiN37zG5ODT6z70sL1fxF7BQwtCX9JOWofSU8iliSNIM+WDEeinnoFtVqPInRSNt8I0RiJxIKCrqstsmQRBNvA== -"@salesforce/sf-plugins-core@^12.0.13": - version "12.0.13" - resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-12.0.13.tgz#29bb68e8876dbd248fddb0ef6c9d340b748c87f4" - integrity sha512-ze13xyK8RisH2//1iXhG63lmtwzJCKVcy2WMjYs9WR1XRxuQe8vn1kyluYc8ZQEbl+HcNnVi5JuIDIvbCTHuFg== +"@salesforce/sf-plugins-core@^12.1.0": + version "12.1.0" + resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-12.1.0.tgz#874531acb39755a634ceda5de6462c3b6256baf6" + integrity sha512-xJXF0WE+4lq2kb/w24wcZc+76EUCIKv7dj1oATugk9JFzYKySdC1smzCY/BhPGzMQGvXcbkWo5PG5iXDBrtwYQ== dependencies: "@inquirer/confirm" "^3.1.22" "@inquirer/password" "^2.2.0" From aaa5ad6b39e23e7aa29c73333154761767729f46 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 2 Dec 2024 10:36:56 -0700 Subject: [PATCH 12/15] refactor: comply with latest api spec --- src/agentTester.ts | 181 +++++++----------- test/agentTester.test.ts | 7 +- test/mocks/einstein_ai-evaluations_runs.json | 3 +- ...tions_runs_4KBSM000000003F4AQ_details.json | 103 +++++++--- 4 files changed, 145 insertions(+), 149 deletions(-) diff --git a/src/agentTester.ts b/src/agentTester.ts index 8b8dc17..5062ebb 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -8,47 +8,77 @@ import { Connection, Lifecycle, PollingClient, StatusResult } from '@salesforce/ import { Duration } from '@salesforce/kit'; import { MaybeMock } from './maybe-mock'; -type Format = 'human' | 'tap' | 'junit' | 'json'; +type Format = 'human' | 'json'; + +type TestStatus = 'NEW' | 'IN_PROGRESS' | 'COMPLETED' | 'ERROR'; type AgentTestStartResponse = { - id: string; + aiEvaluationId: string; + status: TestStatus; }; type AgentTestStatusResponse = { - status: 'NEW' | 'IN_PROGRESS' | 'COMPLETED' | 'ERROR'; + status: TestStatus; startTime: string; endTime?: string; errorMessage?: string; }; -type AgentTestDetailsResponse = { - AiEvaluationSuiteDefinition: string; - tests: Array<{ - AiEvaluationDefinition: string; - results: Array<{ - test_number: number; - results: Array<{ - name: string; - actual: string[]; - is_pass: boolean; - execution_time_ms: number; - error?: string; - }>; - }>; +type TestCaseResult = { + status: TestStatus; + number: string; + startTime: string; + endTime?: string; + generatedData: { + type: 'AGENT'; + actionsSequence: string[]; + outcome: 'Success' | 'Failure'; + topic: string; + inputTokensCount: string; + outputTokensCount: string; + }; + expectationResults: Array<{ + name: string; + actualValue: string; + expectedValue: string; + score: number; + result: 'Passed' | 'Failed'; + metricLabel: 'Accuracy' | 'Precision'; + metricExplainability: string; + status: TestStatus; + startTime: string; + endTime?: string; + errorCode?: string; + errorMessage?: string; }>; }; +type AgentTestDetailsResponse = { + status: TestStatus; + startTime: string; + endTime?: string; + errorMessage?: string; + testCases: TestCaseResult[]; +}; + export class AgentTester { private maybeMock: MaybeMock; public constructor(connection: Connection) { this.maybeMock = new MaybeMock(connection); } - public async start(suiteId: string): Promise<{ id: string }> { + /** + * Starts an AI evaluation run based on the provided name or ID. + * + * @param nameOrId - The name or ID of the AI evaluation definition. + * @param type - Specifies whether the provided identifier is a 'name' or 'id'. Defaults to 'name'. If 'name' is provided, nameOrId is treated as the name of the AiEvaluationDefinition. If 'id' is provided, nameOrId is treated as the unique ID of the AiEvaluationDefinition. + * @returns A promise that resolves to an object containing the ID of the started AI evaluation run. + */ + public async start(nameOrId: string, type: 'name' | 'id' = 'name'): Promise<{ aiEvaluationId: string }> { const url = '/einstein/ai-evaluations/runs'; return this.maybeMock.request('POST', url, { - aiEvaluationSuiteDefinition: suiteId, + [type === 'name' ? 'aiEvaluationDefinitionName' : 'aiEvaluationDefinitionVersionId']: nameOrId, }); } @@ -100,14 +130,7 @@ export class AgentTester { const response = await this.maybeMock.request('GET', url); return { response, - formatted: - format === 'human' - ? await humanFormat(response) - : format === 'tap' - ? await tapFormat(response) - : format === 'junit' - ? await junitFormat(response) - : await jsonFormat(response), + formatted: format === 'human' ? await humanFormat(jobId, response) : await jsonFormat(response), }; } @@ -118,100 +141,30 @@ export class AgentTester { } } -export async function humanFormat(details: AgentTestDetailsResponse): Promise { - // TODO: the api response isn't finalized so this is just a POC +export async function humanFormat(name: string, details: AgentTestDetailsResponse): Promise { const { Ux } = await import('@salesforce/sf-plugins-core'); const ux = new Ux(); + const tables: string[] = []; - for (const aiEvalDef of details.tests) { - for (const result of aiEvalDef.results) { - const table = ux.makeTable({ - title: `Test Results for ${aiEvalDef.AiEvaluationDefinition} (#${result.test_number})`, - data: result.results.map((r) => ({ - 'TEST NAME': r.name, - OUTCOME: r.is_pass ? 'Pass' : 'Fail', - MESSAGE: r.error ?? '', - 'RUNTIME (MS)': r.execution_time_ms, - })), - }); - tables.push(table); - } + for (const testCase of details.testCases) { + const table = ux.makeTable({ + title: `Test Case #${testCase.number}`, + data: testCase.expectationResults.map((r) => ({ + name: r.name, + outcome: r.result === 'Passed' ? 'Pass' : 'Fail', + actualValue: r.actualValue, + expectedValue: r.expectedValue, + score: r.score, + 'metric label': r.metricLabel, + message: r.errorMessage ?? '', + 'runtime (MS)': r.endTime ? new Date(r.endTime).getTime() - new Date(r.startTime).getTime() : 0, + })), + }); + tables.push(table); } - return tables.join('\n'); } -export async function junitFormat(details: AgentTestDetailsResponse): Promise { - // APEX EXAMPLE - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - // - await Promise.reject(new Error('Not implemented')); - return JSON.stringify(details, null, 2); -} - -export async function tapFormat(details: AgentTestDetailsResponse): Promise { - // APEX EXAMPLE (these are streamed in chunks) - // 1..11 - // ok 1 TestPropertyController.testGetPagedPropertyList - // ok 2 TestPropertyController.testGetPicturesNoResults - // ok 3 TestPropertyController.testGetPicturesWithResults - // ok 4 FileUtilitiesTest.createFileFailsWhenIncorrectBase64Data - // ok 5 FileUtilitiesTest.createFileFailsWhenIncorrectFilename - // ok 6 FileUtilitiesTest.createFileFailsWhenIncorrectRecordId - // ok 7 FileUtilitiesTest.createFileSucceedsWhenCorrectInput - // ok 8 TestSampleDataController.importSampleData - // ok 9 GeocodingServiceTest.blankAddress - // ok 10 GeocodingServiceTest.errorResponse - // ok 11 GeocodingServiceTest.successResponse - // # Run "sf apex get test -i 707Ei00000dUJry -o test-mgoe8ogsltwe@example.com --result-format " to retrieve test results in a different format. - await Promise.reject(new Error('Not implemented')); - return JSON.stringify(details, null, 2); -} - export async function jsonFormat(details: AgentTestDetailsResponse): Promise { return Promise.resolve(JSON.stringify(details, null, 2)); } diff --git a/test/agentTester.test.ts b/test/agentTester.test.ts index 1281163..7f55a73 100644 --- a/test/agentTester.test.ts +++ b/test/agentTester.test.ts @@ -57,8 +57,9 @@ describe('AgentTester', () => { const output = await tester.poll('4KBSM000000003F4AQ'); expect(output).to.be.ok; // TODO: make these assertions more meaningful - expect(output.formatted).to.include('Test Results for my first test'); - expect(output.response.tests[0].results[0].results[0].is_pass).to.be.true; + expect(output.formatted).to.include('Test Case #1'); + expect(output.formatted).to.include('Test Case #2'); + expect(output.response.testCases[0].status).to.equal('Completed'); }); it('should poll until test run is complete (json format)', async () => { @@ -68,7 +69,7 @@ describe('AgentTester', () => { expect(output).to.be.ok; // TODO: make these assertions more meaningful expect(JSON.parse(output.formatted)).to.deep.equal(output.response); - expect(output.response.tests[0].results[0].results[0].is_pass).to.be.true; + expect(output.response.testCases[0].status).to.equal('Completed'); }); }); diff --git a/test/mocks/einstein_ai-evaluations_runs.json b/test/mocks/einstein_ai-evaluations_runs.json index 3c37def..87f063c 100644 --- a/test/mocks/einstein_ai-evaluations_runs.json +++ b/test/mocks/einstein_ai-evaluations_runs.json @@ -1,3 +1,4 @@ { - "id": "4KBSM000000003F4AQ" + "aiEvaluationId": "4KBSM000000003F4AQ", + "status": "NEW" } diff --git a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json index d0a8a06..b7dd417 100644 --- a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json +++ b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json @@ -1,39 +1,80 @@ { - "AiEvaluationSuiteDefinition": "", - "tests": [ + "status": "Completed", + "startTime": "2024-11-28T12:00:00Z", + "endTime": "2024-11-28T12:05:00Z", + "errorMessage": null, + "testCases": [ { - "AiEvaluationDefinition": "my first test", - "results": [ + "status": "Completed", + "number": 1, + "startTime": "2024-11-28T12:00:10Z", + "endTime": "2024-11-28T12:00:20Z", + "generatedData": { + "type": "AGENT", + "actionsSequence": ["Action1", "Action2"], + "outcome": "Success", + "topic": "Mathematics", + "inputTokensCount": 50, + "outputTokensCount": 55 + }, + "expectationResults": [ { - "test_number": 1, - "results": [ - { - "name": "action_assertion", - "actual": ["Identify Record by Name", "Get Record Details"], - "is_pass": true, - "execution_time_ms": 3000, - "error": "" - }, - { - "name": "action_assertion", - "actual": ["Identify Record by Name", "Get Record Details"], - "is_pass": false, - "execution_time_ms": 3000, - "error": "assertion failed" - } - ] + "name": "topic_sequence_match", + "actualValue": "Result A", + "expectedValue": "Result A", + "score": 1.0, + "result": "Passed", + "metricLabel": "Accuracy", + "metricExplainability": "Measures the correctness of the result.", + "status": "Completed", + "startTime": "2024-11-28T12:00:12Z", + "endTime": "2024-11-28T12:00:13Z", + "errorCode": null, + "errorMessage": null }, { - "test_number": 2, - "results": [ - { - "name": "action_assertion", - "actual": ["Identify Record by Name", "Get Record Details"], - "is_pass": true, - "execution_time_ms": 3000, - "error": "" - } - ] + "name": "action_sequence_match", + "actualValue": "Result B", + "expectedValue": "Result B", + "score": 0.9, + "result": "Passed", + "metricLabel": "Precision", + "metricExplainability": "Measures the precision of the result.", + "status": "Completed", + "startTime": "2024-11-28T12:00:14Z", + "endTime": "2024-11-28T12:00:15Z", + "errorCode": null, + "errorMessage": null + } + ] + }, + { + "status": "Failed", + "number": 2, + "startTime": "2024-11-28T12:00:30Z", + "endTime": "2024-11-28T12:00:40Z", + "generatedData": { + "type": "AGENT", + "actionsSequence": ["Action3", "Action4"], + "outcome": "Failure", + "topic": "Physics", + "inputTokensCount": 60, + "outputTokensCount": 50 + }, + "expectationResults": [ + { + "name": "topic_sequence_match", + "actualValue": "Result C", + "expectedValue": "Result D", + "score": 0.5, + "result": "Failed", + "metricLabel": "Accuracy", + "metricExplainability": "Measures the correctness of the result.", + "status": "Completed", + "startTime": "2024-11-28T12:00:32Z", + "endTime": "2024-11-28T12:00:33Z", + "errorCode": null, + "errorMessage": null } ] } From 61b03dcba132ed07df194953c850595771f3ccff Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 2 Dec 2024 11:20:14 -0700 Subject: [PATCH 13/15] feat: poll both status and details --- src/agentTester.ts | 26 ++++++++++++++++--- ...tions_runs_4KBSM000000003F4AQ_details.json | 6 ++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/agentTester.ts b/src/agentTester.ts index 5062ebb..c1f1a94 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -104,13 +104,31 @@ export class AgentTester { const lifecycle = Lifecycle.getInstance(); const client = await PollingClient.create({ poll: async (): Promise => { - const { status } = await this.status(jobId); - if (status === 'COMPLETED') { - await lifecycle.emit('AGENT_TEST_POLLING_EVENT', { jobId, status }); + const [detailsResponse, statusResponse] = await Promise.all([this.details(jobId, format), this.status(jobId)]); + const totalTestCases = detailsResponse.response.testCases.length; + const failingTestCases = detailsResponse.response.testCases.filter((tc) => tc.status === 'ERROR').length; + const passingTestCases = detailsResponse.response.testCases.filter( + (tc) => tc.status === 'COMPLETED' && tc.expectationResults.every((r) => r.result === 'Passed') + ).length; + + if (statusResponse.status.toLowerCase() === 'completed') { + await lifecycle.emit('AGENT_TEST_POLLING_EVENT', { + jobId, + status: statusResponse.status, + totalTestCases, + failingTestCases, + passingTestCases, + }); return { payload: await this.details(jobId, format), completed: true }; } - await lifecycle.emit('AGENT_TEST_POLLING_EVENT', { jobId, status }); + await lifecycle.emit('AGENT_TEST_POLLING_EVENT', { + jobId, + status: statusResponse.status, + totalTestCases, + failingTestCases, + passingTestCases, + }); return { completed: false }; }, frequency: Duration.seconds(1), diff --git a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json index b7dd417..b895af0 100644 --- a/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json +++ b/test/mocks/einstein_ai-evaluations_runs_4KBSM000000003F4AQ_details.json @@ -1,11 +1,11 @@ { - "status": "Completed", + "status": "COMPLETED", "startTime": "2024-11-28T12:00:00Z", "endTime": "2024-11-28T12:05:00Z", "errorMessage": null, "testCases": [ { - "status": "Completed", + "status": "COMPLETED", "number": 1, "startTime": "2024-11-28T12:00:10Z", "endTime": "2024-11-28T12:00:20Z", @@ -49,7 +49,7 @@ ] }, { - "status": "Failed", + "status": "ERROR", "number": 2, "startTime": "2024-11-28T12:00:30Z", "endTime": "2024-11-28T12:00:40Z", From 0ca243ef01c350ec6942a7d241c020692cbab0a8 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 2 Dec 2024 11:21:10 -0700 Subject: [PATCH 14/15] test: update assertions --- test/agentTester.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/agentTester.test.ts b/test/agentTester.test.ts index 7f55a73..9b1526f 100644 --- a/test/agentTester.test.ts +++ b/test/agentTester.test.ts @@ -59,7 +59,7 @@ describe('AgentTester', () => { // TODO: make these assertions more meaningful expect(output.formatted).to.include('Test Case #1'); expect(output.formatted).to.include('Test Case #2'); - expect(output.response.testCases[0].status).to.equal('Completed'); + expect(output.response.testCases[0].status).to.equal('COMPLETED'); }); it('should poll until test run is complete (json format)', async () => { @@ -69,7 +69,7 @@ describe('AgentTester', () => { expect(output).to.be.ok; // TODO: make these assertions more meaningful expect(JSON.parse(output.formatted)).to.deep.equal(output.response); - expect(output.response.testCases[0].status).to.equal('Completed'); + expect(output.response.testCases[0].status).to.equal('COMPLETED'); }); }); From 8d346c982b9e90f135062a4e8911eab6decc7083 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 2 Dec 2024 12:47:09 -0700 Subject: [PATCH 15/15] chore: clean up --- src/agentTester.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agentTester.ts b/src/agentTester.ts index c1f1a94..20799f8 100644 --- a/src/agentTester.ts +++ b/src/agentTester.ts @@ -104,6 +104,8 @@ export class AgentTester { const lifecycle = Lifecycle.getInstance(); const client = await PollingClient.create({ poll: async (): Promise => { + // NOTE: we don't actually need to call the status API here since all the same information is present on the + // details API. We could just call the details API and check the status there. const [detailsResponse, statusResponse] = await Promise.all([this.details(jobId, format), this.status(jobId)]); const totalTestCases = detailsResponse.response.testCases.length; const failingTestCases = detailsResponse.response.testCases.filter((tc) => tc.status === 'ERROR').length;