diff --git a/src/client/metadataApiDeploy.ts b/src/client/metadataApiDeploy.ts index 9c2eb20a3b..80b1805213 100644 --- a/src/client/metadataApiDeploy.ts +++ b/src/client/metadataApiDeploy.ts @@ -15,7 +15,6 @@ */ import { join, relative, resolve as pathResolve, sep } from 'node:path'; import { format } from 'node:util'; -import { EOL } from 'node:os'; import { isString } from '@salesforce/ts-types'; import JSZip from 'jszip'; import fs from 'graceful-fs'; @@ -23,9 +22,7 @@ import { Lifecycle } from '@salesforce/core/lifecycle'; import { Messages } from '@salesforce/core/messages'; import { SfError } from '@salesforce/core/sfError'; import { envVars } from '@salesforce/core/envVars'; -import { Connection } from '@salesforce/core'; -import { ensureArray, env } from '@salesforce/kit'; -import { SourceComponent } from '../resolve/sourceComponent'; +import { ensureArray } from '@salesforce/kit'; import { RegistryAccess } from '../registry'; import { ReplacementEvent } from '../convert/types'; import { MetadataConverter } from '../convert'; @@ -207,15 +204,6 @@ export class MetadataApiDeploy extends MetadataTransfer< // this is used as the version in the manifest (package.xml). this.components.sourceApiVersion ??= apiVersion; } - if (this.options.components) { - // we must ensure AiAuthoringBundles compile before deployment - // Use optimized getter method instead of filtering all components - const aabComponents = this.options.components.getAiAuthoringBundles().toArray(); - - if (aabComponents.length > 0 && env.getBoolean('SF_AAB_COMPILATION', true)) { - await compileAABComponents(connection, aabComponents); - } - } // only do event hooks if source, (NOT a metadata format) deploy if (this.options.components) { await LifecycleInstance.emit('scopedPreDeploy', { @@ -436,123 +424,6 @@ export class MetadataApiDeploy extends MetadataTransfer< } } -const compileAABComponents = async (connection: Connection, aabComponents: SourceComponent[]): Promise => { - // we need to use a namedJWT connection for this request - const { accessToken, instanceUrl } = connection.getConnectionOptions(); - if (!instanceUrl) { - throw SfError.create({ - name: 'ApiAccessError', - message: 'Missing Instance URL for org connection', - }); - } - if (!accessToken) { - throw SfError.create({ - name: 'ApiAccessError', - message: 'Missing Access Token for org connection', - }); - } - const url = `${instanceUrl}/agentforce/bootstrap/nameduser`; - // For the namdeduser endpoint request to work we need to delete the access token - delete connection.accessToken; - const response = await connection.request<{ - access_token: string; - }>( - { - method: 'GET', - url, - headers: { - 'Content-Type': 'application/json', - Cookie: `sid=${accessToken}`, - }, - }, - { retry: { maxRetries: 3 } } - ); - connection.accessToken = response.access_token; - const results = await Promise.all( - aabComponents.map(async (aab) => { - // aab.content points to a directory, we need to find the .agent file and read it - if (!aab.content) { - throw new SfError( - messages.getMessage('error_expected_source_files', [aab.fullName, 'aiauthoringbundle']), - 'ExpectedSourceFilesError' - ); - } - - const contentPath = aab.tree.find('content', aab.name, aab.content); - - if (!contentPath) { - // if this didn't exist, they'll have deploy issues anyways, but we can check here for type reasons - throw new SfError(`No .agent file found in directory: ${aab.content}`, 'MissingAgentFileError'); - } - - const agentContent = await fs.promises.readFile(contentPath, 'utf-8'); - - let result: { - // minimal typings here, more is returned, just using what we need - status: 'failure' | 'success'; - errors: Array<{ - description: string; - lineStart: number; - colStart: number; - }>; - name: string; - }; - try { - // to avoid circular dependencies between libraries, just call the compile endpoint here - result = await connection.request({ - method: 'POST', - url: `https://${ - env.getBoolean('SF_TEST_API') ? 'test.' : '' - }api.salesforce.com/einstein/ai-agent/v1.1/authoring/scripts`, - headers: { - 'x-client-name': 'afdx', - 'content-type': 'application/json', - }, - body: JSON.stringify({ - assets: [ - { - type: 'AFScript', - name: 'AFScript', - content: agentContent, - }, - ], - afScriptVersion: '1.0.1', - }), - }); - result.name = aab.name; - return result; - } catch (e) { - const error = SfError.wrap(e); - if (error.name.includes('ERROR_HTTP_404')) { - error.message = 'HTTP 404 error encountered when compiling AiAuthoringBundles'; - error.actions = [ - "Ensure the org's agent functionality is working outside of deployments", - "Try the 'sf agent validate authoring-bundle' command", - ]; - } - throw error; - } finally { - // regardless of success or failure, we don't need the named user jwt access token anymore - delete connection.accessToken; - await connection.refreshAuth(); - } - }) - ); - - const errors = results - .filter((result) => result.status === 'failure') - .map((result) => - result.errors.map((r) => `${result.name}.agent: ${r.description} ${r.lineStart}:${r.colStart}`).join(EOL) - ); - - if (errors.length > 0) { - throw SfError.create({ - message: `${EOL}${errors.join(EOL)}`, - name: 'AgentCompilationError', - }); - } -}; - /** * If a component fails to delete because it doesn't exist in the org, you get a message like * key: 'ApexClass#destructiveChanges.xml' diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index 89aab2c512..90661b1396 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -107,9 +107,6 @@ export class ComponentSet extends LazyCollection { // used to store components meant for a "constructive" (not destructive) manifest private manifestComponents = new DecodeableMap>(); - // optimization: track AiAuthoringBundles separately for faster access during compilation check - private aiAuthoringBundles = new Set(); - private destructiveChangesType = DestructiveChangesType.POST; public constructor(components: Iterable = [], registry = new RegistryAccess()) { @@ -532,16 +529,6 @@ export class ComponentSet extends LazyCollection { return new LazyCollection(iter).filter((c) => c instanceof SourceComponent) as LazyCollection; } - /** - * Get all constructive AiAuthoringBundle components in the set, which require compilation before deploy. - * This is an optimized method that uses a cached Set of AAB components. - * - * @returns Collection of AiAuthoringBundle source components - */ - public getAiAuthoringBundles(): LazyCollection { - return new LazyCollection(this.aiAuthoringBundles); - } - public add(component: ComponentLike, deletionType?: DestructiveChangesType): void { const key = simpleKey(component); if (!this.components.has(key)) { @@ -571,11 +558,6 @@ export class ComponentSet extends LazyCollection { // we're working with SourceComponents now this.components.get(key)?.set(srcKey, component); - // track AiAuthoringBundles separately for fast access (exclude destructive changes - no need to compile something that will be deleted) - if (component.type.id === 'aiauthoringbundle' && !deletionType) { - this.aiAuthoringBundles.add(component); - } - // Build maps of destructive components and regular components as they are added // as an optimization when building manifests. if (deletionType) { diff --git a/test/client/metadataApiDeploy.test.ts b/test/client/metadataApiDeploy.test.ts index 7e7b6cf2d4..ad9f999c6c 100644 --- a/test/client/metadataApiDeploy.test.ts +++ b/test/client/metadataApiDeploy.test.ts @@ -17,15 +17,13 @@ import { basename, join, sep } from 'node:path'; import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; import chai, { assert, expect } from 'chai'; import { AnyJson, ensureString, getString } from '@salesforce/ts-types'; -import { Connection, envVars, Lifecycle, Messages, PollingClient, SfError, StatusResult } from '@salesforce/core'; +import { envVars, Lifecycle, Messages, PollingClient, StatusResult } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import * as sinon from 'sinon'; -import fs from 'graceful-fs'; import { ComponentSet, ComponentStatus, - DestructiveChangesType, DeployMessage, DeployResult, FileResponse, @@ -78,8 +76,6 @@ describe('MetadataApiDeploy', () => { components, }); - expect(components.getAiAuthoringBundles().toArray()).to.be.empty; - await operation.start(); expect(convertStub.calledWith(components, 'metadata', { type: 'zip' })).to.be.true; @@ -1360,493 +1356,4 @@ describe('MetadataApiDeploy', () => { expect(mdOpts.apiOptions).to.have.property('singlePackage', true); }); }); - - describe('AiAuthoringBundle compilation', () => { - const aabType = registry.types.aiauthoringbundle; - const aabName = 'TestAAB'; - const aabContentDir = join('path', 'to', 'aiAuthoringBundles', aabName); - const agentFileName = `${aabName}.agent`; - const agentContent = 'test agent script content'; - - const createAABComponent = (): SourceComponent => - SourceComponent.createVirtualComponent( - { - name: aabName, - type: aabType, - xml: join(aabContentDir, `${aabName}${META_XML_SUFFIX}`), - content: aabContentDir, - }, - [ - { - dirPath: join('path', 'to', 'aiAuthoringBundles'), - children: [aabName], - }, - { - dirPath: aabContentDir, - children: [agentFileName], - }, - ] - ); - const aabComponent = createAABComponent(); - const components = new ComponentSet([aabComponent]); - - it('should throw error with correct data when compilation fails', async () => { - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('65.0'); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile').resolves(agentContent); - const refreshAuthStub = $$.SANDBOX.stub(Connection.prototype, 'refreshAuth').resolves(); - - $$.SANDBOX.stub(connection, 'getConnectionOptions').returns({ - accessToken: 'test-access-token', - instanceUrl: 'https://test.salesforce.com', - }); - - const compileErrors = [ - { description: 'Syntax error on line 5', lineStart: 5, colStart: 10 }, - { description: 'Missing token', lineStart: 8, colStart: 15 }, - ]; - - // Configure connection.request stub (already created by TestContext) - let callCount = 0; - let compileUrl: string | undefined; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - callCount++; - if (request.url?.includes('agentforce/bootstrap/nameduser')) { - return Promise.resolve({ access_token: 'named-user-token' }); - } - if (request.url?.includes('einstein/ai-agent')) { - compileUrl = request.url; - return Promise.resolve({ status: 'failure' as const, errors: compileErrors }); - } - // For other requests, return empty object (deploy stub handles its own requests) - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - try { - await operation.start(); - expect.fail('Should have thrown AgentCompilationError'); - } catch (error: unknown) { - const err = error as { name?: string; message?: string }; - expect(err).to.have.property('name', 'AgentCompilationError'); - expect(err.message).to.include(`${aabName}.agent: Syntax error on line 5 5:10`); - expect(err.message).to.include(`${aabName}.agent: Missing token 8:15`); - } - - expect(readFileStub.calledOnce).to.be.true; - expect(callCount).to.be.at.least(2); - // Verify URL uses production endpoint when SF_TEST_API is not set - expect(compileUrl).to.equal('https://api.salesforce.com/einstein/ai-agent/v1.1/authoring/scripts'); - // Regardless of success or failure, compileAABComponents should refresh auth. - expect(refreshAuthStub.calledOnce).to.be.true; - }); - - it('should not throw error when compilation succeeds', async () => { - const aabComponent = createAABComponent(); - const components = new ComponentSet([aabComponent]); - - // Stub retrieveMaxApiVersion on prototype before getting connection - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const refreshAuthStub = $$.SANDBOX.stub(Connection.prototype, 'refreshAuth').resolves(); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile').resolves(agentContent); - - $$.SANDBOX.stub(connection, 'getConnectionOptions').returns({ - accessToken: 'test-access-token', - instanceUrl: 'https://test.salesforce.com', - }); - - // Configure connection.request stub (already created by TestContext) - let callCount = 0; - let compileUrl: string | undefined; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - callCount++; - if (request.url?.includes('agentforce/bootstrap/nameduser')) { - return Promise.resolve({ access_token: 'named-user-token' }); - } - if (request.url?.includes('einstein/ai-agent')) { - compileUrl = request.url; - return Promise.resolve({ status: 'success' as const, errors: [] }); - } - // For other requests, return empty object (deploy stub handles its own requests) - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - // Should not throw - await operation.start(); - - expect(readFileStub.calledOnce).to.be.true; - expect(callCount).to.be.at.least(2); - // Verify URL uses production endpoint when SF_TEST_API is not set - expect(compileUrl).to.equal('https://api.salesforce.com/einstein/ai-agent/v1.1/authoring/scripts'); - // Regardless of success or failure, compileAABComponents should refresh auth. - expect(refreshAuthStub.calledOnce).to.be.true; - }); - - it('should not compile when no AABs present in component set', async () => { - const components = new ComponentSet([COMPONENT]); - - // Stub retrieveMaxApiVersion on prototype before getting connection - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile'); - // Track calls to connection.request to verify compilation wasn't attempted - const compileCallCount = { count: 0 }; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - const url = request.url ?? ''; - if (url.includes('einstein/ai-agent') || url.includes('agentforce/bootstrap')) { - compileCallCount.count++; - } - // For other requests, return empty object (deploy stub handles its own requests) - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - await operation.start(); - - // Verify compilation endpoints were not called - expect(readFileStub.called).to.be.false; - expect(compileCallCount.count).to.equal(0); - }); - - it('should skip AAB compilation when AAB is only in destructive changes', async () => { - // AAB marked for delete only (e.g. from destructiveChanges.xml) - no source to compile - const aabForDelete = new SourceComponent({ - name: 'toDelete', - type: aabType, - }); - const components = new ComponentSet([COMPONENT]); - components.add(aabForDelete, DestructiveChangesType.POST); - - expect(components.getAiAuthoringBundles().toArray()).to.be.empty; - - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile'); - const compileCallCount = { count: 0 }; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - const url = request.url ?? ''; - if (url.includes('einstein/ai-agent') || url.includes('agentforce/bootstrap')) { - compileCallCount.count++; - } - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - await operation.start(); - - expect(readFileStub.called).to.be.false; - expect(compileCallCount.count).to.equal(0); - }); - - it('should compile only constructive AABs when both constructive and destructive AABs exist', async () => { - const aabConstructive = createAABComponent(); - const aabForDelete = new SourceComponent({ - name: 'AABToDelete', - type: aabType, - }); - const components = new ComponentSet([aabConstructive]); - components.add(aabForDelete, DestructiveChangesType.POST); - - expect(components.getAiAuthoringBundles().toArray()).to.have.length(1); - expect(components.getAiAuthoringBundles().toArray()[0].fullName).to.equal(aabName); - - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile').resolves(agentContent); - $$.SANDBOX.stub(connection, 'getConnectionOptions').returns({ - accessToken: 'test-access-token', - instanceUrl: 'https://test.salesforce.com', - }); - - let compileCallCount = 0; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - if (request.url?.includes('agentforce/bootstrap/nameduser')) { - return Promise.resolve({ access_token: 'named-user-token' }); - } - if (request.url?.includes('einstein/ai-agent')) { - compileCallCount++; - return Promise.resolve({ status: 'success' as const, errors: [] }); - } - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - await operation.start(); - - expect(readFileStub.calledOnce).to.be.true; - expect(compileCallCount).to.equal(1); - }); - - it('should handle multiple AABs in parallel', async () => { - const aab1 = SourceComponent.createVirtualComponent( - { - name: 'AAB1', - type: aabType, - xml: join('path', 'to', 'aiAuthoringBundles', 'AAB1', `AAB1${META_XML_SUFFIX}`), - content: join('path', 'to', 'aiAuthoringBundles', 'AAB1'), - }, - [ - { - dirPath: join('path', 'to', 'aiAuthoringBundles'), - children: ['AAB1'], - }, - { - dirPath: join('path', 'to', 'aiAuthoringBundles', 'AAB1'), - children: ['AAB1.agent'], - }, - ] - ); - - const aab2 = SourceComponent.createVirtualComponent( - { - name: 'AAB2', - type: aabType, - xml: join('path', 'to', 'aiAuthoringBundles', 'AAB2', `AAB2${META_XML_SUFFIX}`), - content: join('path', 'to', 'aiAuthoringBundles', 'AAB2'), - }, - [ - { - dirPath: join('path', 'to', 'aiAuthoringBundles'), - children: ['AAB2'], - }, - { - dirPath: join('path', 'to', 'aiAuthoringBundles', 'AAB2'), - children: ['AAB2.agent'], - }, - ] - ); - - const components = new ComponentSet([aab1, aab2]); - - // Stub retrieveMaxApiVersion on prototype before getting connection - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile').resolves(agentContent); - - $$.SANDBOX.stub(connection, 'getConnectionOptions').returns({ - accessToken: 'test-access-token', - instanceUrl: 'https://test.salesforce.com', - }); - - // Configure connection.request stub (already created by TestContext) - // Handle multiple AABs: 2 nameduser + 2 compile calls - let namedUserCallCount = 0; - let compileCallCount = 0; - const compileUrls: string[] = []; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - if (request.url?.includes('agentforce/bootstrap/nameduser')) { - namedUserCallCount++; - return Promise.resolve({ access_token: 'named-user-token' }); - } - if (request.url?.includes('einstein/ai-agent')) { - compileCallCount++; - compileUrls.push(request.url); - return Promise.resolve({ status: 'success' as const, errors: [] }); - } - // For other requests, return empty object (deploy stub handles its own requests) - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - await operation.start(); - - // Should read both agent files - expect(readFileStub.callCount).to.equal(2); - // Should call compile endpoint twice (once per AAB) and nameduser once - expect(namedUserCallCount).to.equal(1); - expect(compileCallCount).to.equal(2); - // Verify both URLs use production endpoint when SF_TEST_API is not set - compileUrls.forEach((url) => { - expect(url).to.equal('https://api.salesforce.com/einstein/ai-agent/v1.1/authoring/scripts'); - }); - }); - it('should gracefully handle 404 errors', async () => { - const aab1 = SourceComponent.createVirtualComponent( - { - name: 'AAB1', - type: aabType, - xml: join('path', 'to', 'aiAuthoringBundles', 'AAB1', `AAB1${META_XML_SUFFIX}`), - content: join('path', 'to', 'aiAuthoringBundles', 'AAB1'), - }, - [ - { - dirPath: join('path', 'to', 'aiAuthoringBundles'), - children: ['AAB1'], - }, - { - dirPath: join('path', 'to', 'aiAuthoringBundles', 'AAB1'), - children: ['AAB1.agent'], - }, - ] - ); - - const components = new ComponentSet([aab1]); - - // Stub retrieveMaxApiVersion on prototype before getting connection - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const refreshAuthStub = $$.SANDBOX.stub(Connection.prototype, 'refreshAuth').resolves(); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile').resolves(agentContent); - - $$.SANDBOX.stub(connection, 'getConnectionOptions').returns({ - accessToken: 'test-access-token', - instanceUrl: 'https://test.salesforce.com', - }); - - // Configure connection.request stub (already created by TestContext) - // Handle multiple AABs: 2 nameduser + 2 compile calls - let namedUserCallCount = 0; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - if (request.url?.includes('agentforce/bootstrap/nameduser')) { - namedUserCallCount++; - return Promise.resolve({ access_token: 'named-user-token' }); - } - if (request.url?.includes('einstein/ai-agent')) { - const error = new Error('ERROR_HTTP_404'); - error.name = 'ERROR_HTTP_404'; - return Promise.reject(error); - } - // For other requests, return empty object (deploy stub handles its own requests) - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - try { - await operation.start(); - assert.fail('the above should throw a 404'); - } catch (e) { - expect(readFileStub.callCount).to.equal(1); - expect(namedUserCallCount).to.equal(1); - expect((e as Error).name).to.equal('ERROR_HTTP_404'); - expect((e as SfError).actions!.length).to.equal(2); - } - // Regardless of success or failure, compileAABComponents should refresh auth. - expect(refreshAuthStub.calledOnce).to.be.true; - }); - - it('should not compile when SF_AAB_COMPILATION=false', async () => { - process.env['SF_AAB_COMPILATION'] = 'false'; - const aab1 = SourceComponent.createVirtualComponent( - { - name: 'AAB1', - type: aabType, - xml: join('path', 'to', 'aiAuthoringBundles', 'AAB1', `AAB1${META_XML_SUFFIX}`), - content: join('path', 'to', 'aiAuthoringBundles', 'AAB1'), - }, - [ - { - dirPath: join('path', 'to', 'aiAuthoringBundles'), - children: ['AAB1'], - }, - { - dirPath: join('path', 'to', 'aiAuthoringBundles', 'AAB1'), - children: ['AAB1.agent'], - }, - ] - ); - - const components = new ComponentSet([aab1]); - - // Stub retrieveMaxApiVersion on prototype before getting connection - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile').resolves(agentContent); - - $$.SANDBOX.stub(connection, 'getConnectionOptions').returns({ - accessToken: 'test-access-token', - instanceUrl: 'https://test.salesforce.com', - }); - - // Configure connection.request stub (already created by TestContext) - // Handle multiple AABs: 2 nameduser + 2 compile calls - let namedUserCallCount = 0; - let compileCallCount = 0; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - if (request.url?.includes('agentforce/bootstrap/nameduser')) { - namedUserCallCount++; - return Promise.resolve({ access_token: 'named-user-token' }); - } - if (request.url?.includes('einstein/ai-agent')) { - compileCallCount++; - return Promise.resolve({ status: 'success' as const, errors: [] }); - } - // For other requests, return empty object (deploy stub handles its own requests) - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - await operation.start(); - - // Should not read any agent files, get any new auth info, or compile anything (SF_AAB_COMPILATION=false) - expect(readFileStub.callCount).to.equal(0); - expect(namedUserCallCount).to.equal(0); - expect(compileCallCount).to.equal(0); - delete process.env.SF_AAB_COMPILATION; - }); - - it('should use test API URL when SF_TEST_API is set to true', async () => { - const originalEnvValue = process.env.SF_TEST_API; - try { - process.env.SF_TEST_API = 'true'; - const aabComponent = createAABComponent(); - const components = new ComponentSet([aabComponent]); - - // Stub retrieveMaxApiVersion on prototype before getting connection - $$.SANDBOX.stub(Connection.prototype, 'retrieveMaxApiVersion').resolves('60.0'); - const connection = await testOrg.getConnection(); - - const readFileStub = $$.SANDBOX.stub(fs.promises, 'readFile').resolves(agentContent); - - $$.SANDBOX.stub(connection, 'getConnectionOptions').returns({ - accessToken: 'test-access-token', - instanceUrl: 'https://test.salesforce.com', - }); - - // Configure connection.request stub (already created by TestContext) - let compileUrl: string | undefined; - (connection.request as sinon.SinonStub).callsFake((request: { url?: string }) => { - if (request.url?.includes('agentforce/bootstrap/nameduser')) { - return Promise.resolve({ access_token: 'named-user-token' }); - } - if (request.url?.includes('einstein/ai-agent')) { - compileUrl = request.url; - return Promise.resolve({ status: 'success' as const, errors: [] }); - } - // For other requests, return empty object (deploy stub handles its own requests) - return Promise.resolve({}); - }); - - const { operation } = await stubMetadataDeploy($$, testOrg, { components }); - - await operation.start(); - - expect(readFileStub.calledOnce).to.be.true; - // Verify URL uses test endpoint when SF_TEST_API is set to true - expect(compileUrl).to.equal('https://test.api.salesforce.com/einstein/ai-agent/v1.1/authoring/scripts'); - } finally { - if (originalEnvValue === undefined) { - delete process.env.SF_TEST_API; - } else { - process.env.SF_TEST_API = originalEnvValue; - } - } - }); - }); });