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' ;
88import { join , parse } from 'node:path' ;
99import { existsSync } from 'node:fs' ;
1010import { SfCommand , Flags } from '@salesforce/sf-plugins-core' ;
11- import { Messages , SfProject } from '@salesforce/core' ;
11+ import { Messages , SfError , SfProject } from '@salesforce/core' ;
1212import { AgentTest } from '@salesforce/agents' ;
1313import { select , input , confirm , checkbox } from '@inquirer/prompts' ;
1414import { XMLParser } from 'fast-xml-parser' ;
1515import { ComponentSet , ComponentSetBuilder } from '@salesforce/source-deploy-retrieve' ;
1616import { warn } from '@oclif/core/errors' ;
17+ import { ensureArray } from '@salesforce/kit' ;
1718import { theme } from '../../../inquirer-theme.js' ;
1819import yesNoOrCancel from '../../../yes-no-cancel.js' ;
1920
@@ -36,11 +37,6 @@ type TestCase = {
3637 } > ;
3738} ;
3839
39- function castArray < T > ( value : T | T [ ] ) : T [ ] {
40- if ( ! value ) return [ ] ;
41- return Array . isArray ( value ) ? value : [ value ] ;
42- }
43-
4440/**
4541 * Prompts the user for test case information through interactive prompts.
4642 *
@@ -79,10 +75,10 @@ async function promptForTestCase(genAiPlugins: Record<string, string>, genAiFunc
7975 // the actions from the plugin are read from the GenAiPlugin file
8076 let actions : string [ ] = [ ] ;
8177 if ( genAiPlugins [ expectedTopic ] ) {
82- const genAiPluginXml = await readFile ( genAiPlugins [ expectedTopic ] , 'utf-8' ) ;
78+ const genAiPluginXml = await fs . promises . readFile ( genAiPlugins [ expectedTopic ] , 'utf-8' ) ;
8379 const parser = new XMLParser ( ) ;
8480 const parsed = parser . parse ( genAiPluginXml ) as { GenAiPlugin : { genAiFunctions : Array < { functionName : string } > } } ;
85- actions = castArray ( parsed . GenAiPlugin . genAiFunctions ?? [ ] ) . map ( ( f ) => f . functionName ) ;
81+ actions = ensureArray ( parsed . GenAiPlugin . genAiFunctions ?? [ ] ) . map ( ( f ) => f . functionName ) ;
8682 }
8783
8884 const expectedActions = (
@@ -215,7 +211,7 @@ async function promptForCustomEvaluations(): Promise<NonNullable<TestCase['custo
215211 return customEvaluations ;
216212}
217213
218- function getMetadataFilePaths ( cs : ComponentSet , type : string ) : Record < string , string > {
214+ export function getMetadataFilePaths ( cs : ComponentSet , type : string ) : Record < string , string > {
219215 return [ ...cs . filter ( ( component ) => component . type . name === type && component . fullName !== '*' ) ] . reduce <
220216 Record < string , string >
221217 > (
@@ -249,52 +245,103 @@ function getMetadataFilePaths(cs: ComponentSet, type: string): Record<string, st
249245 * - genAiPlugins: Record of plugin names to their file paths
250246 * - genAiFunctions: Array of function names
251247 */
252- async function getPluginsAndFunctions (
248+ export async function getPluginsAndFunctions (
253249 subjectName : string ,
254250 cs : ComponentSet
255251) : Promise < {
256252 genAiPlugins : Record < string , string > ;
257253 genAiFunctions : string [ ] ;
258254} > {
259255 const botVersions = getMetadataFilePaths ( cs , 'Bot' ) ;
260- const genAiPlanners = getMetadataFilePaths ( cs , 'GenAiPlanner' ) ;
256+ let genAiFunctions : string [ ] = [ ] ;
257+ let genAiPlugins : Record < string , string > = { } ;
261258
262259 const parser = new XMLParser ( ) ;
263- const botVersionXml = await readFile ( botVersions [ subjectName ] , 'utf-8' ) ;
260+ const botVersionXml = await fs . promises . readFile ( botVersions [ subjectName ] , 'utf-8' ) ;
264261 const parsedBotVersion = parser . parse ( botVersionXml ) as {
265262 BotVersion : { conversationDefinitionPlanners : { genAiPlannerName : string } } ;
266263 } ;
267264
268- const plannerXml = await readFile (
269- genAiPlanners [ parsedBotVersion . BotVersion . conversationDefinitionPlanners . genAiPlannerName ?? subjectName ] ,
270- 'utf-8'
271- ) ;
272- const parsedPlanner = parser . parse ( plannerXml ) as {
273- GenAiPlanner : {
274- genAiPlugins : Array < { genAiPluginName : string } > ;
275- genAiFunctions : Array < { genAiFunctionName : string } > ;
265+ try {
266+ // if the users still have genAiPlanner, not the bundle, we can work with that
267+ const genAiPlanners = getMetadataFilePaths ( cs , 'GenAiPlanner' ) ;
268+
269+ const plannerXml = await fs . promises . readFile (
270+ genAiPlanners [ parsedBotVersion . BotVersion . conversationDefinitionPlanners . genAiPlannerName ?? subjectName ] ,
271+ 'utf-8'
272+ ) ;
273+ const parsedPlanner = parser . parse ( plannerXml ) as {
274+ GenAiPlanner : {
275+ genAiPlugins : Array < { genAiPluginName : string } > ;
276+ genAiFunctions : Array < { genAiFunctionName : string } > ;
277+ } ;
276278 } ;
277- } ;
279+ genAiFunctions = ensureArray ( parsedPlanner . GenAiPlanner . genAiFunctions ) . map (
280+ ( { genAiFunctionName } ) => genAiFunctionName
281+ ) ;
278282
279- const genAiFunctions = castArray ( parsedPlanner . GenAiPlanner . genAiFunctions ) . map (
280- ( { genAiFunctionName } ) => genAiFunctionName
281- ) ;
283+ genAiPlugins = ensureArray ( parsedPlanner . GenAiPlanner . genAiPlugins ) . reduce (
284+ ( acc , { genAiPluginName } ) => ( {
285+ ...acc ,
286+ [ genAiPluginName ] : cs . getComponentFilenamesByNameAndType ( {
287+ fullName : genAiPluginName ,
288+ type : 'GenAiPlugin' ,
289+ } ) [ 0 ] ,
290+ } ) ,
291+ { }
292+ ) ;
293+ } catch ( e ) {
294+ // do nothing, we were trying to read the old genAiPlanner
295+ }
282296
283- const genAiPlugins = castArray ( parsedPlanner . GenAiPlanner . genAiPlugins ) . reduce (
284- ( acc , { genAiPluginName } ) => ( {
285- ...acc ,
286- [ genAiPluginName ] : cs . getComponentFilenamesByNameAndType ( {
287- fullName : genAiPluginName ,
288- type : 'GenAiPlugin' ,
289- } ) [ 0 ] ,
290- } ) ,
291- { }
292- ) ;
297+ try {
298+ if ( genAiFunctions . length === 0 && Object . keys ( genAiPlugins ) . length === 0 ) {
299+ // if we've already found functions and plugins from the genAiPlanner, don't try to read the bundle
300+ const genAiPlannerBundles = getMetadataFilePaths ( cs , 'GenAiPlannerBundle' ) ;
301+ const plannerBundleXml = await fs . promises . readFile (
302+ genAiPlannerBundles [ parsedBotVersion . BotVersion . conversationDefinitionPlanners . genAiPlannerName ?? subjectName ] ,
303+ 'utf-8'
304+ ) ;
305+ const parsedPlannerBundle = parser . parse ( plannerBundleXml ) as {
306+ GenAiPlannerBundle : {
307+ genAiPlugins : Array <
308+ | {
309+ genAiPluginName : string ;
310+ }
311+ | { genAiPluginName : string ; genAiCustomizedPlugin : { genAiFunctions : Array < { functionName : string } > } }
312+ > ;
313+ } ;
314+ } ;
315+ genAiFunctions = ensureArray ( parsedPlannerBundle . GenAiPlannerBundle . genAiPlugins )
316+ . filter ( ( f ) => 'genAiCustomizedPlugin' in f )
317+ . map (
318+ ( { genAiCustomizedPlugin } ) =>
319+ genAiCustomizedPlugin . genAiFunctions . find ( ( plugin ) => plugin . functionName !== '' ) ! . functionName
320+ ) ;
321+
322+ genAiPlugins = ensureArray ( parsedPlannerBundle . GenAiPlannerBundle . genAiPlugins ) . reduce (
323+ ( acc , { genAiPluginName } ) => ( {
324+ ...acc ,
325+ [ genAiPluginName ] : cs . getComponentFilenamesByNameAndType ( {
326+ fullName : genAiPluginName ,
327+ type : 'GenAiPlugin' ,
328+ } ) [ 0 ] ,
329+ } ) ,
330+ { }
331+ ) ;
332+ }
333+ } catch ( e ) {
334+ throw new SfError (
335+ `Error parsing GenAiPlannerBundle: ${
336+ parsedBotVersion . BotVersion . conversationDefinitionPlanners . genAiPlannerName ?? subjectName
337+ } `
338+ ) ;
339+ }
293340
294341 return { genAiPlugins, genAiFunctions } ;
295342}
296343
297- function ensureYamlExtension ( filePath : string ) : string {
344+ export function ensureYamlExtension ( filePath : string ) : string {
298345 const parsedPath = parse ( filePath ) ;
299346
300347 if ( parsedPath . ext === '.yaml' || parsedPath . ext === '.yml' ) return filePath ;
@@ -391,7 +438,7 @@ export default class AgentGenerateTestSpec extends SfCommand<void> {
391438
392439 const cs = await ComponentSetBuilder . build ( {
393440 metadata : {
394- metadataEntries : [ 'GenAiPlanner' , 'GenAiPlugin' , 'Bot' ] ,
441+ metadataEntries : [ 'GenAiPlanner' , 'GenAiPlannerBundle' , ' GenAiPlugin', 'Bot' ] ,
395442 directoryPaths,
396443 } ,
397444 } ) ;
0 commit comments