From be288601f871649144e237e89eb37731a0c115c0 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 1 Apr 2026 08:57:36 -0600 Subject: [PATCH 1/6] feat: add new flags to open AAB --- command-snapshot.json | 12 +- messages/open.authoring-bundle.md | 32 ++- src/commands/org/open/authoring-bundle.ts | 26 +- test/unit/org/open/authoring-bundle.test.ts | 255 ++++++++++++++++++++ 4 files changed, 322 insertions(+), 3 deletions(-) create mode 100644 test/unit/org/open/authoring-bundle.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index 2b7ac1d5..cc5c1fcc 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -158,7 +158,17 @@ "command": "org:open:authoring-bundle", "flagAliases": ["urlonly"], "flagChars": ["b", "o", "r"], - "flags": ["api-version", "browser", "flags-dir", "json", "private", "target-org", "url-only"], + "flags": [ + "api-name", + "api-version", + "browser", + "flags-dir", + "json", + "private", + "target-org", + "url-only", + "version" + ], "plugin": "@salesforce/plugin-org" }, { diff --git a/messages/open.authoring-bundle.md b/messages/open.authoring-bundle.md index 2c1b13f5..b837842a 100644 --- a/messages/open.authoring-bundle.md +++ b/messages/open.authoring-bundle.md @@ -1,11 +1,13 @@ # summary -Open your org in Agentforce Studio, specifically in the list view showing the list of agents. +Open your org in Agentforce Studio, specifically in the list view showing the list of agents, or open a specific agent in Agentforce Builder. # description The list view shows the agents in your org that are implemented with Agent Script and an authoring bundle. Click on an agent name to open it in Agentforce Builder in a new browser window. +To open a specific agent directly in Agentforce Builder, provide the --api-name flag. Optionally include --version to open a specific version of the agent. + To generate the URL but not launch it in your browser, specify --url-only. # examples @@ -14,6 +16,14 @@ To generate the URL but not launch it in your browser, specify --url-only. $ <%= config.bin %> <%= command.id %> +- Open a specific agent directly in Agentforce Builder: + + $ <%= config.bin %> <%= command.id %> --api-name MyAgent + +- Open a specific version of an agent in Agentforce Builder: + + $ <%= config.bin %> <%= command.id %> --api-name MyAgent --version 1 + - Open the agents list view in an incognito window of your default browser: $ <%= config.bin %> <%= command.id %> --private @@ -22,6 +32,26 @@ To generate the URL but not launch it in your browser, specify --url-only. $ <%= config.bin %> <%= command.id %> --target-org MyTestOrg1 --browser firefox +- Open a specific agent in a different org and display the URL only: + + $ <%= config.bin %> <%= command.id %> --api-name MyAgent --version 2 --target-org MyTestOrg1 --url-only + +# flags.api-name.summary + +API name of the agent to open in Agentforce Builder. + +# flags.api-name.description + +The API name of the agent to open directly in Agentforce Builder. Optionally specify --version to open a specific version. + +# flags.version.summary + +Version number of the agent to open in Agentforce Builder. + +# flags.version.description + +The version number of the agent to open directly in Agentforce Builder. Can only be used with the --api-name flag. + # flags.private.summary Open the org in the default browser using private (incognito) mode. diff --git a/src/commands/org/open/authoring-bundle.ts b/src/commands/org/open/authoring-bundle.ts index 0139dc14..33095779 100644 --- a/src/commands/org/open/authoring-bundle.ts +++ b/src/commands/org/open/authoring-bundle.ts @@ -31,6 +31,15 @@ export class OrgOpenAuthoringBundle extends OrgOpenCommandBase { ...OrgOpenCommandBase.flags, 'target-org': Flags.requiredOrg(), 'api-version': Flags.orgApiVersion(), + 'api-name': Flags.string({ + summary: messages.getMessage('flags.api-name.summary'), + description: messages.getMessage('flags.api-name.description'), + }), + version: Flags.string({ + summary: messages.getMessage('flags.version.summary'), + description: messages.getMessage('flags.version.description'), + dependsOn: ['api-name'], + }), private: Flags.boolean({ summary: messages.getMessage('flags.private.summary'), exclusive: ['url-only', 'browser'], @@ -54,6 +63,21 @@ export class OrgOpenAuthoringBundle extends OrgOpenCommandBase { this.org = flags['target-org']; this.connection = this.org.getConnection(flags['api-version']); - return this.openOrgUI(flags, await this.org.getFrontDoorUrl('lightning/n/standard-AgentforceStudio')); + // Build the URL based on whether api-name is provided + let path: string; + if (flags['api-name']) { + const queryParams = new URLSearchParams({ + projectName: flags['api-name'], + }); + if (flags.version) { + queryParams.set('projectVersionNumber', flags.version); + } + path = `AgentAuthoring/agentAuthoringBuilder.app#/project?${queryParams.toString()}`; + } else { + // Default to the list view + path = 'lightning/n/standard-AgentforceStudio'; + } + + return this.openOrgUI(flags, await this.org.getFrontDoorUrl(path)); } } diff --git a/test/unit/org/open/authoring-bundle.test.ts b/test/unit/org/open/authoring-bundle.test.ts new file mode 100644 index 00000000..3853664e --- /dev/null +++ b/test/unit/org/open/authoring-bundle.test.ts @@ -0,0 +1,255 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventEmitter } from 'node:events'; +import { assert, expect } from 'chai'; +import { Connection, SfdcUrl } from '@salesforce/core'; +import { stubMethod } from '@salesforce/ts-sinon'; +import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; +import { stubSfCommandUx, stubSpinner, stubUx } from '@salesforce/sf-plugins-core'; +import { OrgOpenAuthoringBundle } from '../../../../src/commands/org/open/authoring-bundle.js'; +import { OrgOpenOutput } from '../../../../src/shared/orgTypes.js'; +import utils from '../../../../src/shared/orgOpenUtils.js'; + +describe('org:open:authoring-bundle', () => { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + + const singleUseToken = (Math.random() + 1).toString(36).substring(2); + const expectedDefaultSingleUseUrl = `${testOrg.instanceUrl}/secur/frontdoor.jsp?otp=${singleUseToken}`; + const getExpectedUrlWithPath = (path: string) => `${expectedDefaultSingleUseUrl}&startURL=${path}`; + + let sfCommandUxStubs: ReturnType; + + const testJsonStructure = (response: OrgOpenOutput) => { + expect(response).to.have.property('url'); + expect(response).to.have.property('username').equal(testOrg.username); + expect(response).to.have.property('orgId').equal(testOrg.orgId); + }; + + const spies = new Map(); + + beforeEach(async () => { + sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); + stubUx($$.SANDBOX); + stubSpinner($$.SANDBOX); + await $$.stubAuths(testOrg); + spies.set('open', stubMethod($$.SANDBOX, utils, 'openUrl').resolves(new EventEmitter())); + spies.set( + 'requestGet', + stubMethod($$.SANDBOX, Connection.prototype, 'requestGet').callsFake((url: string) => { + const urlObj = new URL(url); + const redirectUri = urlObj.searchParams.get('redirect_uri'); + return Promise.resolve({ + // eslint-disable-next-line camelcase + frontdoor_uri: redirectUri + ? `${expectedDefaultSingleUseUrl}&startURL=${redirectUri}` + : expectedDefaultSingleUseUrl, + }); + }) + ); + spies.set('resolver', stubMethod($$.SANDBOX, SfdcUrl.prototype, 'checkLightningDomain').resolves('1.1.1.1')); + }); + + afterEach(() => { + spies.clear(); + }); + + describe('url generation', () => { + it('opens default Agentforce Studio list view without flags', async () => { + const response = await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username, '--url-only']); + assert(response); + testJsonStructure(response); + expect(response.url).to.equal(getExpectedUrlWithPath('lightning/n/standard-AgentforceStudio')); + }); + + it('builds URL with api-name only', async () => { + const apiName = 'MyTestAgent'; + const response = await OrgOpenAuthoringBundle.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + apiName, + ]); + assert(response); + testJsonStructure(response); + const expectedPath = `AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=${apiName}`; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); + + it('builds URL with api-name and version', async () => { + const apiName = 'MyTestAgent'; + const version = '1'; + const response = await OrgOpenAuthoringBundle.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + apiName, + '--version', + version, + ]); + assert(response); + testJsonStructure(response); + const expectedPath = `AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=${apiName}&projectVersionNumber=${version}`; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); + + it('properly encodes special characters in api-name', async () => { + const apiName = 'My Test Agent'; + const response = await OrgOpenAuthoringBundle.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + apiName, + ]); + assert(response); + testJsonStructure(response); + const expectedPath = 'AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=My+Test+Agent'; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); + + it('properly encodes special characters in version', async () => { + const apiName = 'MyAgent'; + const version = '1.0-beta'; + const response = await OrgOpenAuthoringBundle.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + apiName, + '--version', + version, + ]); + assert(response); + testJsonStructure(response); + const expectedPath = + 'AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=MyAgent&projectVersionNumber=1.0-beta'; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); + + it('generates single-use URL when --url-only is not passed', async () => { + const response = await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username]); + assert(response); + testJsonStructure(response); + expect(spies.get('requestGet').callCount).to.equal(1); + expect(spies.get('open').callCount).to.equal(1); + expect(response.url).to.equal(getExpectedUrlWithPath('lightning/n/standard-AgentforceStudio')); + }); + }); + + describe('browser integration', () => { + it('opens in specified browser with api-name', async () => { + const apiName = 'MyAgent'; + await OrgOpenAuthoringBundle.run([ + '--json', + '--target-org', + testOrg.username, + '--api-name', + apiName, + '--browser', + 'firefox', + ]); + + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('open').args[0][1]).to.not.eql({}); + }); + + it('opens in private mode', async () => { + await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username, '--private']); + + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('open').args[0][1]).to.have.property('newInstance'); + }); + }); + + describe('flag validation', () => { + it('allows api-name without version', async () => { + const response = await OrgOpenAuthoringBundle.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + 'MyAgent', + ]); + assert(response); + testJsonStructure(response); + }); + + it('requires api-name when version is provided', async () => { + try { + await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username, '--url-only', '--version', '1']); + assert.fail('Should have thrown an error'); + } catch (error) { + // Expected to fail due to missing api-name dependency + expect(error).to.exist; + } + }); + }); + + describe('human output', () => { + it('outputs success message without URL when opening browser', async () => { + await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username]); + + expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); + expect(spies.get('open').callCount).to.equal(1); + }); + + it('outputs URL when using --url-only', async () => { + await OrgOpenAuthoringBundle.run(['--target-org', testOrg.username, '--url-only']); + + expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); + expect(spies.get('open').callCount).to.equal(0); + }); + + it('outputs URL with api-name when using --url-only', async () => { + await OrgOpenAuthoringBundle.run([ + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + 'MyAgent', + '--version', + '1', + ]); + + expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); + expect(spies.get('open').callCount).to.equal(0); + }); + }); + + describe('api-version flag', () => { + it('respects api-version flag', async () => { + const apiVersion = '59.0'; + const response = await OrgOpenAuthoringBundle.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-version', + apiVersion, + ]); + assert(response); + testJsonStructure(response); + }); + }); +}); From fecb66c37c03f9abf0cbf7fc3436086c5eab3f6c Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 7 Apr 2026 09:16:17 -0600 Subject: [PATCH 2/6] fix: deprecate 'org open authoring-bundle' expand 'org open agent' --- command-snapshot.json | 13 +- messages/open.agent.md | 35 ++- src/commands/org/open/agent.ts | 29 +- src/commands/org/open/authoring-bundle.ts | 2 + test/unit/org/open/agent.test.ts | 354 ++++++++++++++++++++++ 5 files changed, 427 insertions(+), 6 deletions(-) create mode 100644 test/unit/org/open/agent.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index cc5c1fcc..e6cce429 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -150,7 +150,18 @@ "command": "org:open:agent", "flagAliases": ["urlonly"], "flagChars": ["b", "n", "o", "r"], - "flags": ["api-name", "api-version", "browser", "flags-dir", "json", "private", "target-org", "url-only"], + "flags": [ + "api-name", + "api-version", + "authoring-bundle", + "browser", + "flags-dir", + "json", + "private", + "target-org", + "url-only", + "version" + ], "plugin": "@salesforce/plugin-org" }, { diff --git a/messages/open.agent.md b/messages/open.agent.md index 78121b07..890671a8 100644 --- a/messages/open.agent.md +++ b/messages/open.agent.md @@ -4,11 +4,16 @@ Open an agent in your org's Agent Builder UI in a browser. # description -Use the --api-name flag to open an agent using its API name in the Agent Builder UI of your org. To find the agent's API name, go to Setup in your org and navigate to the agent's details page. +Use the --api-name flag to open an agent using its API name in the Agent Builder UI of your org. To find the agent's API +name, go to Setup in your org and navigate to the agent's details page. + +Alternatively, use the --authoring-bundle flag to open an agent in Agentforce Builder. Optionally include --version to +open a specific version of the agent. You'll specify the api name of the authoring bundle. To generate the URL but not launch it in your browser, specify --url-only. -To open Agent Builder in a specific browser, use the --browser flag. Supported browsers are "chrome", "edge", and "firefox". If you don't specify --browser, the org opens in your default browser. +To open Agent Builder in a specific browser, use the --browser flag. Supported browsers are "chrome", "edge", and " +firefox". If you don't specify --browser, the org opens in your default browser. # examples @@ -24,6 +29,14 @@ To open Agent Builder in a specific browser, use the --browser flag. Supported b $ <%= config.bin %> <%= command.id %> --target-org MyTestOrg1 --browser firefox --api-name Coral_Cloud_Agent +- Open an agent in Agentforce Builder using its authoring bundle name: + + $ <%= config.bin %> <%= command.id %> --authoring-bundle MyAgent + +- Open a specific version of an agent in Agentforce Builder: + + $ <%= config.bin %> <%= command.id %> --authoring-bundle MyAgent --version 1 + # flags.api-name.summary API name, also known as developer name, of the agent you want to open in the org's Agent Builder UI. @@ -39,3 +52,21 @@ Browser where the org opens. # flags.url-only.summary Display navigation URL, but don’t launch browser. + +# flags.authoring-bundle.summary + +API name of the agent to open in Agentforce Builder. + +# flags.authoring-bundle.description + +The API name of the agent to open directly in Agentforce Builder. Optionally specify --version to open a specific +version. + +# flags.version.summary + +Version number of the agent to open in Agentforce Builder. + +# flags.version.description + +The version number of the agent to open directly in Agentforce Builder. Can only be used with the --authoring-bundle +flag. diff --git a/src/commands/org/open/agent.ts b/src/commands/org/open/agent.ts index 0dc797ad..6e66824c 100644 --- a/src/commands/org/open/agent.ts +++ b/src/commands/org/open/agent.ts @@ -34,7 +34,7 @@ export class OrgOpenAgent extends OrgOpenCommandBase { 'api-name': Flags.string({ char: 'n', summary: messages.getMessage('flags.api-name.summary'), - required: true, + exactlyOne: ['api-name', 'authoring-bundle'], }), private: Flags.boolean({ summary: messages.getMessage('flags.private.summary'), @@ -52,6 +52,16 @@ export class OrgOpenAgent extends OrgOpenCommandBase { aliases: ['urlonly'], deprecateAliases: true, }), + 'authoring-bundle': Flags.string({ + summary: messages.getMessage('flags.authoring-bundle.summary'), + description: messages.getMessage('flags.authoring-bundle.description'), + exactlyOne: ['api-name', 'authoring-bundle'], + }), + version: Flags.string({ + summary: messages.getMessage('flags.version.summary'), + description: messages.getMessage('flags.version.description'), + dependsOn: ['authoring-bundle'], + }), }; public async run(): Promise { @@ -59,9 +69,22 @@ export class OrgOpenAgent extends OrgOpenCommandBase { this.org = flags['target-org']; this.connection = this.org.getConnection(flags['api-version']); - const agentBuilderRedirect = await buildRetUrl(this.connection, flags['api-name']); + let path: string; + if (flags['api-name']) { + path = await buildRetUrl(this.connection, flags['api-name']); + } else { + // authoring-bundle is provided + const queryParams = new URLSearchParams({ + // flags.authoring-bundle guaranteed by OCLIF definition + projectName: flags['authoring-bundle']!, + }); + if (flags.version) { + queryParams.set('projectVersionNumber', flags.version); + } + path = `AgentAuthoring/agentAuthoringBuilder.app#/project?${queryParams.toString()}`; + } - return this.openOrgUI(flags, await this.org.getFrontDoorUrl(agentBuilderRedirect)); + return this.openOrgUI(flags, await this.org.getFrontDoorUrl(path)); } } diff --git a/src/commands/org/open/authoring-bundle.ts b/src/commands/org/open/authoring-bundle.ts index 33095779..cd86f91b 100644 --- a/src/commands/org/open/authoring-bundle.ts +++ b/src/commands/org/open/authoring-bundle.ts @@ -26,6 +26,8 @@ export class OrgOpenAuthoringBundle extends OrgOpenCommandBase { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); + public static readonly state = 'deprecated'; + public static readonly deprecationOptions = { to: 'org open agent --authoring-bundle' }; public static readonly flags = { ...OrgOpenCommandBase.flags, diff --git a/test/unit/org/open/agent.test.ts b/test/unit/org/open/agent.test.ts new file mode 100644 index 00000000..b85c334b --- /dev/null +++ b/test/unit/org/open/agent.test.ts @@ -0,0 +1,354 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventEmitter } from 'node:events'; +import { assert, expect } from 'chai'; +import { Connection, SfdcUrl } from '@salesforce/core'; +import { stubMethod } from '@salesforce/ts-sinon'; +import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; +import { stubSfCommandUx, stubSpinner, stubUx } from '@salesforce/sf-plugins-core'; +import { OrgOpenAgent } from '../../../../src/commands/org/open/agent.js'; +import { OrgOpenOutput } from '../../../../src/shared/orgTypes.js'; +import utils from '../../../../src/shared/orgOpenUtils.js'; + +describe('org:open:agent', () => { + const $$ = new TestContext(); + const testOrg = new MockTestOrgData(); + + const singleUseToken = (Math.random() + 1).toString(36).substring(2); + const expectedDefaultSingleUseUrl = `${testOrg.instanceUrl}/secur/frontdoor.jsp?otp=${singleUseToken}`; + const getExpectedUrlWithPath = (path: string) => `${expectedDefaultSingleUseUrl}&startURL=${path}`; + + const mockBotId = '0Xx1234567890ABCD'; + const mockBotName = 'TestAgent'; + + let sfCommandUxStubs: ReturnType; + + const testJsonStructure = (response: OrgOpenOutput) => { + expect(response).to.have.property('url'); + expect(response).to.have.property('username').equal(testOrg.username); + expect(response).to.have.property('orgId').equal(testOrg.orgId); + }; + + const spies = new Map(); + + beforeEach(async () => { + sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); + stubUx($$.SANDBOX); + stubSpinner($$.SANDBOX); + await $$.stubAuths(testOrg); + spies.set('open', stubMethod($$.SANDBOX, utils, 'openUrl').resolves(new EventEmitter())); + spies.set( + 'requestGet', + stubMethod($$.SANDBOX, Connection.prototype, 'requestGet').callsFake((url: string) => { + const urlObj = new URL(url); + const redirectUri = urlObj.searchParams.get('redirect_uri'); + return Promise.resolve({ + // eslint-disable-next-line camelcase + frontdoor_uri: redirectUri + ? `${expectedDefaultSingleUseUrl}&startURL=${redirectUri}` + : expectedDefaultSingleUseUrl, + }); + }) + ); + spies.set('resolver', stubMethod($$.SANDBOX, SfdcUrl.prototype, 'checkLightningDomain').resolves('1.1.1.1')); + spies.set( + 'singleRecordQuery', + stubMethod($$.SANDBOX, Connection.prototype, 'singleRecordQuery').resolves({ Id: mockBotId }) + ); + }); + + afterEach(() => { + spies.clear(); + }); + + describe('flag validation', () => { + it('requires either api-name or authoring-bundle', async () => { + try { + await OrgOpenAgent.run(['--json', '--target-org', testOrg.username, '--url-only']); + assert.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect((error as Error).message).to.include('Exactly one of the following must be provided'); + expect((error as Error).message).to.include('--api-name'); + expect((error as Error).message).to.include('--authoring-bundle'); + } + }); + + it('does not allow both api-name and authoring-bundle', async () => { + try { + await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + mockBotName, + '--authoring-bundle', + 'MyAgent', + ]); + assert.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect((error as Error).message).to.include('--api-name'); + expect((error as Error).message).to.include('--authoring-bundle'); + expect((error as Error).message).to.match(/exactly one|cannot also be provided/i); + } + }); + + it('requires authoring-bundle when version is provided', async () => { + try { + await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + mockBotName, + '--version', + '1', + ]); + assert.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect((error as Error).message).to.include('--version'); + expect((error as Error).message).to.include('--authoring-bundle'); + expect((error as Error).message).to.match(/All of the following must be provided|depends on/i); + } + }); + }); + + describe('url generation with api-name', () => { + it('builds URL with api-name using BotDefinition query', async () => { + const response = await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + mockBotName, + ]); + assert(response); + testJsonStructure(response); + + // Verify the BotDefinition query was made + expect(spies.get('singleRecordQuery').callCount).to.equal(1); + expect(spies.get('singleRecordQuery').firstCall.args[0]).to.include(mockBotName); + expect(spies.get('singleRecordQuery').firstCall.args[0]).to.include('BotDefinition'); + + const expectedPath = `AiCopilot/copilotStudio.app#/copilot/builder?copilotId=${mockBotId}`; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); + + it('generates single-use URL when --url-only is not passed', async () => { + const response = await OrgOpenAgent.run(['--json', '--target-org', testOrg.username, '--api-name', mockBotName]); + assert(response); + testJsonStructure(response); + expect(spies.get('requestGet').callCount).to.equal(1); + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('singleRecordQuery').callCount).to.equal(1); + }); + + it('properly queries BotDefinition with special characters in api-name', async () => { + const specialName = 'Test_Agent_01'; + await OrgOpenAgent.run(['--json', '--target-org', testOrg.username, '--url-only', '--api-name', specialName]); + + expect(spies.get('singleRecordQuery').callCount).to.equal(1); + expect(spies.get('singleRecordQuery').firstCall.args[0]).to.include(specialName); + }); + }); + + describe('url generation with authoring-bundle', () => { + it('builds URL with authoring-bundle only', async () => { + const bundleName = 'MyTestAgent'; + const response = await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--authoring-bundle', + bundleName, + ]); + assert(response); + testJsonStructure(response); + + // Verify no BotDefinition query was made + expect(spies.get('singleRecordQuery').callCount).to.equal(0); + + const expectedPath = `AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=${bundleName}`; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); + + it('builds URL with authoring-bundle and version', async () => { + const bundleName = 'MyTestAgent'; + const version = '13'; + const response = await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--authoring-bundle', + bundleName, + '--version', + version, + ]); + assert(response); + testJsonStructure(response); + + const expectedPath = `AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=${bundleName}&projectVersionNumber=${version}`; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); + + it('generates single-use URL when --url-only is not passed', async () => { + const response = await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--authoring-bundle', + 'MyAgent', + ]); + assert(response); + testJsonStructure(response); + expect(spies.get('requestGet').callCount).to.equal(1); + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('singleRecordQuery').callCount).to.equal(0); + }); + }); + + describe('browser integration', () => { + it('opens in specified browser with api-name', async () => { + await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--api-name', + mockBotName, + '--browser', + 'firefox', + ]); + + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('open').args[0][1]).to.not.eql({}); + }); + + it('opens in specified browser with authoring-bundle', async () => { + await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--authoring-bundle', + 'MyAgent', + '--browser', + 'firefox', + ]); + + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('open').args[0][1]).to.not.eql({}); + }); + + it('opens in private mode with api-name', async () => { + await OrgOpenAgent.run(['--json', '--target-org', testOrg.username, '--api-name', mockBotName, '--private']); + + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('open').args[0][1]).to.have.property('newInstance'); + }); + + it('opens in private mode with authoring-bundle', async () => { + await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--authoring-bundle', + 'MyAgent', + '--private', + ]); + + expect(spies.get('open').callCount).to.equal(1); + expect(spies.get('open').args[0][1]).to.have.property('newInstance'); + }); + }); + + describe('human output', () => { + it('outputs success message without URL when opening browser with api-name', async () => { + await OrgOpenAgent.run(['--json', '--target-org', testOrg.username, '--api-name', mockBotName]); + + expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); + expect(spies.get('open').callCount).to.equal(1); + }); + + it('outputs success message without URL when opening browser with authoring-bundle', async () => { + await OrgOpenAgent.run(['--json', '--target-org', testOrg.username, '--authoring-bundle', 'MyAgent']); + + expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); + expect(spies.get('open').callCount).to.equal(1); + }); + + it('outputs URL when using --url-only with api-name', async () => { + await OrgOpenAgent.run(['--target-org', testOrg.username, '--url-only', '--api-name', mockBotName]); + + expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); + expect(spies.get('open').callCount).to.equal(0); + }); + + it('outputs URL when using --url-only with authoring-bundle', async () => { + await OrgOpenAgent.run([ + '--target-org', + testOrg.username, + '--url-only', + '--authoring-bundle', + 'MyAgent', + '--version', + '1', + ]); + + expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); + expect(spies.get('open').callCount).to.equal(0); + }); + }); + + describe('api-version flag', () => { + it('respects api-version flag with api-name', async () => { + const apiVersion = '59.0'; + const response = await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-version', + apiVersion, + '--api-name', + mockBotName, + ]); + assert(response); + testJsonStructure(response); + }); + + it('respects api-version flag with authoring-bundle', async () => { + const apiVersion = '59.0'; + const response = await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-version', + apiVersion, + '--authoring-bundle', + 'MyAgent', + ]); + assert(response); + testJsonStructure(response); + }); + }); +}); From 6550d33709558bce379f719f42024868edb60694 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 7 Apr 2026 09:19:28 -0600 Subject: [PATCH 3/6] chore: undo unneeded changes to open AAB --- src/commands/org/open/authoring-bundle.ts | 26 +- test/unit/org/open/authoring-bundle.test.ts | 255 -------------------- 2 files changed, 1 insertion(+), 280 deletions(-) delete mode 100644 test/unit/org/open/authoring-bundle.test.ts diff --git a/src/commands/org/open/authoring-bundle.ts b/src/commands/org/open/authoring-bundle.ts index cd86f91b..39296dba 100644 --- a/src/commands/org/open/authoring-bundle.ts +++ b/src/commands/org/open/authoring-bundle.ts @@ -33,15 +33,6 @@ export class OrgOpenAuthoringBundle extends OrgOpenCommandBase { ...OrgOpenCommandBase.flags, 'target-org': Flags.requiredOrg(), 'api-version': Flags.orgApiVersion(), - 'api-name': Flags.string({ - summary: messages.getMessage('flags.api-name.summary'), - description: messages.getMessage('flags.api-name.description'), - }), - version: Flags.string({ - summary: messages.getMessage('flags.version.summary'), - description: messages.getMessage('flags.version.description'), - dependsOn: ['api-name'], - }), private: Flags.boolean({ summary: messages.getMessage('flags.private.summary'), exclusive: ['url-only', 'browser'], @@ -65,21 +56,6 @@ export class OrgOpenAuthoringBundle extends OrgOpenCommandBase { this.org = flags['target-org']; this.connection = this.org.getConnection(flags['api-version']); - // Build the URL based on whether api-name is provided - let path: string; - if (flags['api-name']) { - const queryParams = new URLSearchParams({ - projectName: flags['api-name'], - }); - if (flags.version) { - queryParams.set('projectVersionNumber', flags.version); - } - path = `AgentAuthoring/agentAuthoringBuilder.app#/project?${queryParams.toString()}`; - } else { - // Default to the list view - path = 'lightning/n/standard-AgentforceStudio'; - } - - return this.openOrgUI(flags, await this.org.getFrontDoorUrl(path)); + return this.openOrgUI(flags, await this.org.getFrontDoorUrl('lightning/n/standard-AgentforceStudio')); } } diff --git a/test/unit/org/open/authoring-bundle.test.ts b/test/unit/org/open/authoring-bundle.test.ts deleted file mode 100644 index 3853664e..00000000 --- a/test/unit/org/open/authoring-bundle.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2026, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { EventEmitter } from 'node:events'; -import { assert, expect } from 'chai'; -import { Connection, SfdcUrl } from '@salesforce/core'; -import { stubMethod } from '@salesforce/ts-sinon'; -import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; -import { stubSfCommandUx, stubSpinner, stubUx } from '@salesforce/sf-plugins-core'; -import { OrgOpenAuthoringBundle } from '../../../../src/commands/org/open/authoring-bundle.js'; -import { OrgOpenOutput } from '../../../../src/shared/orgTypes.js'; -import utils from '../../../../src/shared/orgOpenUtils.js'; - -describe('org:open:authoring-bundle', () => { - const $$ = new TestContext(); - const testOrg = new MockTestOrgData(); - - const singleUseToken = (Math.random() + 1).toString(36).substring(2); - const expectedDefaultSingleUseUrl = `${testOrg.instanceUrl}/secur/frontdoor.jsp?otp=${singleUseToken}`; - const getExpectedUrlWithPath = (path: string) => `${expectedDefaultSingleUseUrl}&startURL=${path}`; - - let sfCommandUxStubs: ReturnType; - - const testJsonStructure = (response: OrgOpenOutput) => { - expect(response).to.have.property('url'); - expect(response).to.have.property('username').equal(testOrg.username); - expect(response).to.have.property('orgId').equal(testOrg.orgId); - }; - - const spies = new Map(); - - beforeEach(async () => { - sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); - stubUx($$.SANDBOX); - stubSpinner($$.SANDBOX); - await $$.stubAuths(testOrg); - spies.set('open', stubMethod($$.SANDBOX, utils, 'openUrl').resolves(new EventEmitter())); - spies.set( - 'requestGet', - stubMethod($$.SANDBOX, Connection.prototype, 'requestGet').callsFake((url: string) => { - const urlObj = new URL(url); - const redirectUri = urlObj.searchParams.get('redirect_uri'); - return Promise.resolve({ - // eslint-disable-next-line camelcase - frontdoor_uri: redirectUri - ? `${expectedDefaultSingleUseUrl}&startURL=${redirectUri}` - : expectedDefaultSingleUseUrl, - }); - }) - ); - spies.set('resolver', stubMethod($$.SANDBOX, SfdcUrl.prototype, 'checkLightningDomain').resolves('1.1.1.1')); - }); - - afterEach(() => { - spies.clear(); - }); - - describe('url generation', () => { - it('opens default Agentforce Studio list view without flags', async () => { - const response = await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username, '--url-only']); - assert(response); - testJsonStructure(response); - expect(response.url).to.equal(getExpectedUrlWithPath('lightning/n/standard-AgentforceStudio')); - }); - - it('builds URL with api-name only', async () => { - const apiName = 'MyTestAgent'; - const response = await OrgOpenAuthoringBundle.run([ - '--json', - '--target-org', - testOrg.username, - '--url-only', - '--api-name', - apiName, - ]); - assert(response); - testJsonStructure(response); - const expectedPath = `AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=${apiName}`; - expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); - }); - - it('builds URL with api-name and version', async () => { - const apiName = 'MyTestAgent'; - const version = '1'; - const response = await OrgOpenAuthoringBundle.run([ - '--json', - '--target-org', - testOrg.username, - '--url-only', - '--api-name', - apiName, - '--version', - version, - ]); - assert(response); - testJsonStructure(response); - const expectedPath = `AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=${apiName}&projectVersionNumber=${version}`; - expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); - }); - - it('properly encodes special characters in api-name', async () => { - const apiName = 'My Test Agent'; - const response = await OrgOpenAuthoringBundle.run([ - '--json', - '--target-org', - testOrg.username, - '--url-only', - '--api-name', - apiName, - ]); - assert(response); - testJsonStructure(response); - const expectedPath = 'AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=My+Test+Agent'; - expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); - }); - - it('properly encodes special characters in version', async () => { - const apiName = 'MyAgent'; - const version = '1.0-beta'; - const response = await OrgOpenAuthoringBundle.run([ - '--json', - '--target-org', - testOrg.username, - '--url-only', - '--api-name', - apiName, - '--version', - version, - ]); - assert(response); - testJsonStructure(response); - const expectedPath = - 'AgentAuthoring/agentAuthoringBuilder.app#/project?projectName=MyAgent&projectVersionNumber=1.0-beta'; - expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); - }); - - it('generates single-use URL when --url-only is not passed', async () => { - const response = await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username]); - assert(response); - testJsonStructure(response); - expect(spies.get('requestGet').callCount).to.equal(1); - expect(spies.get('open').callCount).to.equal(1); - expect(response.url).to.equal(getExpectedUrlWithPath('lightning/n/standard-AgentforceStudio')); - }); - }); - - describe('browser integration', () => { - it('opens in specified browser with api-name', async () => { - const apiName = 'MyAgent'; - await OrgOpenAuthoringBundle.run([ - '--json', - '--target-org', - testOrg.username, - '--api-name', - apiName, - '--browser', - 'firefox', - ]); - - expect(spies.get('open').callCount).to.equal(1); - expect(spies.get('open').args[0][1]).to.not.eql({}); - }); - - it('opens in private mode', async () => { - await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username, '--private']); - - expect(spies.get('open').callCount).to.equal(1); - expect(spies.get('open').args[0][1]).to.have.property('newInstance'); - }); - }); - - describe('flag validation', () => { - it('allows api-name without version', async () => { - const response = await OrgOpenAuthoringBundle.run([ - '--json', - '--target-org', - testOrg.username, - '--url-only', - '--api-name', - 'MyAgent', - ]); - assert(response); - testJsonStructure(response); - }); - - it('requires api-name when version is provided', async () => { - try { - await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username, '--url-only', '--version', '1']); - assert.fail('Should have thrown an error'); - } catch (error) { - // Expected to fail due to missing api-name dependency - expect(error).to.exist; - } - }); - }); - - describe('human output', () => { - it('outputs success message without URL when opening browser', async () => { - await OrgOpenAuthoringBundle.run(['--json', '--target-org', testOrg.username]); - - expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); - expect(spies.get('open').callCount).to.equal(1); - }); - - it('outputs URL when using --url-only', async () => { - await OrgOpenAuthoringBundle.run(['--target-org', testOrg.username, '--url-only']); - - expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); - expect(spies.get('open').callCount).to.equal(0); - }); - - it('outputs URL with api-name when using --url-only', async () => { - await OrgOpenAuthoringBundle.run([ - '--target-org', - testOrg.username, - '--url-only', - '--api-name', - 'MyAgent', - '--version', - '1', - ]); - - expect(sfCommandUxStubs.logSuccess.callCount).to.be.greaterThan(0); - expect(spies.get('open').callCount).to.equal(0); - }); - }); - - describe('api-version flag', () => { - it('respects api-version flag', async () => { - const apiVersion = '59.0'; - const response = await OrgOpenAuthoringBundle.run([ - '--json', - '--target-org', - testOrg.username, - '--url-only', - '--api-version', - apiVersion, - ]); - assert(response); - testJsonStructure(response); - }); - }); -}); From f9fd41c9cb8c600537ade2476d6361fab0f18781 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 7 Apr 2026 09:20:34 -0600 Subject: [PATCH 4/6] chore: regen snapshot --- command-snapshot.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index e6cce429..ea7d9577 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -169,17 +169,7 @@ "command": "org:open:authoring-bundle", "flagAliases": ["urlonly"], "flagChars": ["b", "o", "r"], - "flags": [ - "api-name", - "api-version", - "browser", - "flags-dir", - "json", - "private", - "target-org", - "url-only", - "version" - ], + "flags": ["api-version", "browser", "flags-dir", "json", "private", "target-org", "url-only"], "plugin": "@salesforce/plugin-org" }, { From 56e1e7fa8c0762ebb647a73a9f2b464fd17459d2 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 13 Apr 2026 14:02:32 -0600 Subject: [PATCH 5/6] fix: remove mid-sentence line breaks in open.agent.md --- messages/open.agent.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/messages/open.agent.md b/messages/open.agent.md index 890671a8..1120e5bd 100644 --- a/messages/open.agent.md +++ b/messages/open.agent.md @@ -4,11 +4,9 @@ Open an agent in your org's Agent Builder UI in a browser. # description -Use the --api-name flag to open an agent using its API name in the Agent Builder UI of your org. To find the agent's API -name, go to Setup in your org and navigate to the agent's details page. +Use the --api-name flag to open an agent using its API name in the Agent Builder UI of your org. To find the agent's API name, go to Setup in your org and navigate to the agent's details page. -Alternatively, use the --authoring-bundle flag to open an agent in Agentforce Builder. Optionally include --version to -open a specific version of the agent. You'll specify the api name of the authoring bundle. +Alternatively, use the --authoring-bundle flag to open an agent in Agentforce Builder. Optionally include --version to open a specific version of the agent. You'll specify the api name of the authoring bundle. To generate the URL but not launch it in your browser, specify --url-only. From 885f0f9e05a13bc8ec6a1f9830646c2725ce7617 Mon Sep 17 00:00:00 2001 From: Esteban Romero Date: Mon, 13 Apr 2026 17:35:56 -0300 Subject: [PATCH 6/6] chore: remove mid-sentence line breaks in open.agent.md --- messages/open.agent.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/messages/open.agent.md b/messages/open.agent.md index 1120e5bd..b5a911c5 100644 --- a/messages/open.agent.md +++ b/messages/open.agent.md @@ -10,8 +10,7 @@ Alternatively, use the --authoring-bundle flag to open an agent in Agentforce Bu To generate the URL but not launch it in your browser, specify --url-only. -To open Agent Builder in a specific browser, use the --browser flag. Supported browsers are "chrome", "edge", and " -firefox". If you don't specify --browser, the org opens in your default browser. +To open Agent Builder in a specific browser, use the --browser flag. Supported browsers are "chrome", "edge", and "firefox". If you don't specify --browser, the org opens in your default browser. # examples