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
@@ -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