Skip to content
Open
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
12 changes: 12 additions & 0 deletions messages/agent.generate.authoring-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,15 @@ Canceled authoring bundle generation.
# warning.noSpecDir

No agent spec directory found at %s.

# error.jsonRequiresName

When using --json, you must specify --name.

# error.jsonRequiresSpecOrNoSpec

When using --json, you must specify either --spec or --no-spec.

# error.jsonAabExists

An authoring bundle with the API name "%s" already exists in the project. Use --force-overwrite to overwrite it, or specify a different --api-name.
4 changes: 4 additions & 0 deletions messages/agent.test.run.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ Number of minutes to wait for the command to complete and display results to the
- Start an agent test and write the JSON-formatted results into a directory called "test-results":

<%= config.bin %> <%= command.id %> --api-name Resort_Manager_Test --wait 10 --output-dir ./test-results --result-format json

# error.missingRequiredFlags

When using --json, you must specify the required flag(s): %s.
49 changes: 40 additions & 9 deletions src/commands/agent/generate/authoring-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ async function resolveUniqueBundle(
return resolveUniqueBundle(baseOutputDir, false);
}

function resolveNameAndApiNameForJson(
flags: { name: string; 'api-name'?: string; 'force-overwrite'?: boolean },
baseOutputDir: string
): { name: string; bundleApiName: string } {
const name = flags.name;
const bundleApiName = flags['api-name'] ?? generateApiName(name);
const bundleDir = join(baseOutputDir, 'aiAuthoringBundles', bundleApiName);
if (existsSync(bundleDir) && !flags['force-overwrite']) {
throw messages.createError('error.jsonAabExists', [bundleApiName]);
}
return { name, bundleApiName };
}

export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerateAuthoringBundleResult> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
Expand Down Expand Up @@ -138,6 +151,7 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
}),
};

// eslint-disable-next-line complexity -- run() branches on json vs interactive, spec source, and name resolution
public async run(): Promise<AgentGenerateAuthoringBundleResult> {
const { flags } = await this.parse(AgentGenerateAuthoringBundle);
const { 'output-dir': outputDir } = flags;
Expand All @@ -146,6 +160,16 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
throw new SfError(messages.getMessage('error.specAndNoSpec'));
}

const jsonMode = this.jsonEnabled();
if (jsonMode) {
if (!flags.name) {
throw messages.createError('error.jsonRequiresName');
}
if (flags.spec === undefined && !flags['no-spec']) {
throw messages.createError('error.jsonRequiresSpecOrNoSpec');
}
}

// Resolve spec: --no-spec => undefined, --spec <path> => path, missing => wizard prompts
let spec: string | undefined;
if (flags['no-spec']) {
Expand Down Expand Up @@ -207,19 +231,26 @@ export default class AgentGenerateAuthoringBundle extends SfCommand<AgentGenerat
const defaultOutputDir = join(this.project!.getDefaultPackage().fullPath, 'main', 'default');
const baseOutputDir = outputDir ?? defaultOutputDir;

const resolved = await resolveUniqueBundle(
baseOutputDir,
flags['force-overwrite'] ?? false,
flags['name'],
flags['api-name']
);

let resolved: { name: string; bundleApiName: string } | undefined;
if (jsonMode) {
resolved = resolveNameAndApiNameForJson(
{ name: flags.name!, 'api-name': flags['api-name'], 'force-overwrite': flags['force-overwrite'] },
baseOutputDir
);
} else {
const unique = await resolveUniqueBundle(
baseOutputDir,
flags['force-overwrite'] ?? false,
flags['name'],
flags['api-name']
);
resolved = unique ? { name: unique.name, bundleApiName: unique.apiName } : undefined;
}
if (!resolved) {
this.log(messages.getMessage('info.cancel'));
return { agentPath: '', metaXmlPath: '', outputDir: '' };
}

const { name, apiName: bundleApiName } = resolved;
const { name, bundleApiName } = resolved;

try {
const targetOutputDir = join(baseOutputDir, 'aiAuthoringBundles', bundleApiName);
Expand Down
5 changes: 5 additions & 0 deletions src/commands/agent/publish/authoring-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export default class AgentPublishAuthoringBundle extends SfCommand<AgentPublishA

public async run(): Promise<AgentPublishAuthoringBundleResult> {
const { flags } = await this.parse(AgentPublishAuthoringBundle);

if (this.jsonEnabled() && !flags['api-name']) {
throw messages.createError('error.missingRequiredFlags', ['api-name']);
}

// If api-name is not provided, prompt user to select an .agent file from the project and extract the API name from it
const aabName =
flags['api-name'] ??
Expand Down
5 changes: 5 additions & 0 deletions src/commands/agent/test/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export default class AgentTestRun extends SfCommand<AgentTestRunResult> {
public async run(): Promise<AgentTestRunResult> {
const { flags } = await this.parse(AgentTestRun);
const connection = flags['target-org'].getConnection(flags['api-version']);

if (this.jsonEnabled() && !flags['api-name']) {
throw messages.createError('error.missingRequiredFlags', ['api-name']);
}

const apiName =
flags['api-name'] ?? (await promptForAiEvaluationDefinitionApiName(FLAGGABLE_PROMPTS['api-name'], connection));

Expand Down
5 changes: 5 additions & 0 deletions src/commands/agent/validate/authoring-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export default class AgentValidateAuthoringBundle extends SfCommand<AgentValidat

public async run(): Promise<AgentValidateAuthoringBundleResult> {
const { flags } = await this.parse(AgentValidateAuthoringBundle);

if (this.jsonEnabled() && !flags['api-name']) {
throw messages.createError('error.missingRequiredFlags', ['api-name']);
}

// If api-name is not provided, prompt user to select an .agent file from the project and extract the API name from it
const aabName =
flags['api-name'] ??
Expand Down
112 changes: 112 additions & 0 deletions test/commands/agent/generate/authoring-bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,116 @@ describe('agent generate authoring-bundle', () => {
expect(result.agentPath).to.include('BrandNewAgent.agent');
});
});

describe('when --json is used', () => {
it('should throw when --name is not specified', async () => {
try {
await AgentGenerateAuthoringBundle.run([
'--json',
'--no-spec',
'--api-name',
'MyAgent',
'--target-org',
'test@org.com',
]);
expect.fail('Expected error');
} catch (error) {
expect((error as Error).message).to.include('you must specify --name');
}
});

it('should throw when neither --spec nor --no-spec is specified', async () => {
try {
await AgentGenerateAuthoringBundle.run([
'--json',
'--name',
'My Agent',
'--api-name',
'MyAgent',
'--target-org',
'test@org.com',
]);
expect.fail('Expected error');
} catch (error) {
expect((error as Error).message).to.include('you must specify either --spec or --no-spec');
}
});

it('should throw when existing AAB matches api-name and --force-overwrite is not set', async () => {
const EXISTING_BUNDLE_API_NAME = 'Willie_Resort_Manager';
try {
await AgentGenerateAuthoringBundle.run([
'--json',
'--no-spec',
'--name',
'Willie Resort Manager',
'--api-name',
EXISTING_BUNDLE_API_NAME,
'--target-org',
'test@org.com',
]);
expect.fail('Expected error');
} catch (error) {
expect((error as Error).message).to.include(EXISTING_BUNDLE_API_NAME);
expect((error as Error).message).to.include('--force-overwrite');
}
});

it('should use generateApiName(name) as api-name when --api-name is omitted and not prompt', async () => {
const result = await AgentGenerateAuthoringBundle.run([
'--json',
'--no-spec',
'--name',
'My Custom Agent',
'--target-org',
'test@org.com',
]);

expect(inputStub.called).to.be.false;
const expectedApiName = generateApiName('My Custom Agent');
expect(result.outputDir).to.include(expectedApiName);
expect(createAuthoringBundleStub.calledOnce).to.be.true;
const callArgs = createAuthoringBundleStub.firstCall.args[0] as CreateAuthoringBundleArgs;
expect(callArgs.bundleApiName).to.equal(expectedApiName);
expect(callArgs.agentSpec.name).to.equal('My Custom Agent');
});

it('should succeed with --json when all required flags provided and no existing AAB', async () => {
const result = await AgentGenerateAuthoringBundle.run([
'--json',
'--no-spec',
'--name',
'Json Agent',
'--api-name',
'JsonAgent',
'--target-org',
'test@org.com',
]);

expect(selectStub.called).to.be.false;
expect(inputStub.called).to.be.false;
expect(yesNoOrCancelStub.called).to.be.false;
expect(result.agentPath).to.include('JsonAgent.agent');
expect(createAuthoringBundleStub.calledOnce).to.be.true;
});

it('should succeed with --json and --force-overwrite when existing AAB matches', async () => {
const EXISTING_BUNDLE_API_NAME = 'Willie_Resort_Manager';
const result = await AgentGenerateAuthoringBundle.run([
'--json',
'--no-spec',
'--name',
'Willie Resort Manager',
'--api-name',
EXISTING_BUNDLE_API_NAME,
'--force-overwrite',
'--target-org',
'test@org.com',
]);

expect(yesNoOrCancelStub.called).to.be.false;
expect(createAuthoringBundleStub.calledOnce).to.be.true;
expect(result.agentPath).to.include(`${EXISTING_BUNDLE_API_NAME}.agent`);
});
});
});