Skip to content

Commit e9aad6b

Browse files
authored
Merge pull request #174 from salesforcecli/wr/fixGAPB
fix: read from genAiPlanner/Bundle W-19057025
2 parents 1d63bf3 + 8260d54 commit e9aad6b

File tree

5 files changed

+1751
-1232
lines changed

5 files changed

+1751
-1232
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
"@oclif/core": "^4",
1111
"@oclif/multi-stage-output": "^0.8.17",
1212
"@salesforce/agents": "0.15.3",
13-
"@salesforce/core": "^8.13.0",
13+
"@salesforce/core": "^8.18.3",
1414
"@salesforce/kit": "^3.2.3",
1515
"@salesforce/sf-plugins-core": "^12.2.0",
16-
"@salesforce/source-deploy-retrieve": "^12.19.3",
16+
"@salesforce/source-deploy-retrieve": "^12.21.4",
1717
"@salesforce/types": "^1.3.0",
1818
"ansis": "^3.3.2",
1919
"fast-xml-parser": "^4.5.1",
@@ -36,6 +36,7 @@
3636
"eslint-plugin-react": "^7.34.3",
3737
"eslint-plugin-react-hooks": "^4.6.2",
3838
"eslint-plugin-sf-plugin": "^1.20.20",
39+
"esmock": "^2.7.1",
3940
"oclif": "^4.17.44",
4041
"ts-node": "^10.9.2",
4142
"typescript": "^5.5.4"

src/commands/agent/generate/test-spec.ts

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import { readFile } from 'node:fs/promises';
7+
import * as fs from 'node:fs';
88
import { join, parse } from 'node:path';
99
import { existsSync } from 'node:fs';
1010
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
11-
import { Messages, SfProject } from '@salesforce/core';
11+
import { Messages, SfError, SfProject } from '@salesforce/core';
1212
import { AgentTest } from '@salesforce/agents';
1313
import { select, input, confirm, checkbox } from '@inquirer/prompts';
1414
import { XMLParser } from 'fast-xml-parser';
1515
import { ComponentSet, ComponentSetBuilder } from '@salesforce/source-deploy-retrieve';
1616
import { warn } from '@oclif/core/errors';
17+
import { ensureArray } from '@salesforce/kit';
1718
import { theme } from '../../../inquirer-theme.js';
1819
import yesNoOrCancel from '../../../yes-no-cancel.js';
1920

@@ -27,11 +28,6 @@ type TestCase = {
2728
expectedOutcome: string;
2829
};
2930

30-
function castArray<T>(value: T | T[]): T[] {
31-
if (!value) return [];
32-
return Array.isArray(value) ? value : [value];
33-
}
34-
3531
/**
3632
* Prompts the user for test case information through interactive prompts.
3733
*
@@ -68,10 +64,10 @@ async function promptForTestCase(genAiPlugins: Record<string, string>, genAiFunc
6864
// the actions from the plugin are read from the GenAiPlugin file
6965
let actions: string[] = [];
7066
if (genAiPlugins[expectedTopic]) {
71-
const genAiPluginXml = await readFile(genAiPlugins[expectedTopic], 'utf-8');
67+
const genAiPluginXml = await fs.promises.readFile(genAiPlugins[expectedTopic], 'utf-8');
7268
const parser = new XMLParser();
7369
const parsed = parser.parse(genAiPluginXml) as { GenAiPlugin: { genAiFunctions: Array<{ functionName: string }> } };
74-
actions = castArray(parsed.GenAiPlugin.genAiFunctions ?? []).map((f) => f.functionName);
70+
actions = ensureArray(parsed.GenAiPlugin.genAiFunctions ?? []).map((f) => f.functionName);
7571
}
7672

7773
const expectedActions = (
@@ -116,7 +112,7 @@ async function promptForTestCase(genAiPlugins: Record<string, string>, genAiFunc
116112
};
117113
}
118114

119-
function getMetadataFilePaths(cs: ComponentSet, type: string): Record<string, string> {
115+
export function getMetadataFilePaths(cs: ComponentSet, type: string): Record<string, string> {
120116
return [...cs.filter((component) => component.type.name === type && component.fullName !== '*')].reduce<
121117
Record<string, string>
122118
>(
@@ -150,52 +146,103 @@ function getMetadataFilePaths(cs: ComponentSet, type: string): Record<string, st
150146
* - genAiPlugins: Record of plugin names to their file paths
151147
* - genAiFunctions: Array of function names
152148
*/
153-
async function getPluginsAndFunctions(
149+
export async function getPluginsAndFunctions(
154150
subjectName: string,
155151
cs: ComponentSet
156152
): Promise<{
157153
genAiPlugins: Record<string, string>;
158154
genAiFunctions: string[];
159155
}> {
160156
const botVersions = getMetadataFilePaths(cs, 'Bot');
161-
const genAiPlanners = getMetadataFilePaths(cs, 'GenAiPlanner');
157+
let genAiFunctions: string[] = [];
158+
let genAiPlugins: Record<string, string> = {};
162159

163160
const parser = new XMLParser();
164-
const botVersionXml = await readFile(botVersions[subjectName], 'utf-8');
161+
const botVersionXml = await fs.promises.readFile(botVersions[subjectName], 'utf-8');
165162
const parsedBotVersion = parser.parse(botVersionXml) as {
166163
BotVersion: { conversationDefinitionPlanners: { genAiPlannerName: string } };
167164
};
168165

169-
const plannerXml = await readFile(
170-
genAiPlanners[parsedBotVersion.BotVersion.conversationDefinitionPlanners.genAiPlannerName ?? subjectName],
171-
'utf-8'
172-
);
173-
const parsedPlanner = parser.parse(plannerXml) as {
174-
GenAiPlanner: {
175-
genAiPlugins: Array<{ genAiPluginName: string }>;
176-
genAiFunctions: Array<{ genAiFunctionName: string }>;
166+
try {
167+
// if the users still have genAiPlanner, not the bundle, we can work with that
168+
const genAiPlanners = getMetadataFilePaths(cs, 'GenAiPlanner');
169+
170+
const plannerXml = await fs.promises.readFile(
171+
genAiPlanners[parsedBotVersion.BotVersion.conversationDefinitionPlanners.genAiPlannerName ?? subjectName],
172+
'utf-8'
173+
);
174+
const parsedPlanner = parser.parse(plannerXml) as {
175+
GenAiPlanner: {
176+
genAiPlugins: Array<{ genAiPluginName: string }>;
177+
genAiFunctions: Array<{ genAiFunctionName: string }>;
178+
};
177179
};
178-
};
180+
genAiFunctions = ensureArray(parsedPlanner.GenAiPlanner.genAiFunctions).map(
181+
({ genAiFunctionName }) => genAiFunctionName
182+
);
179183

180-
const genAiFunctions = castArray(parsedPlanner.GenAiPlanner.genAiFunctions).map(
181-
({ genAiFunctionName }) => genAiFunctionName
182-
);
184+
genAiPlugins = ensureArray(parsedPlanner.GenAiPlanner.genAiPlugins).reduce(
185+
(acc, { genAiPluginName }) => ({
186+
...acc,
187+
[genAiPluginName]: cs.getComponentFilenamesByNameAndType({
188+
fullName: genAiPluginName,
189+
type: 'GenAiPlugin',
190+
})[0],
191+
}),
192+
{}
193+
);
194+
} catch (e) {
195+
// do nothing, we were trying to read the old genAiPlanner
196+
}
183197

184-
const genAiPlugins = castArray(parsedPlanner.GenAiPlanner.genAiPlugins).reduce(
185-
(acc, { genAiPluginName }) => ({
186-
...acc,
187-
[genAiPluginName]: cs.getComponentFilenamesByNameAndType({
188-
fullName: genAiPluginName,
189-
type: 'GenAiPlugin',
190-
})[0],
191-
}),
192-
{}
193-
);
198+
try {
199+
if (genAiFunctions.length === 0 && Object.keys(genAiPlugins).length === 0) {
200+
// if we've already found functions and plugins from the genAiPlanner, don't try to read the bundle
201+
const genAiPlannerBundles = getMetadataFilePaths(cs, 'GenAiPlannerBundle');
202+
const plannerBundleXml = await fs.promises.readFile(
203+
genAiPlannerBundles[parsedBotVersion.BotVersion.conversationDefinitionPlanners.genAiPlannerName ?? subjectName],
204+
'utf-8'
205+
);
206+
const parsedPlannerBundle = parser.parse(plannerBundleXml) as {
207+
GenAiPlannerBundle: {
208+
genAiPlugins: Array<
209+
| {
210+
genAiPluginName: string;
211+
}
212+
| { genAiPluginName: string; genAiCustomizedPlugin: { genAiFunctions: Array<{ functionName: string }> } }
213+
>;
214+
};
215+
};
216+
genAiFunctions = ensureArray(parsedPlannerBundle.GenAiPlannerBundle.genAiPlugins)
217+
.filter((f) => 'genAiCustomizedPlugin' in f)
218+
.map(
219+
({ genAiCustomizedPlugin }) =>
220+
genAiCustomizedPlugin.genAiFunctions.find((plugin) => plugin.functionName !== '')!.functionName
221+
);
222+
223+
genAiPlugins = ensureArray(parsedPlannerBundle.GenAiPlannerBundle.genAiPlugins).reduce(
224+
(acc, { genAiPluginName }) => ({
225+
...acc,
226+
[genAiPluginName]: cs.getComponentFilenamesByNameAndType({
227+
fullName: genAiPluginName,
228+
type: 'GenAiPlugin',
229+
})[0],
230+
}),
231+
{}
232+
);
233+
}
234+
} catch (e) {
235+
throw new SfError(
236+
`Error parsing GenAiPlannerBundle: ${
237+
parsedBotVersion.BotVersion.conversationDefinitionPlanners.genAiPlannerName ?? subjectName
238+
}`
239+
);
240+
}
194241

195242
return { genAiPlugins, genAiFunctions };
196243
}
197244

198-
function ensureYamlExtension(filePath: string): string {
245+
export function ensureYamlExtension(filePath: string): string {
199246
const parsedPath = parse(filePath);
200247

201248
if (parsedPath.ext === '.yaml' || parsedPath.ext === '.yml') return filePath;
@@ -292,7 +339,7 @@ export default class AgentGenerateTestSpec extends SfCommand<void> {
292339

293340
const cs = await ComponentSetBuilder.build({
294341
metadata: {
295-
metadataEntries: ['GenAiPlanner', 'GenAiPlugin', 'Bot'],
342+
metadataEntries: ['GenAiPlanner', 'GenAiPlannerBundle', 'GenAiPlugin', 'Bot'],
296343
directoryPaths,
297344
},
298345
});

0 commit comments

Comments
 (0)