Skip to content

Commit 33ddd42

Browse files
authored
feat: wire --test-runner agentforce-studio into agent test create @W-22513740@ (#430)
1 parent 70296c6 commit 33ddd42

5 files changed

Lines changed: 145 additions & 14 deletions

File tree

command-snapshot.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,17 @@
178178
"command": "agent:test:create",
179179
"flagAliases": [],
180180
"flagChars": ["o"],
181-
"flags": ["api-name", "api-version", "flags-dir", "force-overwrite", "json", "preview", "spec", "target-org"],
181+
"flags": [
182+
"api-name",
183+
"api-version",
184+
"flags-dir",
185+
"force-overwrite",
186+
"json",
187+
"preview",
188+
"spec",
189+
"target-org",
190+
"test-runner"
191+
],
182192
"plugin": "@salesforce/plugin-agent"
183193
},
184194
{

messages/agent.test.create.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Create an agent test in your org using a local test spec YAML file.
66

77
To run this command, you must have an agent test spec file, which is a YAML file that lists the test cases for testing a specific agent. Use the "agent generate test-spec" CLI command to generate a test spec file. Then specify the file to this command with the --spec flag, or run this command with no flags to be prompted.
88

9-
When this command completes, your org contains the new agent test, which you can view and edit using the Testing Center UI. This command also retrieves the metadata component (AiEvaluationDefinition) associated with the new test to your local Salesforce DX project and displays its filename.
9+
When this command completes, your org contains the new agent test, which you can view and edit using the Testing Center UI (legacy) or Agentforce Studio (NGT). This command also retrieves the metadata component associated with the new test to your local Salesforce DX project and displays its filename. By default, the legacy AiEvaluationDefinition is created; use --test-runner agentforce-studio to author an AiTestingDefinition (NGT) instead.
1010

1111
After you've created the test in the org, use the "agent test run" command to run it.
1212

@@ -16,7 +16,7 @@ Path to the test spec YAML file.
1616

1717
# flags.preview.summary
1818

19-
Preview the test metadata file (AiEvaluationDefinition) without deploying to your org.
19+
Preview the test metadata file without deploying to your org.
2020

2121
# flags.force-overwrite.summary
2222

@@ -40,13 +40,17 @@ API name of the new test; the API name must not exist in the org.
4040

4141
<%= config.bin %> <%= command.id %> --spec specs/Resort_Manager-testSpec.yaml --api-name Resort_Manager_Test --preview
4242

43+
- Author an Agentforce Studio (NGT) test from an NGT-shaped YAML; writes an AiTestingDefinition metadata file:
44+
45+
<%= config.bin %> <%= command.id %> --spec specs/ReturnsCheckout.ngt.yaml --api-name Returns_Checkout --test-runner agentforce-studio --target-org my-org
46+
4347
# prompt.confirm
4448

4549
A test with the API name %s already exists in the org. Do you want to overwrite it?
4650

4751
# info.success
4852

49-
Local AiEvaluationDefinition metadata XML file created at %s and agent test deployed to %s.
53+
Local test metadata XML file created at %s and agent test deployed to %s.
5054

5155
# info.preview-success
5256

src/commands/agent/test/create.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { AgentTest, AgentTestCreateLifecycleStages } from '@salesforce/agents';
2121
import { DeployResult } from '@salesforce/source-deploy-retrieve';
2222
import { MultiStageOutput } from '@oclif/multi-stage-output';
2323
import { CLIError } from '@oclif/core/errors';
24-
import { makeFlags, promptForFlag, promptForYamlFile } from '../../../flags.js';
24+
import { makeFlags, promptForFlag, promptForYamlFile, testRunnerFlag } from '../../../flags.js';
2525
import yesNoOrCancel from '../../../yes-no-cancel.js';
2626

2727
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
@@ -110,6 +110,7 @@ export default class AgentTestCreate extends SfCommand<AgentTestCreateResult> {
110110
'force-overwrite': Flags.boolean({
111111
summary: messages.getMessage('flags.force-overwrite.summary'),
112112
}),
113+
'test-runner': testRunnerFlag,
113114
};
114115
private mso?: MultiStageOutput<{ path: string }>;
115116

@@ -175,33 +176,37 @@ export default class AgentTestCreate extends SfCommand<AgentTestCreateResult> {
175176
return Promise.resolve();
176177
});
177178

179+
const testRunner = flags['test-runner'];
180+
const outputDirName = testRunner === 'agentforce-studio' ? 'aiTestingDefinitions' : 'aiEvaluationDefinitions';
181+
178182
let path;
179183
let contents;
180184
try {
181185
const result = await AgentTest.create(connection, apiName, spec, {
182-
outputDir: join('force-app', 'main', 'default', 'aiEvaluationDefinitions'),
186+
outputDir: join('force-app', 'main', 'default', outputDirName),
183187
preview: flags.preview,
188+
testRunner,
184189
});
185190
path = result.path;
186191
contents = result.contents;
187192
} catch (error) {
188193
const wrapped = SfError.wrap(error);
189194

190-
// Check for file not found errors
191-
if (
192-
wrapped.message.toLowerCase().includes('not found') ||
193-
wrapped.message.toLowerCase().includes('enoent') ||
194-
wrapped.code === 'ENOENT'
195-
) {
195+
if (wrapped.code === 'ENOENT' || wrapped.name === 'ENOENT') {
196196
throw new SfError(`Test spec file not found: ${spec}`, 'SpecFileNotFound', [], 2, wrapped);
197197
}
198198

199-
// Check for deployment failures (API/network)
199+
// NGT validateNgtSpec errors are user-fixable spec issues — exit 1, not deploy/network.
200+
if (wrapped.name?.startsWith('ngt')) {
201+
throw wrapped;
202+
}
203+
204+
// Deploy failures from the lib are bare SfErrors with the componentFailures text as message
205+
// and no structured code, so message substring is the only available signal.
200206
if (wrapped.message.toLowerCase().includes('deploy') || wrapped.message.toLowerCase().includes('api')) {
201207
throw new SfError(`Deployment failed: ${wrapped.message}`, 'DeploymentFailed', [wrapped.message], 4, wrapped);
202208
}
203209

204-
// Other errors (validation, format issues) use exit 1
205210
throw wrapped;
206211
}
207212

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: ReturnsCheckoutSuite
2+
description: Validates the Returns / Checkout flow on agent v1.
3+
subjectType: AGENT
4+
subjectName: ReturnsAgent
5+
subjectVersion: v1
6+
testCases:
7+
# 1: assertion scorers - topic + action + LLM-judged outcome.
8+
- inputs:
9+
- utterance: 'Where is my order #12345?'
10+
scorers:
11+
- name: topic_sequence_match
12+
expected: order_status
13+
- name: action_sequence_match
14+
expected: Get_Order_Status
15+
- name: bot_response_rating
16+
expected: 'Agent looks up the order and returns its status'
17+
# 2: assertion + quality + numeric mix.
18+
- inputs:
19+
- utterance: 'Cancel my order'
20+
scorers:
21+
- name: topic_sequence_match
22+
expected: returns
23+
- name: bot_response_rating
24+
expected: 'Agent confirms the cancellation'
25+
- name: coherence
26+
- name: factuality
27+
- name: output_latency_milliseconds
28+
# 3: multi-action expected - Python-list-string format.
29+
- inputs:
30+
- utterance: 'Verify identity and look up my order'
31+
scorers:
32+
- name: action_sequence_match
33+
expected: "['Verify_Customer','Get_Order_Status']"
34+
# 4: contextVariables + multi-turn conversationHistory + task_resolution.
35+
- inputs:
36+
- utterance: 'Yes, my email is jane@example.com'
37+
contextVariables:
38+
- name: RoutableId
39+
value: '0Mw000000000001'
40+
conversationHistory:
41+
- role: user
42+
message: 'I need help with my order'
43+
- role: agent
44+
topic: identity_verification
45+
message: "I can help. What's your email on file?"
46+
scorers:
47+
- name: topic_sequence_match
48+
expected: identity_verification
49+
- name: response_match
50+
expected: 'Agent verifies identity and continues'
51+
- name: task_resolution
52+
# 5: quality scorers without `expected`.
53+
- inputs:
54+
- utterance: 'Show order details'
55+
scorers:
56+
- name: factuality
57+
- name: completeness
58+
# 6: handoff scorer - expected is the target agent's DeveloperName.
59+
- inputs:
60+
- utterance: 'I need a sales rep'
61+
scorers:
62+
- name: topic_sequence_match
63+
expected: handoff
64+
- name: agent_handoff_match
65+
expected: SDRAgent
66+
# 7: multi-input - same scorers evaluate against three phrasings.
67+
- inputs:
68+
- utterance: "What's the status of order #12345?"
69+
- utterance: 'Where is my order 12345'
70+
- utterance: 'Tell me about order #12345'
71+
scorers:
72+
- name: topic_sequence_match
73+
expected: order_status
74+
- name: action_sequence_match
75+
expected: Get_Order_Status

test/nuts/agent.test.create.nut.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,41 @@ describe('agent test create', function () {
7777
}
7878
);
7979
});
80+
81+
it('should create NGT test from NGT-shaped spec file with --test-runner agentforce-studio', () => {
82+
const testApiName = genUniqueString('Test_Agent_NGT_%s');
83+
const specPath = join(session.project.dir, 'specs', 'ngtTestSpec.yaml');
84+
85+
const commandResult = execCmd<AgentTestCreateResult>(
86+
`agent test create --api-name "${testApiName}" --spec "${specPath}" --test-runner agentforce-studio --target-org ${getUsername()} --preview --json`,
87+
{ ensureExitCode: 0 }
88+
);
89+
90+
const result = commandResult.jsonOutput?.result;
91+
if (!result || typeof result !== 'object' || !result.path || !result.contents) {
92+
throw new Error(
93+
`Command failed or returned invalid result. Result type: ${typeof result}, value: ${JSON.stringify(result)}`
94+
);
95+
}
96+
97+
expect(result.path).to.be.a('string').and.not.be.empty;
98+
expect(result.contents).to.be.a('string').and.not.be.empty;
99+
// preview mode writes <apiName>-preview-<ISO>.xml; non-preview would be .aiTestingDefinition-meta.xml.
100+
expect(result.path).to.match(/-preview-.*\.xml$/);
101+
expect(result.contents).to.include('<AiTestingDefinition');
102+
});
103+
104+
it('should fail with NGT validation error when legacy YAML is passed with --test-runner agentforce-studio', () => {
105+
const testApiName = genUniqueString('Test_Agent_Legacy_%s');
106+
const legacySpecPath = join(session.project.dir, 'specs', 'testSpec.yaml');
107+
108+
const commandResult = execCmd<AgentTestCreateResult>(
109+
`agent test create --api-name "${testApiName}" --spec "${legacySpecPath}" --test-runner agentforce-studio --target-org ${getUsername()} --preview --json`,
110+
{ ensureExitCode: 'nonZero' }
111+
);
112+
113+
// Legacy YAML uses top-level utterance/expectedTopic per testCase, so NGT validation fails on
114+
// the missing `inputs:` array. Asserts the NGT validator runs (rather than the legacy path).
115+
expect(commandResult.jsonOutput?.message ?? '').to.match(/NGT test case|inputs/i);
116+
});
80117
});

0 commit comments

Comments
 (0)