Skip to content

Commit a6dd2a7

Browse files
fix: usability, errors, tests
1 parent 240c90e commit a6dd2a7

7 files changed

Lines changed: 328 additions & 8 deletions

File tree

command-snapshot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"command": "agent:activate",
55
"flagAliases": [],
66
"flagChars": ["n", "o"],
7-
"flags": ["api-name", "api-version", "flags-dir", "json", "target-org"],
7+
"flags": ["api-name", "api-version", "flags-dir", "json", "target-org", "version"],
88
"plugin": "@salesforce/plugin-agent"
99
},
1010
{

schemas/agent-activate.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/AgentActivateResult",
4+
"definitions": {
5+
"AgentActivateResult": {
6+
"type": "object",
7+
"properties": {
8+
"success": {
9+
"type": "boolean"
10+
},
11+
"version": {
12+
"type": "number"
13+
}
14+
},
15+
"required": ["success", "version"],
16+
"additionalProperties": false
17+
}
18+
}
19+
}

schemas/agent-deactivate.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/AgentActivateResult",
4+
"definitions": {
5+
"AgentActivateResult": {
6+
"type": "object",
7+
"properties": {
8+
"success": {
9+
"type": "boolean"
10+
},
11+
"version": {
12+
"type": "number"
13+
}
14+
},
15+
"required": ["success", "version"],
16+
"additionalProperties": false
17+
}
18+
}
19+
}

src/agentActivation.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,13 @@ export const getVersionForActivation = async (config: {
138138
agent: ProductionAgent;
139139
status: 'Active' | 'Inactive';
140140
versionFlag?: number;
141-
}): Promise<number | undefined> => {
142-
const { agent, status, versionFlag } = config;
141+
jsonEnabled?: boolean;
142+
}): Promise<{ version: number | undefined; warning?: string }> => {
143+
const { agent, status, versionFlag, jsonEnabled } = config;
143144

144145
// If version flag is provided, return it
145146
if (versionFlag !== undefined) {
146-
return versionFlag;
147+
return { version: versionFlag };
147148
}
148149

149150
// Get bot metadata to access versions
@@ -152,7 +153,7 @@ export const getVersionForActivation = async (config: {
152153

153154
// If there's only one version, return it
154155
if (versions.length === 1) {
155-
return versions[0].VersionNumber;
156+
return { version: versions[0].VersionNumber };
156157
}
157158

158159
// Get version choices and filter out disabled ones
@@ -161,7 +162,19 @@ export const getVersionForActivation = async (config: {
161162

162163
// If there's only one available choice, return it automatically
163164
if (availableChoices.length === 1) {
164-
return availableChoices[0].value.version;
165+
return { version: availableChoices[0].value.version };
166+
}
167+
168+
// If JSON mode is enabled, automatically select the latest available version
169+
if (jsonEnabled) {
170+
// Find the latest (highest version number) available version
171+
const latestVersion = availableChoices.reduce((latest, choice) =>
172+
choice.value.version > latest.value.version ? choice : latest
173+
);
174+
return {
175+
version: latestVersion.value.version,
176+
warning: `No version specified, automatically selected latest available version: ${latestVersion.value.version}`,
177+
};
165178
}
166179

167180
// Prompt user to select a version
@@ -170,5 +183,5 @@ export const getVersionForActivation = async (config: {
170183
choices,
171184
});
172185

173-
return versionChoice.version;
186+
return { version: versionChoice.version };
174187
};

src/commands/agent/activate.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,19 @@ export default class AgentActivate extends SfCommand<AgentActivateResult> {
4848
throw messages.createError('error.missingRequiredFlags', ['api-name']);
4949
}
5050
const agent = await getAgentForActivation({ targetOrg, status: 'Active', apiNameFlag });
51-
const version = await getVersionForActivation({ agent, status: 'Active', versionFlag: flags.version });
51+
const { version, warning } = await getVersionForActivation({
52+
agent,
53+
status: 'Active',
54+
versionFlag: flags.version,
55+
jsonEnabled: this.jsonEnabled(),
56+
});
5257
const result = await agent.activate(version);
5358
const metadata = await agent.getBotMetadata();
5459

5560
this.log(`${metadata.DeveloperName} v${result.VersionNumber} activated.`);
61+
if (warning) {
62+
this.warn(warning);
63+
}
5664
return { success: true, version: result.VersionNumber };
5765
}
5866
}

test/agentActivation.test.ts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Copyright 2026, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { expect } from 'chai';
18+
import { type BotMetadata, type BotVersionMetadata } from '@salesforce/agents';
19+
import { getAgentChoices, getVersionChoices } from '../src/agentActivation.js';
20+
21+
describe('agentActivation', () => {
22+
describe('getVersionChoices', () => {
23+
it('should mark versions with target status as disabled', () => {
24+
const versions: BotVersionMetadata[] = [
25+
{
26+
Id: 'v1',
27+
Status: 'Active',
28+
VersionNumber: 1,
29+
DeveloperName: 'Test_v1',
30+
} as BotVersionMetadata,
31+
{
32+
Id: 'v2',
33+
Status: 'Inactive',
34+
VersionNumber: 2,
35+
DeveloperName: 'Test_v2',
36+
} as BotVersionMetadata,
37+
{
38+
Id: 'v3',
39+
Status: 'Inactive',
40+
VersionNumber: 3,
41+
DeveloperName: 'Test_v3',
42+
} as BotVersionMetadata,
43+
];
44+
45+
const choices = getVersionChoices(versions, 'Inactive');
46+
47+
expect(choices).to.have.lengthOf(3);
48+
expect(choices[0].disabled).to.equal(false); // Version 1 is Active, can be deactivated
49+
expect(choices[1].disabled).to.equal('(Already Inactive)'); // Version 2 is already Inactive
50+
expect(choices[2].disabled).to.equal('(Already Inactive)'); // Version 3 is already Inactive
51+
});
52+
53+
it('should include version numbers in choices', () => {
54+
const versions: BotVersionMetadata[] = [
55+
{
56+
Id: 'v1',
57+
Status: 'Active',
58+
VersionNumber: 5,
59+
DeveloperName: 'Test_v5',
60+
} as BotVersionMetadata,
61+
];
62+
63+
const choices = getVersionChoices(versions, 'Active');
64+
65+
expect(choices[0].name).to.equal('Version 5');
66+
expect(choices[0].value.version).to.equal(5);
67+
expect(choices[0].value.status).to.equal('Active');
68+
});
69+
70+
it('should mark active versions as available for deactivation', () => {
71+
const versions: BotVersionMetadata[] = [
72+
{
73+
Id: 'v1',
74+
Status: 'Active',
75+
VersionNumber: 1,
76+
DeveloperName: 'Test_v1',
77+
} as BotVersionMetadata,
78+
{
79+
Id: 'v2',
80+
Status: 'Active',
81+
VersionNumber: 2,
82+
DeveloperName: 'Test_v2',
83+
} as BotVersionMetadata,
84+
];
85+
86+
const choices = getVersionChoices(versions, 'Inactive');
87+
88+
expect(choices[0].disabled).to.equal(false);
89+
expect(choices[1].disabled).to.equal(false);
90+
});
91+
});
92+
93+
describe('getAgentChoices', () => {
94+
it('should enable agent when ANY version is available for activation', () => {
95+
const agents: BotMetadata[] = [
96+
{
97+
Id: 'agent1',
98+
DeveloperName: 'Test_Agent',
99+
BotVersions: {
100+
records: [
101+
{ Status: 'Active', VersionNumber: 1 } as BotVersionMetadata,
102+
{ Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata, // Can be activated
103+
{ Status: 'Inactive', VersionNumber: 3 } as BotVersionMetadata,
104+
],
105+
},
106+
} as BotMetadata,
107+
];
108+
109+
const choices = getAgentChoices(agents, 'Active');
110+
111+
expect(choices).to.have.lengthOf(1);
112+
expect(choices[0].disabled).to.equal(false); // Has inactive versions that can be activated
113+
expect(choices[0].value.DeveloperName).to.equal('Test_Agent');
114+
});
115+
116+
it('should enable agent when ANY version is available for deactivation', () => {
117+
const agents: BotMetadata[] = [
118+
{
119+
Id: 'agent1',
120+
DeveloperName: 'Test_Agent',
121+
BotVersions: {
122+
records: [
123+
{ Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata,
124+
{ Status: 'Active', VersionNumber: 2 } as BotVersionMetadata, // Can be deactivated
125+
{ Status: 'Inactive', VersionNumber: 3 } as BotVersionMetadata,
126+
],
127+
},
128+
} as BotMetadata,
129+
];
130+
131+
const choices = getAgentChoices(agents, 'Inactive');
132+
133+
expect(choices).to.have.lengthOf(1);
134+
expect(choices[0].disabled).to.equal(false); // Has active version that can be deactivated
135+
});
136+
137+
it('should disable agent when ALL versions are already in target state', () => {
138+
const agents: BotMetadata[] = [
139+
{
140+
Id: 'agent1',
141+
DeveloperName: 'Test_Agent',
142+
BotVersions: {
143+
records: [
144+
{ Status: 'Active', VersionNumber: 1 } as BotVersionMetadata,
145+
{ Status: 'Active', VersionNumber: 2 } as BotVersionMetadata,
146+
{ Status: 'Active', VersionNumber: 3 } as BotVersionMetadata,
147+
],
148+
},
149+
} as BotMetadata,
150+
];
151+
152+
const choices = getAgentChoices(agents, 'Active');
153+
154+
expect(choices).to.have.lengthOf(1);
155+
expect(choices[0].disabled).to.equal('(Already Active)'); // All versions are already active
156+
});
157+
158+
it('should disable agent when ALL versions are inactive for deactivation', () => {
159+
const agents: BotMetadata[] = [
160+
{
161+
Id: 'agent1',
162+
DeveloperName: 'Test_Agent',
163+
BotVersions: {
164+
records: [
165+
{ Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata,
166+
{ Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata,
167+
],
168+
},
169+
} as BotMetadata,
170+
];
171+
172+
const choices = getAgentChoices(agents, 'Inactive');
173+
174+
expect(choices).to.have.lengthOf(1);
175+
expect(choices[0].disabled).to.equal('(Already Inactive)'); // All versions are already inactive
176+
});
177+
178+
it('should disable unsupported agents', () => {
179+
const agents: BotMetadata[] = [
180+
{
181+
Id: 'agent1',
182+
DeveloperName: 'Copilot_for_Salesforce',
183+
BotVersions: {
184+
records: [{ Status: 'Inactive', VersionNumber: 1 } as BotVersionMetadata],
185+
},
186+
} as BotMetadata,
187+
];
188+
189+
const choices = getAgentChoices(agents, 'Active');
190+
191+
expect(choices).to.have.lengthOf(1);
192+
expect(choices[0].disabled).to.equal('(Not Supported)');
193+
});
194+
195+
it('should handle multiple agents with mixed states', () => {
196+
const agents: BotMetadata[] = [
197+
{
198+
Id: 'agent1',
199+
DeveloperName: 'Agent_All_Active',
200+
BotVersions: {
201+
records: [
202+
{ Status: 'Active', VersionNumber: 1 } as BotVersionMetadata,
203+
{ Status: 'Active', VersionNumber: 2 } as BotVersionMetadata,
204+
],
205+
},
206+
} as BotMetadata,
207+
{
208+
Id: 'agent2',
209+
DeveloperName: 'Agent_Mixed',
210+
BotVersions: {
211+
records: [
212+
{ Status: 'Active', VersionNumber: 1 } as BotVersionMetadata,
213+
{ Status: 'Inactive', VersionNumber: 2 } as BotVersionMetadata,
214+
],
215+
},
216+
} as BotMetadata,
217+
];
218+
219+
const choices = getAgentChoices(agents, 'Active');
220+
221+
expect(choices).to.have.lengthOf(2);
222+
expect(choices[0].value.DeveloperName).to.equal('Agent_All_Active');
223+
expect(choices[0].disabled).to.equal('(Already Active)');
224+
expect(choices[1].value.DeveloperName).to.equal('Agent_Mixed');
225+
expect(choices[1].disabled).to.equal(false);
226+
});
227+
});
228+
});

test/nuts/agent.activate.nut.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,39 @@ describe('agent activate/deactivate NUTs', function () {
105105
expect(finalStatus).to.equal('Active');
106106
});
107107

108+
it('should auto-select latest version in JSON mode without version flag', async () => {
109+
// Ensure agent is inactive first
110+
const initialStatus = await getBotStatus();
111+
if (initialStatus === 'Active') {
112+
execCmd(`agent deactivate --api-name ${botApiName} --target-org ${username} --json`, {
113+
ensureExitCode: 0,
114+
});
115+
await sleep(5000);
116+
}
117+
118+
// Activate with --json but no --version flag
119+
const result = execCmd<{ version: number; success: boolean }>(
120+
`agent activate --api-name ${botApiName} --target-org ${username} --json`,
121+
{
122+
ensureExitCode: 0,
123+
}
124+
);
125+
126+
// Parse the JSON result
127+
const jsonResult = result.jsonOutput!.result;
128+
expect(jsonResult?.success).to.equal(true);
129+
expect(jsonResult?.version).to.be.a('number');
130+
131+
// Verify the warning was included in the output
132+
expect(result.shellOutput.stderr).to.include(
133+
'No version specified, automatically selected latest available version'
134+
);
135+
136+
// Verify the BotVersion status is now 'Active'
137+
const finalStatus = await getBotStatus();
138+
expect(finalStatus).to.equal('Active');
139+
});
140+
108141
it('should deactivate the agent', async () => {
109142
// Verify the BotVersion status has 'Active' initial state
110143
const initialStatus = await getBotStatus();

0 commit comments

Comments
 (0)