From be288601f871649144e237e89eb37731a0c115c0 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Wed, 1 Apr 2026 08:57:36 -0600 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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/9] 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 From 62658c93b6ec7793ed523d759d803bb96e87c1f1 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 16 Apr 2026 09:12:44 -0700 Subject: [PATCH 7/9] feat: support --version flag with --api-name in org open agent Allow --version to work with both --api-name and --authoring-bundle. When used with --api-name, queries BotVersion to get the version ID and adds it to the URL as versionId parameter. --- src/commands/org/open/agent.ts | 13 ++++--- test/unit/org/open/agent.test.ts | 61 +++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/commands/org/open/agent.ts b/src/commands/org/open/agent.ts index 6e66824c..1f8fd956 100644 --- a/src/commands/org/open/agent.ts +++ b/src/commands/org/open/agent.ts @@ -60,7 +60,6 @@ export class OrgOpenAgent extends OrgOpenCommandBase { version: Flags.string({ summary: messages.getMessage('flags.version.summary'), description: messages.getMessage('flags.version.description'), - dependsOn: ['authoring-bundle'], }), }; @@ -71,7 +70,7 @@ export class OrgOpenAgent extends OrgOpenCommandBase { let path: string; if (flags['api-name']) { - path = await buildRetUrl(this.connection, flags['api-name']); + path = await buildRetUrl(this.connection, flags['api-name'], flags.version); } else { // authoring-bundle is provided const queryParams = new URLSearchParams({ @@ -89,8 +88,14 @@ export class OrgOpenAgent extends OrgOpenCommandBase { } // Build the URL part to the Agent Builder given a Bot API name. -const buildRetUrl = async (conn: Connection, botName: string): Promise => { +const buildRetUrl = async (conn: Connection, botName: string, version?: string): Promise => { const query = `SELECT id FROM BotDefinition WHERE DeveloperName='${botName}'`; const botId = (await conn.singleRecordQuery<{ Id: string }>(query)).Id; - return `AiCopilot/copilotStudio.app#/copilot/builder?copilotId=${botId}`; + const queryParams = new URLSearchParams({ copilotId: botId }); + if (version) { + const versionQuery = `SELECT Id FROM BotVersion WHERE BotDefinitionId='${botId}' AND VersionNumber=${version}`; + const versionId = (await conn.singleRecordQuery<{ Id: string }>(versionQuery)).Id; + queryParams.set('versionId', versionId); + } + return `AiCopilot/copilotStudio.app#/copilot/builder?${queryParams.toString()}`; }; diff --git a/test/unit/org/open/agent.test.ts b/test/unit/org/open/agent.test.ts index b85c334b..2788608c 100644 --- a/test/unit/org/open/agent.test.ts +++ b/test/unit/org/open/agent.test.ts @@ -33,6 +33,7 @@ describe('org:open:agent', () => { const mockBotId = '0Xx1234567890ABCD'; const mockBotName = 'TestAgent'; + const mockVersionId = '0X9DD0000000032s0AA'; let sfCommandUxStubs: ReturnType; @@ -107,27 +108,6 @@ describe('org:open:agent', () => { 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', () => { @@ -168,6 +148,45 @@ describe('org:open:agent', () => { expect(spies.get('singleRecordQuery').callCount).to.equal(1); expect(spies.get('singleRecordQuery').firstCall.args[0]).to.include(specialName); }); + + it('builds URL with api-name and version using BotVersion query', async () => { + const version = '2'; + // Override the singleRecordQuery stub to return different results for BotDefinition and BotVersion + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const singleRecordQueryStub = spies.get('singleRecordQuery'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + singleRecordQueryStub.onFirstCall().resolves({ Id: mockBotId }); // BotDefinition query + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + singleRecordQueryStub.onSecondCall().resolves({ Id: mockVersionId }); // BotVersion query + + const response = await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + mockBotName, + '--version', + version, + ]); + assert(response); + testJsonStructure(response); + + // Verify both queries were made + expect(singleRecordQueryStub.callCount).to.equal(2); + + // Verify BotDefinition query + expect(singleRecordQueryStub.firstCall.args[0]).to.include(mockBotName); + expect(singleRecordQueryStub.firstCall.args[0]).to.include('BotDefinition'); + + // Verify BotVersion query + expect(singleRecordQueryStub.secondCall.args[0]).to.include('BotVersion'); + expect(singleRecordQueryStub.secondCall.args[0]).to.include(mockBotId); + expect(singleRecordQueryStub.secondCall.args[0]).to.include(`VersionNumber=${version}`); + + const expectedPath = `AiCopilot/copilotStudio.app#/copilot/builder?copilotId=${mockBotId}&versionId=${mockVersionId}`; + expect(response.url).to.equal(getExpectedUrlWithPath(expectedPath)); + }); }); describe('url generation with authoring-bundle', () => { From 139bb7a4269446b280993a02727a0d7ed4c5a555 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 16 Apr 2026 09:15:42 -0700 Subject: [PATCH 8/9] feat: add helpful error messages for agent not found cases Wrap BotDefinition and BotVersion queries in try-catch blocks to provide user-friendly error messages when an agent or version doesn't exist, instead of showing raw SOQL error. --- src/commands/org/open/agent.ts | 21 ++++++++++--- test/unit/org/open/agent.test.ts | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/commands/org/open/agent.ts b/src/commands/org/open/agent.ts index 1f8fd956..34f0268b 100644 --- a/src/commands/org/open/agent.ts +++ b/src/commands/org/open/agent.ts @@ -15,7 +15,7 @@ */ import { Flags } from '@salesforce/sf-plugins-core'; -import { Connection, Messages } from '@salesforce/core'; +import { Connection, Messages, SfError } from '@salesforce/core'; import { OrgOpenCommandBase } from '../../../shared/orgOpenCommandBase.js'; import { type OrgOpenOutput } from '../../../shared/orgTypes.js'; @@ -90,12 +90,25 @@ export class OrgOpenAgent extends OrgOpenCommandBase { // Build the URL part to the Agent Builder given a Bot API name. const buildRetUrl = async (conn: Connection, botName: string, version?: string): Promise => { const query = `SELECT id FROM BotDefinition WHERE DeveloperName='${botName}'`; - const botId = (await conn.singleRecordQuery<{ Id: string }>(query)).Id; + let botId: string; + try { + botId = (await conn.singleRecordQuery<{ Id: string }>(query)).Id; + } catch (error) { + throw new SfError(`No agent found with API name '${botName}' in the target org.`, 'AgentNotFound'); + } + const queryParams = new URLSearchParams({ copilotId: botId }); if (version) { const versionQuery = `SELECT Id FROM BotVersion WHERE BotDefinitionId='${botId}' AND VersionNumber=${version}`; - const versionId = (await conn.singleRecordQuery<{ Id: string }>(versionQuery)).Id; - queryParams.set('versionId', versionId); + try { + const versionId = (await conn.singleRecordQuery<{ Id: string }>(versionQuery)).Id; + queryParams.set('versionId', versionId); + } catch (error) { + throw new SfError( + `No version '${version}' found for agent '${botName}' in the target org.`, + 'AgentVersionNotFound' + ); + } } return `AiCopilot/copilotStudio.app#/copilot/builder?${queryParams.toString()}`; }; diff --git a/test/unit/org/open/agent.test.ts b/test/unit/org/open/agent.test.ts index 2788608c..24cb51e8 100644 --- a/test/unit/org/open/agent.test.ts +++ b/test/unit/org/open/agent.test.ts @@ -337,6 +337,60 @@ describe('org:open:agent', () => { }); }); + describe('error handling', () => { + it('throws helpful error when agent API name does not exist', async () => { + const nonExistentAgent = 'NonExistent_Agent'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + spies.get('singleRecordQuery').rejects(new Error('No records found')); + + try { + await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + nonExistentAgent, + ]); + assert.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect((error as Error).message).to.include(`No agent found with API name '${nonExistentAgent}'`); + expect((error as Error).name).to.equal('AgentNotFound'); + } + }); + + it('throws helpful error when agent version does not exist', async () => { + const nonExistentVersion = '999'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const singleRecordQueryStub = spies.get('singleRecordQuery'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + singleRecordQueryStub.onFirstCall().resolves({ Id: mockBotId }); // BotDefinition succeeds + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + singleRecordQueryStub.onSecondCall().rejects(new Error('No records found')); // BotVersion fails + + try { + await OrgOpenAgent.run([ + '--json', + '--target-org', + testOrg.username, + '--url-only', + '--api-name', + mockBotName, + '--version', + nonExistentVersion, + ]); + assert.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + expect((error as Error).message).to.include( + `No version '${nonExistentVersion}' found for agent '${mockBotName}'` + ); + expect((error as Error).name).to.equal('AgentVersionNotFound'); + } + }); + }); + describe('api-version flag', () => { it('respects api-version flag with api-name', async () => { const apiVersion = '59.0'; From acd26fefad11f3a6ac969906ef278ee1f88f1c21 Mon Sep 17 00:00:00 2001 From: Esteban Romero Date: Thu, 16 Apr 2026 14:58:55 -0300 Subject: [PATCH 9/9] chore: revert changes in .md file --- messages/open.authoring-bundle.md | 32 +------------------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/messages/open.authoring-bundle.md b/messages/open.authoring-bundle.md index b837842a..2c1b13f5 100644 --- a/messages/open.authoring-bundle.md +++ b/messages/open.authoring-bundle.md @@ -1,13 +1,11 @@ # summary -Open your org in Agentforce Studio, specifically in the list view showing the list of agents, or open a specific agent in Agentforce Builder. +Open your org in Agentforce Studio, specifically in the list view showing the list of agents. # 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 @@ -16,14 +14,6 @@ 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 @@ -32,26 +22,6 @@ 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.