diff --git a/command-snapshot.json b/command-snapshot.json index cfa5e7dc..0ee1cb38 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -4,7 +4,7 @@ "command": "agent:activate", "flagAliases": [], "flagChars": ["n", "o"], - "flags": ["api-name", "api-version", "flags-dir", "json", "target-org"], + "flags": ["api-name", "api-version", "flags-dir", "json", "target-org", "version"], "plugin": "@salesforce/plugin-agent" }, { diff --git a/messages/agent.activate.md b/messages/agent.activate.md index d370cff6..d03814e8 100644 --- a/messages/agent.activate.md +++ b/messages/agent.activate.md @@ -4,23 +4,29 @@ Activate an agent in an org. # description -Activating an agent makes it immediately available to your users. An agent must be active before you can preview it with the "agent preview" CLI command or VS Code. +Activating an agent makes it immediately available to your users. A published agent must be active before you can preview it with the "agent preview" CLI command or VS Code. Agents can have multiple versions; only one version can be active at a time. -You must know the agent's API name to activate it; you can either be prompted for it or you can specify it with the --api-name flag. Find the agent's API name in its Agent Details page of your org's Agentforce Studio UI in Setup. +If you run the command without the --api-name or --version flags, the command provides a list of agent API names and versions for you to choose from. Use the flags to specify the exact agent and version without being prompted. If you use the --json flag and not --version, then the latest agent version is automatically activated. + +The value of the --version flag is always a number, corresponding to the "vX" part of the "BotVersion" metadata in your project. For example, if you have a force-app/main/default/bots/My_Agent/v4.botVersion-meta.xml file in your project, then you activate this version with the "--version 4" flag. # examples -- Activate an agent in your default target org by being prompted: +- Activate an agent in your default target org by being prompted for both its API name and version: <%= config.bin %> <%= command.id %> -- Activate an agent with API name Resort_Manager in the org with alias "my-org": +- Activate version 2 of an agent with API name Resort_Manager in the org with alias "my-org": - <%= config.bin %> <%= command.id %> --api-name Resort_Manager --target-org my-org + <%= config.bin %> <%= command.id %> --api-name Resort_Manager --version 2 --target-org my-org # flags.api-name.summary -API name of the agent to activate. +API name of the agent to activate; if not specified, the command provides a list that you choose from. + +# flags.version.summary + +Version number of the agent to activate; if not specified, the command provides a list that you choose from. # error.missingRequiredFlags diff --git a/messages/agent.activation.md b/messages/agent.activation.md index 96b45a63..46ff6632 100644 --- a/messages/agent.activation.md +++ b/messages/agent.activation.md @@ -13,3 +13,7 @@ Agent %s has been deleted and can't be activated. # error.agentIsDefault Agent %s is the default Agentforce agent in your org and you can't change its activation status. + +# error.noVersionsAvailable + +No versions available to %s. All versions are already in the target state. diff --git a/messages/agent.deactivate.md b/messages/agent.deactivate.md index 7303405e..06f39755 100644 --- a/messages/agent.deactivate.md +++ b/messages/agent.deactivate.md @@ -6,7 +6,7 @@ Deactivate an agent in an org. Deactivating an agent makes it unavailable to your users. To make changes to an agent, such as adding or removing topics or actions, you must deactivate it. You can't preview an agent with the "agent preview" CLI command or VS Code if it's deactivated. -You must know the agent's API name to deactivate it; you can either be prompted for it or you can specify it with the --api-name flag. Find the agent's API name in its Agent Details page of your org's Agentforce Studio UI in Setup. +If you run the command without the --api-name flag, the command provides a list of agent API names for you to choose from. Use the flag to specify the exact agent without being prompted. # examples @@ -20,7 +20,7 @@ You must know the agent's API name to deactivate it; you can either be prompted # flags.api-name.summary -API name of the agent to deactivate. +API name of the agent to deactivate; if not specified, the command provides a list that you choose from. # error.missingRequiredFlags diff --git a/package.json b/package.json index d7db6847..975828be 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@inquirer/prompts": "^7.10.1", "@oclif/core": "^4", "@oclif/multi-stage-output": "^0.8.29", - "@salesforce/agents": "^0.24.1", + "@salesforce/agents": "^0.24.2", "@salesforce/core": "^8.26.3", "@salesforce/kit": "^3.2.4", "@salesforce/sf-plugins-core": "^12.2.6", diff --git a/schemas/agent-activate.json b/schemas/agent-activate.json new file mode 100644 index 00000000..5827721b --- /dev/null +++ b/schemas/agent-activate.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/AgentActivateResult", + "definitions": { + "AgentActivateResult": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "version": { + "type": "number" + } + }, + "required": ["success", "version"], + "additionalProperties": false + } + } +} diff --git a/schemas/agent-deactivate.json b/schemas/agent-deactivate.json new file mode 100644 index 00000000..5827721b --- /dev/null +++ b/schemas/agent-deactivate.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/AgentActivateResult", + "definitions": { + "AgentActivateResult": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "version": { + "type": "number" + } + }, + "required": ["success", "version"], + "additionalProperties": false + } + } +} diff --git a/src/agentActivation.ts b/src/agentActivation.ts index 07d9c110..8fc68bca 100644 --- a/src/agentActivation.ts +++ b/src/agentActivation.ts @@ -15,7 +15,7 @@ */ import { Messages, Org, SfError, SfProject } from '@salesforce/core'; -import { Agent, type BotMetadata, ProductionAgent } from '@salesforce/agents'; +import { Agent, type BotMetadata, type BotVersionMetadata, ProductionAgent } from '@salesforce/agents'; import { select } from '@inquirer/prompts'; type Choice = { @@ -28,6 +28,11 @@ type AgentValue = { DeveloperName: string; }; +type VersionChoice = { + version: number; + status: string; +}; + Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.activation'); @@ -45,26 +50,42 @@ export const validateAgent = (agent: BotMetadata): boolean => { }; export const getAgentChoices = (agents: BotMetadata[], status: 'Active' | 'Inactive'): Array> => - agents.map((agent) => { - let disabled: string | boolean = false; - - const lastBotVersion = agent.BotVersions.records[agent.BotVersions.records.length - 1]; - if (lastBotVersion.Status === status) { - disabled = `(Already ${status})`; - } - if (agentIsUnsupported(agent.DeveloperName)) { - disabled = '(Not Supported)'; - } - - return { + agents + .filter((agent) => { + // Only one version can be active at a time + // For activate (status='Active'): show agents that don't have an active version (all versions are inactive) + // For deactivate (status='Inactive'): show agents that have an active version + const hasActiveVersion = agent.BotVersions.records.some((version) => version.Status === 'Active'); + const canPerformOperation = status === 'Active' ? !hasActiveVersion : hasActiveVersion; + // Filter out agents that can't perform the operation or are unsupported + return canPerformOperation && !agentIsUnsupported(agent.DeveloperName); + }) + .sort((a, b) => a.DeveloperName.localeCompare(b.DeveloperName)) + .map((agent) => ({ name: agent.DeveloperName, value: { Id: agent.Id, DeveloperName: agent.DeveloperName, }, - disabled, - }; - }); + })); + +export const getVersionChoices = ( + versions: BotVersionMetadata[], + status: 'Active' | 'Inactive' +): Array> => + versions + .sort((a, b) => b.VersionNumber - a.VersionNumber) + .map((version) => { + const isTargetStatus = version.Status === status; + return { + name: `Version ${version.VersionNumber}`, + value: { + version: version.VersionNumber, + status: version.Status, + }, + disabled: isTargetStatus ? `(Already ${status})` : false, + }; + }); export const getAgentForActivation = async (config: { targetOrg: Org; @@ -110,3 +131,62 @@ export const getAgentForActivation = async (config: { project: SfProject.getInstance(), }); }; + +export const getVersionForActivation = async (config: { + agent: ProductionAgent; + status: 'Active' | 'Inactive'; + versionFlag?: number; + jsonEnabled?: boolean; +}): Promise<{ version: number | undefined; warning?: string }> => { + const { agent, status, versionFlag, jsonEnabled } = config; + + // If version flag is provided, return it + if (versionFlag !== undefined) { + return { version: versionFlag }; + } + + // Get bot metadata to access versions + const botMetadata = await agent.getBotMetadata(); + // Filter out deleted versions as a defensive measure + const versions = botMetadata.BotVersions.records.filter((v) => !v.IsDeleted); + + // If there's only one version, return it + if (versions.length === 1) { + return { version: versions[0].VersionNumber }; + } + + // Get version choices and filter out disabled ones + const choices = getVersionChoices(versions, status); + const availableChoices = choices.filter((choice) => !choice.disabled); + + // If there's only one available choice, return it automatically + if (availableChoices.length === 1) { + return { version: availableChoices[0].value.version }; + } + + // If no versions are available, throw an error + if (availableChoices.length === 0) { + const action = status === 'Active' ? 'activate' : 'deactivate'; + throw messages.createError('error.noVersionsAvailable', [action]); + } + + // If JSON mode is enabled, automatically select the latest available version + if (jsonEnabled) { + // Find the latest (highest version number) available version + const latestVersion = availableChoices.reduce((latest, choice) => + choice.value.version > latest.value.version ? choice : latest + ); + return { + version: latestVersion.value.version, + warning: `No version specified, automatically selected latest available version: ${latestVersion.value.version}`, + }; + } + + // Prompt user to select a version + const versionChoice = await select({ + message: 'Select a version', + choices, + }); + + return { version: versionChoice.version }; +}; diff --git a/src/commands/agent/activate.ts b/src/commands/agent/activate.ts index 30d68706..3d1b7035 100644 --- a/src/commands/agent/activate.ts +++ b/src/commands/agent/activate.ts @@ -15,15 +15,18 @@ */ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; -import { getAgentForActivation } from '../../agentActivation.js'; +import { getAgentForActivation, getVersionForActivation } from '../../agentActivation.js'; + +export type AgentActivateResult = { success: boolean; version: number }; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.activate'); -export default class AgentActivate extends SfCommand { +export default class AgentActivate extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); + public static readonly enableJsonFlag = true; public static readonly flags = { 'target-org': Flags.requiredOrg(), @@ -32,9 +35,10 @@ export default class AgentActivate extends SfCommand { summary: messages.getMessage('flags.api-name.summary'), char: 'n', }), + version: Flags.integer({ summary: messages.getMessage('flags.version.summary') }), }; - public async run(): Promise { + public async run(): Promise { const { flags } = await this.parse(AgentActivate); const apiNameFlag = flags['api-name']; @@ -43,11 +47,20 @@ export default class AgentActivate extends SfCommand { if (!apiNameFlag && this.jsonEnabled()) { throw messages.createError('error.missingRequiredFlags', ['api-name']); } - const agent = await getAgentForActivation({ targetOrg, status: 'Active', apiNameFlag }); - await agent.activate(); - const agentName = (await agent.getBotMetadata()).DeveloperName; + const { version, warning } = await getVersionForActivation({ + agent, + status: 'Active', + versionFlag: flags.version, + jsonEnabled: this.jsonEnabled(), + }); + const result = await agent.activate(version); + const metadata = await agent.getBotMetadata(); - this.log(`Agent ${agentName} activated.`); + this.log(`${metadata.DeveloperName} v${result.VersionNumber} activated.`); + if (warning) { + this.warn(warning); + } + return { success: true, version: result.VersionNumber }; } } diff --git a/src/commands/agent/deactivate.ts b/src/commands/agent/deactivate.ts index 733764ee..c39019d8 100644 --- a/src/commands/agent/deactivate.ts +++ b/src/commands/agent/deactivate.ts @@ -16,14 +16,16 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import { getAgentForActivation } from '../../agentActivation.js'; +import { AgentActivateResult } from './activate.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.deactivate'); -export default class AgentDeactivate extends SfCommand { +export default class AgentDeactivate extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); + public static enableJsonFlag = true; public static readonly flags = { 'target-org': Flags.requiredOrg(), @@ -34,7 +36,7 @@ export default class AgentDeactivate extends SfCommand { }), }; - public async run(): Promise { + public async run(): Promise { const { flags } = await this.parse(AgentDeactivate); const apiNameFlag = flags['api-name']; @@ -45,9 +47,10 @@ export default class AgentDeactivate extends SfCommand { } const agent = await getAgentForActivation({ targetOrg, status: 'Inactive', apiNameFlag }); - await agent.deactivate(); - const agentName = (await agent.getBotMetadata()).DeveloperName; + const result = await agent.deactivate(); + const metadata = await agent.getBotMetadata(); - this.log(`Agent ${agentName} deactivated.`); + this.log(`${metadata.DeveloperName} v${result.VersionNumber} deactivated.`); + return { success: true, version: result.VersionNumber }; } } diff --git a/test/agentActivation.test.ts b/test/agentActivation.test.ts new file mode 100644 index 00000000..dd244a56 --- /dev/null +++ b/test/agentActivation.test.ts @@ -0,0 +1,396 @@ +/* + * 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 { expect } from 'chai'; +import sinon from 'sinon'; +import { type BotMetadata, type BotVersionMetadata, type ProductionAgent } from '@salesforce/agents'; +import { getAgentChoices, getVersionChoices, getVersionForActivation } from '../src/agentActivation.js'; + +describe('agentActivation', () => { + describe('getVersionChoices', () => { + it('should mark versions with target status as disabled', () => { + const versions: BotVersionMetadata[] = [ + { + Id: 'v1', + Status: 'Active', + VersionNumber: 1, + DeveloperName: 'Test_v1', + } as BotVersionMetadata, + { + Id: 'v2', + Status: 'Inactive', + VersionNumber: 2, + DeveloperName: 'Test_v2', + } as BotVersionMetadata, + { + Id: 'v3', + Status: 'Inactive', + VersionNumber: 3, + DeveloperName: 'Test_v3', + } as BotVersionMetadata, + ]; + + const choices = getVersionChoices(versions, 'Inactive'); + + expect(choices).to.have.lengthOf(3); + // Sorted descending: v3, v2, v1 + expect(choices[0].disabled).to.equal('(Already Inactive)'); // Version 3 is already Inactive + expect(choices[1].disabled).to.equal('(Already Inactive)'); // Version 2 is already Inactive + expect(choices[2].disabled).to.equal(false); // Version 1 is Active, can be deactivated + }); + + it('should include version numbers in choices', () => { + const versions: BotVersionMetadata[] = [ + { + Id: 'v1', + Status: 'Active', + VersionNumber: 5, + DeveloperName: 'Test_v5', + } as BotVersionMetadata, + ]; + + const choices = getVersionChoices(versions, 'Active'); + + expect(choices[0].name).to.equal('Version 5'); + expect(choices[0].value.version).to.equal(5); + expect(choices[0].value.status).to.equal('Active'); + }); + + it('should mark active versions as available for deactivation', () => { + const versions: BotVersionMetadata[] = [ + { + Id: 'v1', + Status: 'Active', + VersionNumber: 1, + DeveloperName: 'Test_v1', + } as BotVersionMetadata, + { + Id: 'v2', + Status: 'Active', + VersionNumber: 2, + DeveloperName: 'Test_v2', + } as BotVersionMetadata, + ]; + + const choices = getVersionChoices(versions, 'Inactive'); + + expect(choices[0].disabled).to.equal(false); + expect(choices[1].disabled).to.equal(false); + }); + }); + + describe('getAgentChoices', () => { + it('should filter out agent when any version is already active (for activation)', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Active', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 3 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(0); // Filtered out because it already has an active version + }); + + it('should include agent when it has an active version (for deactivation)', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Active', VersionNumber: 2 } as BotVersionMetadata, // Can be deactivated + { Status: 'Inactive', VersionNumber: 3 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Inactive'); + + expect(choices).to.have.lengthOf(1); + expect(choices[0].value.DeveloperName).to.equal('Test_Agent'); + }); + + it('should include agent when all versions are inactive (for activation)', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 3 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(1); // All versions are inactive, so can activate one + expect(choices[0].value.DeveloperName).to.equal('Test_Agent'); + }); + + it('should filter out agent when all versions are inactive (for deactivation)', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Test_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Inactive'); + + expect(choices).to.have.lengthOf(0); // All versions are already inactive, nothing to deactivate + }); + + it('should filter out unsupported agents', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Copilot_for_Salesforce', + BotVersions: { + records: [{ Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(0); // Unsupported agents are filtered out + }); + + it('should filter out unavailable agents and sort remaining alphabetically', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Zebra_Agent', + BotVersions: { + records: [ + { Status: 'Active', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + { + Id: 'agent2', + DeveloperName: 'Beta_Agent', + BotVersions: { + records: [ + { Status: 'Active', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + { + Id: 'agent3', + DeveloperName: 'Alpha_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(1); // Only Alpha_Agent has no active version (all inactive) + expect(choices[0].value.DeveloperName).to.equal('Alpha_Agent'); + }); + + it('should sort multiple available agents alphabetically', () => { + const agents: BotMetadata[] = [ + { + Id: 'agent1', + DeveloperName: 'Zebra_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + { + Id: 'agent2', + DeveloperName: 'Alpha_Agent', + BotVersions: { + records: [ + { Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata, + { Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, + ], + }, + } as BotMetadata, + { + Id: 'agent3', + DeveloperName: 'Beta_Agent', + BotVersions: { + records: [{ Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata], + }, + } as BotMetadata, + ]; + + const choices = getAgentChoices(agents, 'Active'); + + expect(choices).to.have.lengthOf(3); + expect(choices[0].value.DeveloperName).to.equal('Alpha_Agent'); + expect(choices[1].value.DeveloperName).to.equal('Beta_Agent'); + expect(choices[2].value.DeveloperName).to.equal('Zebra_Agent'); + }); + }); + + describe('getVersionForActivation', () => { + let mockAgent: sinon.SinonStubbedInstance; + + beforeEach(() => { + mockAgent = { + getBotMetadata: sinon.stub(), + } as unknown as sinon.SinonStubbedInstance; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should return version flag when provided', async () => { + const result = await getVersionForActivation({ + agent: mockAgent as unknown as ProductionAgent, + status: 'Active', + versionFlag: 5, + }); + + expect(result.version).to.equal(5); + expect(result.warning).to.be.undefined; + expect(mockAgent.getBotMetadata.called).to.be.false; + }); + + it('should auto-select when only one version exists', async () => { + mockAgent.getBotMetadata.resolves({ + BotVersions: { + records: [{ VersionNumber: 3, Status: 'Inactive', IsDeleted: false } as BotVersionMetadata], + }, + } as BotMetadata); + + const result = await getVersionForActivation({ + agent: mockAgent as unknown as ProductionAgent, + status: 'Active', + }); + + expect(result.version).to.equal(3); + expect(result.warning).to.be.undefined; + }); + + it('should auto-select when only one available choice exists', async () => { + mockAgent.getBotMetadata.resolves({ + BotVersions: { + records: [ + { VersionNumber: 1, Status: 'Active', IsDeleted: false } as BotVersionMetadata, + { VersionNumber: 2, Status: 'Inactive', IsDeleted: false } as BotVersionMetadata, + ], + }, + } as BotMetadata); + + const result = await getVersionForActivation({ + agent: mockAgent as unknown as ProductionAgent, + status: 'Active', + }); + + // Only version 2 is available (inactive), version 1 is already active + expect(result.version).to.equal(2); + expect(result.warning).to.be.undefined; + }); + + it('should auto-select latest version in JSON mode', async () => { + mockAgent.getBotMetadata.resolves({ + BotVersions: { + records: [ + { VersionNumber: 1, Status: 'Inactive', IsDeleted: false } as BotVersionMetadata, + { VersionNumber: 2, Status: 'Inactive', IsDeleted: false } as BotVersionMetadata, + { VersionNumber: 3, Status: 'Inactive', IsDeleted: false } as BotVersionMetadata, + ], + }, + } as BotMetadata); + + const result = await getVersionForActivation({ + agent: mockAgent as unknown as ProductionAgent, + status: 'Active', + jsonEnabled: true, + }); + + expect(result.version).to.equal(3); + expect(result.warning).to.include('automatically selected latest available version: 3'); + }); + + it('should filter out deleted versions', async () => { + mockAgent.getBotMetadata.resolves({ + BotVersions: { + records: [ + { VersionNumber: 1, Status: 'Inactive', IsDeleted: true } as BotVersionMetadata, + { VersionNumber: 2, Status: 'Inactive', IsDeleted: false } as BotVersionMetadata, + ], + }, + } as BotMetadata); + + const result = await getVersionForActivation({ + agent: mockAgent as unknown as ProductionAgent, + status: 'Active', + }); + + // Only version 2 should be considered (version 1 is deleted) + expect(result.version).to.equal(2); + }); + + it('should throw error when no versions are available', async () => { + mockAgent.getBotMetadata.resolves({ + BotVersions: { + records: [ + { VersionNumber: 1, Status: 'Active', IsDeleted: false } as BotVersionMetadata, + { VersionNumber: 2, Status: 'Active', IsDeleted: false } as BotVersionMetadata, + ], + }, + } as BotMetadata); + + try { + await getVersionForActivation({ + agent: mockAgent as unknown as ProductionAgent, + status: 'Active', + jsonEnabled: true, + }); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect((error as Error).message).to.include('No versions available to activate'); + } + }); + }); +}); diff --git a/test/nuts/agent.activate.nut.ts b/test/nuts/agent.activate.nut.ts index 8ed91121..6d18a86d 100644 --- a/test/nuts/agent.activate.nut.ts +++ b/test/nuts/agent.activate.nut.ts @@ -85,6 +85,59 @@ describe('agent activate/deactivate NUTs', function () { expect(finalStatus).to.equal('Active'); }); + it('should activate the agent with version flag', async () => { + // Ensure agent is inactive first + const initialStatus = await getBotStatus(); + if (initialStatus === 'Active') { + execCmd(`agent deactivate --api-name ${botApiName} --target-org ${username} --json`, { + ensureExitCode: 0, + }); + await sleep(5000); + } + + // Activate with version 1 + execCmd(`agent activate --api-name ${botApiName} --target-org ${username} --version 1 --json`, { + ensureExitCode: 0, + }); + + // Verify the BotVersion status is now 'Active' + const finalStatus = await getBotStatus(); + expect(finalStatus).to.equal('Active'); + }); + + it('should auto-select latest version in JSON mode without version flag', async () => { + // Ensure agent is inactive first + const initialStatus = await getBotStatus(); + if (initialStatus === 'Active') { + execCmd(`agent deactivate --api-name ${botApiName} --target-org ${username} --json`, { + ensureExitCode: 0, + }); + await sleep(5000); + } + + // Activate with --json but no --version flag + const result = execCmd<{ version: number; success: boolean }>( + `agent activate --api-name ${botApiName} --target-org ${username} --json`, + { + ensureExitCode: 0, + } + ); + + // Parse the JSON result + const jsonResult = result.jsonOutput!.result; + expect(jsonResult?.success).to.equal(true); + expect(jsonResult?.version).to.be.a('number'); + + // Verify the warning was included in the output + expect(result.shellOutput.stderr).to.include( + 'No version specified, automatically selected latest available version' + ); + + // Verify the BotVersion status is now 'Active' + const finalStatus = await getBotStatus(); + expect(finalStatus).to.equal('Active'); + }); + it('should deactivate the agent', async () => { // Verify the BotVersion status has 'Active' initial state const initialStatus = await getBotStatus(); @@ -98,4 +151,24 @@ describe('agent activate/deactivate NUTs', function () { const finalStatus = await getBotStatus(); expect(finalStatus).to.equal('Inactive'); }); + + it('should deactivate the agent (version automatically detected)', async () => { + // Ensure agent is active first + const initialStatus = await getBotStatus(); + if (initialStatus === 'Inactive') { + execCmd(`agent activate --api-name ${botApiName} --target-org ${username} --version 1 --json`, { + ensureExitCode: 0, + }); + await sleep(5000); + } + + // Deactivate (version is automatically detected) + execCmd(`agent deactivate --api-name ${botApiName} --target-org ${username} --json`, { + ensureExitCode: 0, + }); + + // Verify the BotVersion status is now 'Inactive' + const finalStatus = await getBotStatus(); + expect(finalStatus).to.equal('Inactive'); + }); }); diff --git a/yarn.lock b/yarn.lock index b68fdb58..5ef712e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1731,14 +1731,14 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@salesforce/agents@^0.24.1": - version "0.24.1" - resolved "https://registry.yarnpkg.com/@salesforce/agents/-/agents-0.24.1.tgz#841e34c7cc2ea6d58a14de67725116dbea5e3180" - integrity sha512-eU9WRbUwtCsKsv2PhCW8ZXIG2sKS4FFhvKlTFeoCKdEj/rleNseF0Rwn6MI3sIsrP9HfZvhAkh2vbvoPRK/8rQ== +"@salesforce/agents@^0.24.2": + version "0.24.2" + resolved "https://registry.yarnpkg.com/@salesforce/agents/-/agents-0.24.2.tgz#e67a36bf07c6a4d29231f88dd1cf562b125bb768" + integrity sha512-apYmYxeS3bnqUl32BvqZX2ALu50GUfdLw4iyJjR2jGm2q/kVTSeb/UBq8FhJBCnsPocnhu+Hmmb60bU0NCLwbw== dependencies: - "@salesforce/core" "^8.26.2" + "@salesforce/core" "^8.26.3" "@salesforce/kit" "^3.2.4" - "@salesforce/source-deploy-retrieve" "^12.31.12" + "@salesforce/source-deploy-retrieve" "^12.31.14" "@salesforce/types" "^1.6.0" fast-xml-parser "^5.3.6" nock "^13.5.6" @@ -1882,7 +1882,7 @@ cli-progress "^3.12.0" terminal-link "^3.0.0" -"@salesforce/source-deploy-retrieve@^12.31.12", "@salesforce/source-deploy-retrieve@^12.31.14": +"@salesforce/source-deploy-retrieve@^12.31.14": version "12.31.14" resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.31.14.tgz#6ed0a2fdb9a14d60ed64a2dd8fdc18c16af143a6" integrity sha512-tLnTCG6t+d+MN8pGijF6nL4lsqE37FaBINZZvyd+IDAw+7eWffFqIXK/nNyQ1ZARTNeHOs5K/NlNXNBp5+x/YQ==