From 1ff4938b41f8c7638002d44dd6f992cd26467a4f Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 29 Jan 2026 14:23:33 -0700 Subject: [PATCH] fix: allow adding AABs to destructive manifests without compiling --- src/collections/componentSet.ts | 6 +-- test/client/metadataApiDeploy.test.ts | 74 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index 5ae15c8061..89aab2c512 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -533,7 +533,7 @@ export class ComponentSet extends LazyCollection { } /** - * Get all AiAuthoringBundle components in the set. + * 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 @@ -571,8 +571,8 @@ export class ComponentSet extends LazyCollection { // we're working with SourceComponents now this.components.get(key)?.set(srcKey, component); - // track AiAuthoringBundles separately for fast access - if (component.type.id === 'aiauthoringbundle') { + // 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); } diff --git a/test/client/metadataApiDeploy.test.ts b/test/client/metadataApiDeploy.test.ts index 83ab316ab6..7e7b6cf2d4 100644 --- a/test/client/metadataApiDeploy.test.ts +++ b/test/client/metadataApiDeploy.test.ts @@ -25,6 +25,7 @@ import fs from 'graceful-fs'; import { ComponentSet, ComponentStatus, + DestructiveChangesType, DeployMessage, DeployResult, FileResponse, @@ -1515,6 +1516,79 @@ describe('MetadataApiDeploy', () => { 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( {