@@ -17,10 +17,11 @@ import { basename, join, sep } from 'node:path';
1717import { MockTestOrgData , TestContext } from '@salesforce/core/testSetup' ;
1818import chai , { assert , expect } from 'chai' ;
1919import { AnyJson , ensureString , getString } from '@salesforce/ts-types' ;
20- import { envVars , Lifecycle , Messages , PollingClient , StatusResult } from '@salesforce/core' ;
20+ import { Connection , envVars , Lifecycle , Messages , PollingClient , StatusResult } from '@salesforce/core' ;
2121import { Duration } from '@salesforce/kit' ;
2222import deepEqualInAnyOrder from 'deep-equal-in-any-order' ;
2323import * as sinon from 'sinon' ;
24+ import fs from 'graceful-fs' ;
2425import {
2526 ComponentSet ,
2627 ComponentStatus ,
@@ -70,15 +71,14 @@ describe('MetadataApiDeploy', () => {
7071
7172 describe ( 'Lifecycle' , ( ) => {
7273 describe ( 'start' , ( ) => {
73- it ( 'should not convert zip, but read from fs' ) ;
74- it ( 'should not mdapiDir, but generate zip buffer from it' ) ;
75-
7674 it ( 'should convert to metadata format and create zip' , async ( ) => {
7775 const components = new ComponentSet ( [ matchingContentFile . COMPONENT ] ) ;
7876 const { operation, convertStub } = await stubMetadataDeploy ( $$ , testOrg , {
7977 components,
8078 } ) ;
8179
80+ expect ( components . getAiAuthoringBundles ( ) . toArray ( ) ) . to . be . empty ;
81+
8282 await operation . start ( ) ;
8383
8484 expect ( convertStub . calledWith ( components , 'metadata' , { type : 'zip' } ) ) . to . be . true ;
@@ -1297,4 +1297,228 @@ describe('MetadataApiDeploy', () => {
12971297 expect ( mdOpts . apiOptions ) . to . have . property ( 'singlePackage' , true ) ;
12981298 } ) ;
12991299 } ) ;
1300+
1301+ describe ( 'AiAuthoringBundle compilation' , ( ) => {
1302+ const aabType = registry . types . aiauthoringbundle ;
1303+ const aabName = 'TestAAB' ;
1304+ const aabContentDir = join ( 'path' , 'to' , 'aiAuthoringBundles' , aabName ) ;
1305+ const agentFileName = `${ aabName } .agent` ;
1306+ const agentContent = 'test agent script content' ;
1307+
1308+ const createAABComponent = ( ) : SourceComponent =>
1309+ SourceComponent . createVirtualComponent (
1310+ {
1311+ name : aabName ,
1312+ type : aabType ,
1313+ xml : join ( aabContentDir , `${ aabName } ${ META_XML_SUFFIX } ` ) ,
1314+ content : aabContentDir ,
1315+ } ,
1316+ [
1317+ {
1318+ dirPath : join ( 'path' , 'to' , 'aiAuthoringBundles' ) ,
1319+ children : [ aabName ] ,
1320+ } ,
1321+ {
1322+ dirPath : aabContentDir ,
1323+ children : [ agentFileName ] ,
1324+ } ,
1325+ ]
1326+ ) ;
1327+
1328+ it ( 'should throw error with correct data when compilation fails' , async ( ) => {
1329+ const aabComponent = createAABComponent ( ) ;
1330+ const components = new ComponentSet ( [ aabComponent ] ) ;
1331+
1332+ // Stub retrieveMaxApiVersion on prototype before getting connection
1333+ $$ . SANDBOX . stub ( Connection . prototype , 'retrieveMaxApiVersion' ) . resolves ( '60.0' ) ;
1334+ const connection = await testOrg . getConnection ( ) ;
1335+
1336+ const readFileStub = $$ . SANDBOX . stub ( fs . promises , 'readFile' ) . resolves ( agentContent ) ;
1337+
1338+ $$ . SANDBOX . stub ( connection , 'getConnectionOptions' ) . returns ( {
1339+ accessToken : 'test-access-token' ,
1340+ instanceUrl : 'https://test.salesforce.com' ,
1341+ } ) ;
1342+
1343+ const compileErrors = [
1344+ { description : 'Syntax error on line 5' , lineStart : 5 , colStart : 10 } ,
1345+ { description : 'Missing token' , lineStart : 8 , colStart : 15 } ,
1346+ ] ;
1347+
1348+ // Configure connection.request stub (already created by TestContext)
1349+ let callCount = 0 ;
1350+ ( connection . request as sinon . SinonStub ) . callsFake ( ( request : { url ?: string } ) => {
1351+ callCount ++ ;
1352+ if ( request . url ?. includes ( 'agentforce/bootstrap/nameduser' ) ) {
1353+ return Promise . resolve ( { access_token : 'named-user-token' } ) ;
1354+ }
1355+ if ( request . url ?. includes ( 'einstein/ai-agent' ) ) {
1356+ return Promise . resolve ( { status : 'failure' as const , errors : compileErrors } ) ;
1357+ }
1358+ // For other requests, return empty object (deploy stub handles its own requests)
1359+ return Promise . resolve ( { } ) ;
1360+ } ) ;
1361+
1362+ const { operation } = await stubMetadataDeploy ( $$ , testOrg , { components } ) ;
1363+
1364+ try {
1365+ await operation . start ( ) ;
1366+ expect . fail ( 'Should have thrown AgentCompilationError' ) ;
1367+ } catch ( error : unknown ) {
1368+ const err = error as { name ?: string ; message ?: string } ;
1369+ expect ( err ) . to . have . property ( 'name' , 'AgentCompilationError' ) ;
1370+ expect ( err . message ) . to . include ( `${ aabName } .agent: Syntax error on line 5 5:10` ) ;
1371+ expect ( err . message ) . to . include ( `${ aabName } .agent: Missing token 8:15` ) ;
1372+ }
1373+
1374+ expect ( readFileStub . calledOnce ) . to . be . true ;
1375+ expect ( callCount ) . to . be . at . least ( 2 ) ;
1376+ } ) ;
1377+
1378+ it ( 'should not throw error when compilation succeeds' , async ( ) => {
1379+ const aabComponent = createAABComponent ( ) ;
1380+ const components = new ComponentSet ( [ aabComponent ] ) ;
1381+
1382+ // Stub retrieveMaxApiVersion on prototype before getting connection
1383+ $$ . SANDBOX . stub ( Connection . prototype , 'retrieveMaxApiVersion' ) . resolves ( '60.0' ) ;
1384+ const connection = await testOrg . getConnection ( ) ;
1385+
1386+ const readFileStub = $$ . SANDBOX . stub ( fs . promises , 'readFile' ) . resolves ( agentContent ) ;
1387+
1388+ $$ . SANDBOX . stub ( connection , 'getConnectionOptions' ) . returns ( {
1389+ accessToken : 'test-access-token' ,
1390+ instanceUrl : 'https://test.salesforce.com' ,
1391+ } ) ;
1392+
1393+ // Configure connection.request stub (already created by TestContext)
1394+ let callCount = 0 ;
1395+ ( connection . request as sinon . SinonStub ) . callsFake ( ( request : { url ?: string } ) => {
1396+ callCount ++ ;
1397+ if ( request . url ?. includes ( 'agentforce/bootstrap/nameduser' ) ) {
1398+ return Promise . resolve ( { access_token : 'named-user-token' } ) ;
1399+ }
1400+ if ( request . url ?. includes ( 'einstein/ai-agent' ) ) {
1401+ return Promise . resolve ( { status : 'success' as const , errors : [ ] } ) ;
1402+ }
1403+ // For other requests, return empty object (deploy stub handles its own requests)
1404+ return Promise . resolve ( { } ) ;
1405+ } ) ;
1406+
1407+ const { operation } = await stubMetadataDeploy ( $$ , testOrg , { components } ) ;
1408+
1409+ // Should not throw
1410+ await operation . start ( ) ;
1411+
1412+ expect ( readFileStub . calledOnce ) . to . be . true ;
1413+ expect ( callCount ) . to . be . at . least ( 2 ) ;
1414+ } ) ;
1415+
1416+ it ( 'should not compile when no AABs present in component set' , async ( ) => {
1417+ const components = new ComponentSet ( [ COMPONENT ] ) ;
1418+
1419+ // Stub retrieveMaxApiVersion on prototype before getting connection
1420+ $$ . SANDBOX . stub ( Connection . prototype , 'retrieveMaxApiVersion' ) . resolves ( '60.0' ) ;
1421+ const connection = await testOrg . getConnection ( ) ;
1422+
1423+ const readFileStub = $$ . SANDBOX . stub ( fs . promises , 'readFile' ) ;
1424+ // Track calls to connection.request to verify compilation wasn't attempted
1425+ const compileCallCount = { count : 0 } ;
1426+ ( connection . request as sinon . SinonStub ) . callsFake ( ( request : { url ?: string } ) => {
1427+ const url = request . url ?? '' ;
1428+ if ( url . includes ( 'einstein/ai-agent' ) || url . includes ( 'agentforce/bootstrap' ) ) {
1429+ compileCallCount . count ++ ;
1430+ }
1431+ // For other requests, return empty object (deploy stub handles its own requests)
1432+ return Promise . resolve ( { } ) ;
1433+ } ) ;
1434+
1435+ const { operation } = await stubMetadataDeploy ( $$ , testOrg , { components } ) ;
1436+
1437+ await operation . start ( ) ;
1438+
1439+ // Verify compilation endpoints were not called
1440+ expect ( readFileStub . called ) . to . be . false ;
1441+ expect ( compileCallCount . count ) . to . equal ( 0 ) ;
1442+ } ) ;
1443+
1444+ it ( 'should handle multiple AABs in parallel' , async ( ) => {
1445+ const aab1 = SourceComponent . createVirtualComponent (
1446+ {
1447+ name : 'AAB1' ,
1448+ type : aabType ,
1449+ xml : join ( 'path' , 'to' , 'aiAuthoringBundles' , 'AAB1' , `AAB1${ META_XML_SUFFIX } ` ) ,
1450+ content : join ( 'path' , 'to' , 'aiAuthoringBundles' , 'AAB1' ) ,
1451+ } ,
1452+ [
1453+ {
1454+ dirPath : join ( 'path' , 'to' , 'aiAuthoringBundles' ) ,
1455+ children : [ 'AAB1' ] ,
1456+ } ,
1457+ {
1458+ dirPath : join ( 'path' , 'to' , 'aiAuthoringBundles' , 'AAB1' ) ,
1459+ children : [ 'AAB1.agent' ] ,
1460+ } ,
1461+ ]
1462+ ) ;
1463+
1464+ const aab2 = SourceComponent . createVirtualComponent (
1465+ {
1466+ name : 'AAB2' ,
1467+ type : aabType ,
1468+ xml : join ( 'path' , 'to' , 'aiAuthoringBundles' , 'AAB2' , `AAB2${ META_XML_SUFFIX } ` ) ,
1469+ content : join ( 'path' , 'to' , 'aiAuthoringBundles' , 'AAB2' ) ,
1470+ } ,
1471+ [
1472+ {
1473+ dirPath : join ( 'path' , 'to' , 'aiAuthoringBundles' ) ,
1474+ children : [ 'AAB2' ] ,
1475+ } ,
1476+ {
1477+ dirPath : join ( 'path' , 'to' , 'aiAuthoringBundles' , 'AAB2' ) ,
1478+ children : [ 'AAB2.agent' ] ,
1479+ } ,
1480+ ]
1481+ ) ;
1482+
1483+ const components = new ComponentSet ( [ aab1 , aab2 ] ) ;
1484+
1485+ // Stub retrieveMaxApiVersion on prototype before getting connection
1486+ $$ . SANDBOX . stub ( Connection . prototype , 'retrieveMaxApiVersion' ) . resolves ( '60.0' ) ;
1487+ const connection = await testOrg . getConnection ( ) ;
1488+
1489+ const readFileStub = $$ . SANDBOX . stub ( fs . promises , 'readFile' ) . resolves ( agentContent ) ;
1490+
1491+ $$ . SANDBOX . stub ( connection , 'getConnectionOptions' ) . returns ( {
1492+ accessToken : 'test-access-token' ,
1493+ instanceUrl : 'https://test.salesforce.com' ,
1494+ } ) ;
1495+
1496+ // Configure connection.request stub (already created by TestContext)
1497+ // Handle multiple AABs: 2 nameduser + 2 compile calls
1498+ let namedUserCallCount = 0 ;
1499+ let compileCallCount = 0 ;
1500+ ( connection . request as sinon . SinonStub ) . callsFake ( ( request : { url ?: string } ) => {
1501+ if ( request . url ?. includes ( 'agentforce/bootstrap/nameduser' ) ) {
1502+ namedUserCallCount ++ ;
1503+ return Promise . resolve ( { access_token : 'named-user-token' } ) ;
1504+ }
1505+ if ( request . url ?. includes ( 'einstein/ai-agent' ) ) {
1506+ compileCallCount ++ ;
1507+ return Promise . resolve ( { status : 'success' as const , errors : [ ] } ) ;
1508+ }
1509+ // For other requests, return empty object (deploy stub handles its own requests)
1510+ return Promise . resolve ( { } ) ;
1511+ } ) ;
1512+
1513+ const { operation } = await stubMetadataDeploy ( $$ , testOrg , { components } ) ;
1514+
1515+ await operation . start ( ) ;
1516+
1517+ // Should read both agent files
1518+ expect ( readFileStub . callCount ) . to . equal ( 2 ) ;
1519+ // Should call compile endpoint twice (once per AAB) and nameduser twice
1520+ expect ( namedUserCallCount ) . to . equal ( 2 ) ;
1521+ expect ( compileCallCount ) . to . equal ( 2 ) ;
1522+ } ) ;
1523+ } ) ;
13001524} ) ;
0 commit comments