Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"command": "agent:publish:authoring-bundle",
"flagAliases": [],
"flagChars": ["n", "o"],
"flags": ["api-name", "api-version", "flags-dir", "json", "target-org"],
"flags": ["api-name", "api-version", "flags-dir", "json", "skip-retrieve", "target-org"],
"plugin": "@salesforce/plugin-agent"
},
{
Expand Down
6 changes: 5 additions & 1 deletion messages/agent.publish.authoring-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Publish an authoring bundle to your org, which results in a new agent or a new v

An authoring bundle is a metadata type (named aiAuthoringBundle) that provides the blueprint for an agent. The metadata type contains two files: the standard metatada XML file and an Agent Script file (extension ".agent") that fully describes the agent using the Agent Script language.

When you publish an authoring bundle to your org, a number of things happen. First, this command validates that the Agent Script file successfully compiles. If there are compilation errors, the command exits and you must fix the Agent Script file to continue. Once the Agent Script file compiles, then it's published to the org, which in turn creates new associated metadata (Bot, BotVersion, GenAiX), or new versions of the metadata if the agent already exists. The new or updated metadata is retrieved back to your DX project, and then the authoring bundle metadata (AiAuthoringBundle) is deployed to your org.
When you publish an authoring bundle to your org, a number of things happen. First, this command validates that the Agent Script file successfully compiles. If there are compilation errors, the command exits and you must fix the Agent Script file to continue. Once the Agent Script file compiles, then it's published to the org, which in turn creates new associated metadata (Bot, BotVersion, GenAiX), or new versions of the metadata if the agent already exists. The new or updated metadata is retrieved back to your DX project; specify the --skip-retrieve flag to skip this step. Finally, the authoring bundle metadata (AiAuthoringBundle) is deployed to your org.

This command uses the API name of the authoring bundle.

Expand All @@ -28,6 +28,10 @@ API name of the authoring bundle you want to publish; if not specified, the comm

API name of the authoring bundle to publish

# flags.skip-retrieve.summary

Don't retrieve the metadata associated with the agent to your DX project.

# error.missingRequiredFlags

Required flag(s) missing: %s.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"@inquirer/prompts": "^7.10.1",
"@oclif/core": "^4",
"@oclif/multi-stage-output": "^0.8.29",
"@salesforce/agents": "^0.21.1",
"@salesforce/agents": "0.21.2-beta.2",
"@salesforce/core": "^8.24.3",
"@salesforce/kit": "^3.2.3",
"@salesforce/sf-plugins-core": "^12.2.6",
Expand Down
20 changes: 15 additions & 5 deletions src/commands/agent/publish/authoring-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export default class AgentPublishAuthoringBundle extends SfCommand<AgentPublishA
char: 'n',
summary: messages.getMessage('flags.api-name.summary'),
}),
'skip-retrieve': Flags.boolean({
summary: messages.getMessage('flags.skip-retrieve.summary'),
}),
};

private static readonly FLAGGABLE_PROMPTS = {
Expand Down Expand Up @@ -92,7 +95,12 @@ export default class AgentPublishAuthoringBundle extends SfCommand<AgentPublishA
mso.goto('Validate Bundle');
const targetOrg = flags['target-org'];
const conn = targetOrg.getConnection(flags['api-version']);
const agent = await Agent.init({ connection: conn, project: this.project!, aabName });
const agent = await Agent.init({
connection: conn,
project: this.project!,
aabName,
skipMetadataRetrieve: flags['skip-retrieve'],
});

// First compile the .agent file to get the Agent JSON
const compileResponse = await agent.compile();
Expand All @@ -103,10 +111,12 @@ export default class AgentPublishAuthoringBundle extends SfCommand<AgentPublishA
}
// Then publish the Agent JSON to create the agent
// Set up lifecycle listeners for retrieve events
Lifecycle.getInstance().on('scopedPreRetrieve', () => {
mso.skipTo('Retrieve Metadata');
return Promise.resolve();
});
if (!flags['skip-retrieve']) {
Lifecycle.getInstance().on('scopedPreRetrieve', () => {
mso.skipTo('Retrieve Metadata');
return Promise.resolve();
});
}
// Set up lifecycle listeners for deploy events
Lifecycle.getInstance().on('scopedPreDeploy', () => {
mso.skipTo('Deploy Metadata');
Expand Down
158 changes: 157 additions & 1 deletion test/nuts/z2.agent.publish.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,78 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { expect } from 'chai';
import { genUniqueString, TestSession } from '@salesforce/cli-plugins-testkit';
import { execCmd } from '@salesforce/cli-plugins-testkit';
import { Connection, Org } from '@salesforce/core';
import type { AgentPublishAuthoringBundleResult } from '../../src/commands/agent/publish/authoring-bundle.js';
import type { AgentGenerateAuthoringBundleResult } from '../../src/commands/agent/generate/authoring-bundle.js';
import { getAgentUsername, getTestSession, getUsername } from './shared-setup.js';

type BotDefinitionWithVersions = {
Id: string;
BotVersions: {
records: Array<{ DeveloperName: string }>;
};
};

const verifyPublishedAgent = async (
botApiName: string,
expectedVersion: string,
connection: Connection
): Promise<void> => {
let botDefinition;
try {
botDefinition = await connection.singleRecordQuery<BotDefinitionWithVersions>(
`SELECT SELECT Id, (SELECT DeveloperName FROM BotVersions LIMIT 10) FROM BotDefinition WHERE DeveloperName = '${botApiName}' LIMIT 1`
);
const botVersion = botDefinition.BotVersions.records[0].DeveloperName;
expect(botVersion).to.equal(expectedVersion);
} catch (error) {
// bot not found
void Promise.reject('Bot not published');
}
};

async function verifyGenAiPlannerBundleExistsOrNot(
projectDir: string,
expectedBundleName: string,
expectedVersion: string,
shouldExist: boolean
): Promise<void> {
const genAiPlannerBundleDir = join(
projectDir,
'force-app',
'main',
'default',
'genAiPlannerBundles',
`${expectedBundleName}_${expectedVersion}`
);

// Verify the genAiPlannerBundles directory exists
expect(existsSync(genAiPlannerBundleDir)).to.equal(shouldExist);

if (shouldExist) {
// Verify the directory contains files
const files = readdirSync(genAiPlannerBundleDir);
expect(files.length).to.be.greaterThan(0);
}
}

describe('agent publish authoring-bundle NUTs', function () {
// Increase timeout for setup since shared setup includes long waits and deployments
this.timeout(30 * 60 * 1000); // 30 minutes

let session: TestSession;
let connection: Connection;
const bundleApiName = genUniqueString('Test_Agent_%s');
before(async function () {
this.timeout(30 * 60 * 1000); // 30 minutes for setup
session = await getTestSession();
const org = await Org.create({ aliasOrUsername: getUsername() });
connection = org.getConnection();
});

it('should publish a new agent (first version)', async function () {
Expand Down Expand Up @@ -73,6 +127,8 @@ describe('agent publish authoring-bundle NUTs', function () {
expect(publishResult?.success).to.be.true;
expect(publishResult?.botDeveloperName).to.be.a('string');
expect(publishResult?.errors).to.be.undefined;
await verifyPublishedAgent(bundleApiName, 'v1', connection);
await verifyGenAiPlannerBundleExistsOrNot(session.project.dir, bundleApiName, 'v1', true);
});

it('should publish a new version of an existing agent', async function () {
Expand All @@ -90,6 +146,30 @@ describe('agent publish authoring-bundle NUTs', function () {
expect(result?.success).to.be.true;
expect(result?.botDeveloperName).to.be.a('string');
expect(result?.errors).to.be.undefined;
await verifyPublishedAgent(bundleApiName, 'v2', connection);
await verifyGenAiPlannerBundleExistsOrNot(session.project.dir, bundleApiName, 'v2', true);
});

it('should publish agent with skip-retrieve flag', async function () {
// Test that the --skip-retrieve flag works correctly
// This flag skips the metadata retrieval step in the publishing process
// Increase timeout to 30 minutes since deployment can take a long time
this.timeout(30 * 60 * 1000); // 30 minutes
// Retry up to 2 times total (1 initial + 1 retries) to handle transient failures
this.retries(1);

const result = execCmd<AgentPublishAuthoringBundleResult>(
`agent publish authoring-bundle --api-name ${bundleApiName} --target-org ${getUsername()} --skip-retrieve --json`,
{ ensureExitCode: 0 }
).jsonOutput?.result;

expect(result).to.be.ok;
expect(result?.success).to.be.true;
expect(result?.botDeveloperName).to.be.a('string');
expect(result?.errors).to.be.undefined;
await verifyPublishedAgent(bundleApiName, 'v3', connection);
// skip-retrieve should not create a new version of the genAiPlannerBundle
await verifyGenAiPlannerBundleExistsOrNot(session.project.dir, bundleApiName, 'v3', false);
});

it('should fail for invalid bundle api-name', async () => {
Expand All @@ -100,4 +180,80 @@ describe('agent publish authoring-bundle NUTs', function () {
{ ensureExitCode: 2 }
);
});

it('should fail when agent script compilation fails', async function () {
// Increase timeout since compilation might take time before failing
this.timeout(30 * 60 * 1000); // 30 minutes

// Try to publish a bundle with invalid script that should fail compilation
execCmd<AgentPublishAuthoringBundleResult>(
`agent publish authoring-bundle --api-name invalid --target-org ${getUsername()} --json`,
{ ensureExitCode: 1 } // Expect failure due to compilation error
);
});

it('should display correct MSO stages during publish process', async function () {
// Increase timeout to 30 minutes since deployment can take a long time
this.timeout(30 * 60 * 1000); // 30 minutes
// Retry up to 2 times total (1 initial + 1 retries) to handle transient failures
this.retries(1);

const specFileName = genUniqueString('agentSpec_%s.yaml');
const specPath = join(session.project.dir, 'specs', specFileName);

// Step 1: Generate an agent spec
execCmd(
`agent generate agent-spec --target-org ${getUsername()} --type customer --role "test agent role" --company-name "Test Company" --company-description "Test Description" --output-file ${specPath}`,
{
ensureExitCode: 0,
}
);

// Step 2: Generate the authoring bundle from the spec
const generateCommand = `agent generate authoring-bundle --spec ${specPath} --name "${bundleApiName}" --api-name ${bundleApiName} --target-org ${getUsername()}`;
execCmd(generateCommand, { ensureExitCode: 0 });

// Step 3: Publish without --json to capture MSO output
const publishCommand = `agent publish authoring-bundle --api-name ${bundleApiName} --target-org ${getUsername()}`;
const result = execCmd(publishCommand, { ensureExitCode: 0 });

// Verify MSO stages are present in output
const output = result.shellOutput.stdout;
expect(output).to.include('Publishing Agent');
expect(output).to.include('✓ Validate Bundle');
expect(output).to.include('✓ Publish Agent');
expect(output).to.include('✓ Retrieve Metadata');
expect(output).to.include('✓ Deploy Metadata');
expect(output).to.include(`Agent Name: ${bundleApiName}`);
});

it('should skip retrieve and deploy stages when using --skip-retrieve flag', async function () {
// Increase timeout to 30 minutes since deployment can take a long time
this.timeout(30 * 60 * 1000); // 30 minutes
// Retry up to 2 times total (1 initial + 1 retries) to handle transient failures
this.retries(1);

const specFileName = genUniqueString('agentSpec_%s.yaml');
const specPath = join(session.project.dir, 'specs', specFileName);

// Step 1: Generate an agent spec
execCmd(
`agent generate agent-spec --target-org ${getUsername()} --type customer --role "test agent role" --company-name "Test Company" --company-description "Test Description" --output-file ${specPath}`,
{
ensureExitCode: 0,
}
);

// Step 2: Generate the authoring bundle from the spec
const generateCommand = `agent generate authoring-bundle --spec ${specPath} --name "${bundleApiName}" --api-name ${bundleApiName} --target-org ${getUsername()}`;
execCmd(generateCommand, { ensureExitCode: 0 });

// Step 3: Publish with --skip-retrieve flag to capture MSO output
const publishCommand = `agent publish authoring-bundle --api-name ${bundleApiName} --target-org ${getUsername()} --skip-retrieve`;
const result = execCmd(publishCommand, { ensureExitCode: 0 });

// Verify MSO stages are present in output
const output = result.shellOutput.stdout;
expect(output).to.include('Retrieve Metadata - Skipped');
});
});
Loading
Loading