From dacba93a77a70f10e528d5751513282493d783bc Mon Sep 17 00:00:00 2001 From: Esteban Romero Date: Tue, 10 Feb 2026 12:04:09 -0300 Subject: [PATCH 1/3] fix: add warinng message for AAB cascade delete --- messages/delete.source.md | 4 ++++ src/commands/project/delete/source.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/messages/delete.source.md b/messages/delete.source.md index e4d6222d..d38970a6 100644 --- a/messages/delete.source.md +++ b/messages/delete.source.md @@ -125,6 +125,10 @@ This operation will delete the following metadata in your org: This operation will deploy the following: %s +# cascadeDeleteWarning + +Deleting components of type "%s" may cause the org to also delete the following related metadata (cascade delete): %s. + # areYouSure Are you sure you want to proceed? diff --git a/src/commands/project/delete/source.ts b/src/commands/project/delete/source.ts index bcd81489..567bedb7 100644 --- a/src/commands/project/delete/source.ts +++ b/src/commands/project/delete/source.ts @@ -59,6 +59,27 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'delete.source'); const xorFlags = ['metadata', 'source-dir']; +/** + * Metadata types that trigger cascade deletion in the org. + */ +const CASCADE_DELETE_TYPES: Record = { + AiAuthoringBundle: ['Bot, BotVersion, GenAiPlannerBundle'], +}; + +/** + * Returns warning messages for components that trigger cascade deletion in the org. + */ +const getCascadeDeleteWarnings = (typesBeingDeleted: Set): string[] => { + const warnings: string[] = []; + for (const typeName of typesBeingDeleted) { + const cascadeTypes = CASCADE_DELETE_TYPES[typeName]; + if (cascadeTypes?.length) { + warnings.push(messages.getMessage('cascadeDeleteWarning', [typeName, cascadeTypes.join(', ')])); + } + } + return warnings; +}; + type MixedDeployDelete = { deploy: string[]; delete: FileResponseSuccess[] }; export class Source extends SfCommand { public static readonly summary = messages.getMessage('summary'); @@ -459,6 +480,9 @@ Update the .forceignore file and try again.`); ) .concat(this.mixedDeployDelete.delete.map((fr) => `${fr.fullName} (${fr.filePath})`)); + const typesBeingDeleted = new Set((this.components ?? []).map((comp) => comp.type.name)); + const cascadeWarnings = getCascadeDeleteWarnings(typesBeingDeleted); + const message: string[] = [ ...(this.mixedDeployDelete.deploy.length ? [messages.getMessage('deployPrompt', [[...new Set(this.mixedDeployDelete.deploy)].join('\n')])] @@ -470,6 +494,8 @@ Update the .forceignore file and try again.`); ...(local.length && (this.mixedDeployDelete.deploy.length || remote.length) ? ['\n'] : []), ...(local.length ? [messages.getMessage('localPrompt', [[...new Set(local)].join('\n')])] : []), + ...(cascadeWarnings.length ? [...cascadeWarnings] : []), + this.flags['check-only'] ?? false ? messages.getMessage('areYouSureCheckOnly') : messages.getMessage('areYouSure'), From 378c20601f0415747b78ea49ed94d2a92a54b4a4 Mon Sep 17 00:00:00 2001 From: Esteban Romero Date: Tue, 10 Feb 2026 14:55:13 -0300 Subject: [PATCH 2/3] Update messages/delete.source.md Co-authored-by: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> --- messages/delete.source.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/delete.source.md b/messages/delete.source.md index d38970a6..eb689bf0 100644 --- a/messages/delete.source.md +++ b/messages/delete.source.md @@ -127,7 +127,7 @@ This operation will deploy the following: # cascadeDeleteWarning -Deleting components of type "%s" may cause the org to also delete the following related metadata (cascade delete): %s. +When you delete components of type "%s", the org also deletes the following related metadata (known as a "cascade delete"): %s. # areYouSure From 8e58483b177f0e7d24d2872493b8d7a47ea54357 Mon Sep 17 00:00:00 2001 From: Esteban Romero Date: Tue, 10 Feb 2026 15:25:41 -0300 Subject: [PATCH 3/3] test: add UT --- test/commands/delete/source.test.ts | 38 ++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/test/commands/delete/source.test.ts b/test/commands/delete/source.test.ts index c78b981c..b78ca7d7 100644 --- a/test/commands/delete/source.test.ts +++ b/test/commands/delete/source.test.ts @@ -74,6 +74,13 @@ const agentComponents: SourceComponent[] = [ }), ]; +// Component with type that triggers cascade delete warning (AiAuthoringBundle) +const aiAuthoringBundleComponent = new SourceComponent({ + name: 'MyAiBundle', + type: registry.getTypeByName('AiAuthoringBundle'), + xml: '/dreamhouse-lwc/force-app/main/default/aiAuthoringBundles/MyAiBundle.agent-meta.xml', +}); + export const exampleDeleteResponse = { // required but ignored by the delete UT getFileResponses: (): void => {}, @@ -168,6 +175,7 @@ describe('project delete source', () => { options?: { sourceApiVersion?: string; inquirerMock?: { checkbox: sinon.SinonStub }; + captureConfirmMessage?: { ref: { message: string } }; } ) => { const cmd = new TestDelete(params, oclifConfigStub); @@ -192,7 +200,15 @@ describe('project delete source', () => { onCancel: () => {}, onError: () => {}, }); - handlePromptStub = stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm); + if (options?.captureConfirmMessage) { + const messageRef = options.captureConfirmMessage.ref; + stubMethod($$.SANDBOX, SfCommand.prototype, 'confirm').callsFake(async (opts: { message: string }) => { + messageRef.message = opts.message; + return true; + }); + } else { + handlePromptStub = stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm); + } if (options?.inquirerMock) { // @ts-expect-error stubbing private member of the command cmd.inquirer = options.inquirerMock; @@ -371,4 +387,24 @@ describe('project delete source', () => { }); ensureHookArgs(); }); + + it('should include cascade delete warning in prompt when deleting AiAuthoringBundle', async () => { + buildComponentSetStub.restore(); + buildComponentSetStub = stubMethod($$.SANDBOX, ComponentSetBuilder, 'build').resolves({ + toArray: () => [aiAuthoringBundleComponent], + forceIgnoredPaths: undefined, + apiVersion: '65.0', + sourceApiVersion: '65.0', + }); + + const captured = { message: '' }; + await runDeleteCmd(['--metadata', 'AiAuthoringBundle:MyAiBundle', '--json'], { + captureConfirmMessage: { ref: captured }, + }); + + expect(captured.message).to.include('AiAuthoringBundle'); + expect(captured.message).to.include('cascade'); + expect(captured.message).to.include('Bot, BotVersion, GenAiPlannerBundle'); + ensureHookArgs(); + }); });