Skip to content

Commit 6614605

Browse files
authored
Merge pull request #113 from salesforcecli/ew/validate-bot-type
W-18022410 Validate bot type
2 parents 43d235d + bf805fc commit 6614605

20 files changed

+1237
-11
lines changed

messages/agent.generate.template.md

+20
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,23 @@ Version of the agent (BotVersion).
2323
# flags.agent-file.summary
2424

2525
Path to an agent (Bot) metadata file.
26+
27+
# error.invalid-agent-file
28+
29+
Invalid Agent file. Must be a Bot metadata file. Example: force-app/main/default/bots/MyBot/MyBot.bot-meta.xml
30+
31+
# error.no-entry-dialog
32+
33+
No entryDialog found in BotVersion file.
34+
35+
# error.invalid-bot-type
36+
37+
The 'type' attribute of this Bot metadata component XML file can't have a value of 'Bot', which indicates that it's an Einstein Bot and not an agent: %s.
38+
39+
# error.no-label
40+
41+
No label found in Agent (Bot) file: %s.
42+
43+
# error.no-ml-domain
44+
45+
No botMlDomain found in Agent (Bot) file: %s.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
"prepack": "sf-prepack",
105105
"prepare": "sf-install",
106106
"test": "wireit",
107-
"test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel",
107+
"test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --reporter-options maxDiffSize=15000",
108108
"test:only": "wireit",
109109
"version": "oclif readme"
110110
},

src/commands/agent/generate/template.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ import type {
2020
ConversationVariable,
2121
} from '@salesforce/types/metadata';
2222

23-
type GenAiPlannerExt = {
23+
export type GenAiPlannerExt = {
2424
GenAiPlanner: GenAiPlanner & { botTemplate?: string };
2525
};
2626

27-
type BotTemplateExt = {
27+
export type BotTemplateExt = {
2828
'?xml': { '@_version': '1.0'; '@_encoding': 'UTF-8' };
2929
BotTemplate: Omit<BotTemplate, 'botDialogGroups' | 'conversationGoals' | 'conversationVariables'> & {
3030
agentType?: string;
@@ -75,9 +75,7 @@ export default class AgentGenerateTemplate extends SfCommand<AgentGenerateTempla
7575
const { 'agent-file': agentFile, 'agent-version': botVersion } = flags;
7676

7777
if (!agentFile.endsWith('.bot-meta.xml')) {
78-
throw new SfError(
79-
'Invalid Agent file. Must be a Bot metadata file. Example: force-app/main/default/bots/MyBot.bot-meta.xml'
80-
);
78+
throw new SfError(messages.getMessage('error.invalid-agent-file'));
8179
}
8280

8381
const parser = new XMLParser({ ignoreAttributes: false });
@@ -137,12 +135,15 @@ const convertBotToBotTemplate = (
137135
// This will be added to the BotTemplate
138136
const entryDialogJson = botVersionJson.BotVersion.botDialogs.find((dialog) => dialog.developerName === entryDialog);
139137

140-
if (!entryDialogJson) throw new SfError('No entryDialog found in BotVersion file');
138+
if (!entryDialogJson) throw new SfError(messages.getMessage('error.no-entry-dialog'));
141139
// TODO: Test this on a newer org. I had to have this renamed.
142140
entryDialogJson.label = entryDialog;
143141

144-
if (!bot.Bot.label) throw new SfError(`No label found in Agent (Bot) file: ${botFilePath}`);
145-
if (!bot.Bot.botMlDomain) throw new SfError(`No botMlDomain found in Agent (Bot) file: ${botFilePath}`);
142+
// Validate the Bot file to ensure successful Agent creation from a BotTemplate
143+
if (bot.Bot.type === 'Bot') throw new SfError(messages.getMessage('error.invalid-bot-type', [botFilePath]));
144+
if (!bot.Bot.label) throw new SfError(messages.getMessage('error.no-label', [botFilePath]));
145+
if (!bot.Bot.botMlDomain) throw new SfError(messages.getMessage('error.no-ml-domain', [botFilePath]));
146+
146147
const masterLabel = bot.Bot.label;
147148
const mlDomain = bot.Bot.botMlDomain;
148149

src/commands/agent/preview.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { resolve } from 'node:path';
8+
import { resolve, join } from 'node:path';
99
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
1010
import { Messages, SfError } from '@salesforce/core';
1111
import React from 'react';
@@ -161,7 +161,7 @@ export const resolveOutputDir = async (outputDir: string | undefined): Promise<s
161161
if (response) {
162162
const getDir = await input({
163163
message: 'Enter the output directory',
164-
default: env.getString('SF_AGENT_PREVIEW_OUTPUT_DIR', 'temp/agent-preview'),
164+
default: env.getString('SF_AGENT_PREVIEW_OUTPUT_DIR', join('temp', 'agent-preview')),
165165
required: true,
166166
});
167167

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import { join, resolve } from 'node:path';
8+
import { readFileSync } from 'node:fs';
9+
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
10+
import { XMLParser } from 'fast-xml-parser';
11+
import { expect } from 'chai';
12+
import {
13+
AgentGenerateTemplateResult,
14+
BotTemplateExt,
15+
GenAiPlannerExt,
16+
} from '../../../../src/commands/agent/generate/template.js';
17+
18+
describe('agent generate template NUTs', () => {
19+
let session: TestSession;
20+
21+
before(async () => {
22+
session = await TestSession.create({
23+
devhubAuthStrategy: 'NONE',
24+
project: { sourceDir: join('test', 'mock-projects', 'agent-generate-template') },
25+
});
26+
});
27+
28+
after(async () => {
29+
await session?.clean();
30+
});
31+
32+
it('throws an error if Bot "type" is equal to "Bot"', async () => {
33+
const agentVersion = 1;
34+
const agentFile = join('force-app', 'main', 'default', 'bots', 'Local_Info_Agent', 'Local_Info_Agent.bot-meta.xml');
35+
const command = `agent generate template --agent-version ${agentVersion} --agent-file "${agentFile}" --json`;
36+
const output = execCmd<AgentGenerateTemplateResult>(command, {
37+
ensureExitCode: 1,
38+
}).jsonOutput;
39+
40+
expect(output?.message).to.include(
41+
"The 'type' attribute of this Bot metadata component XML file can't have a value of 'Bot'"
42+
);
43+
});
44+
45+
it('Converts an Agent into an BotTemplate and GenAiPlanner', async () => {
46+
const agentVersion = 1;
47+
const agentFile = join(
48+
'force-app',
49+
'main',
50+
'default',
51+
'bots',
52+
'Guest_Experience_Agent',
53+
'Guest_Experience_Agent.bot-meta.xml'
54+
);
55+
const command = `agent generate template --agent-version ${agentVersion} --agent-file "${agentFile}" --json`;
56+
const output = execCmd<AgentGenerateTemplateResult>(command, {
57+
ensureExitCode: 0,
58+
}).jsonOutput;
59+
60+
const botTemplateFilePath = join(
61+
'force-app',
62+
'main',
63+
'default',
64+
'botTemplates',
65+
'Guest_Experience_Agent_v1_Template.botTemplate-meta.xml'
66+
);
67+
const genAiPlannerFilePath = join(
68+
'force-app',
69+
'main',
70+
'default',
71+
'genAiPlanners',
72+
'Guest_Experience_Agent_v1_Template.genAiPlanner-meta.xml'
73+
);
74+
75+
const generatedBotTemplateFilePath = resolve(session.project.dir, botTemplateFilePath);
76+
const generatedGenAiPlannerFilePath = resolve(session.project.dir, genAiPlannerFilePath);
77+
// Ensure it returns the paths to the generated files
78+
expect(output?.result.botTemplatePath).to.equal(generatedBotTemplateFilePath);
79+
expect(output?.result.genAiPlannerPath).to.equal(generatedGenAiPlannerFilePath);
80+
81+
// Compare generated files with mock files
82+
const mockBotTemplateFilePath = join(
83+
'test',
84+
'mock-projects',
85+
'agent-generate-template',
86+
'MOCK-XML',
87+
botTemplateFilePath
88+
);
89+
const mockGenAiPlannerFilePath = join(
90+
'test',
91+
'mock-projects',
92+
'agent-generate-template',
93+
'MOCK-XML',
94+
genAiPlannerFilePath
95+
);
96+
97+
const parser = new XMLParser({ ignoreAttributes: false });
98+
99+
// read both files and compare them
100+
const generatedBotTemplateFile = parser.parse(
101+
readFileSync(generatedBotTemplateFilePath, 'utf-8')
102+
) as BotTemplateExt;
103+
const mockBotTemplateFile = parser.parse(readFileSync(mockBotTemplateFilePath, 'utf-8')) as BotTemplateExt;
104+
expect(generatedBotTemplateFile).to.deep.equal(mockBotTemplateFile);
105+
106+
const generatedGenAiPlannerFile = parser.parse(
107+
readFileSync(generatedGenAiPlannerFilePath, 'utf-8')
108+
) as GenAiPlannerExt;
109+
const mockGenAiPlannerFile = parser.parse(readFileSync(mockGenAiPlannerFilePath, 'utf-8')) as GenAiPlannerExt;
110+
expect(generatedGenAiPlannerFile).to.deep.equal(mockGenAiPlannerFile);
111+
});
112+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status
2+
# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
3+
#
4+
5+
package.xml
6+
7+
# LWC configuration files
8+
**/jsconfig.json
9+
**/.eslintrc.json
10+
11+
# LWC Jest
12+
**/__tests__/**

0 commit comments

Comments
 (0)