Skip to content

Commit c3731a0

Browse files
committed
fix: agent preview tests
1 parent dda01b8 commit c3731a0

9 files changed

+274
-11
lines changed

src/agentPreview.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ export type AgentPreviewSendResponse = {
7474
_links: AgentPreviewMessageLinks;
7575
};
7676

77+
export type AgentPreviewEndMessage = {
78+
type: string;
79+
id: string;
80+
reason: string;
81+
feedbackId: string;
82+
};
83+
7784
export type AgentPreviewEndResponse = {
78-
messages: {
79-
type: string;
80-
id: string;
81-
reason: string;
82-
feedbackId: string;
83-
};
85+
messages: AgentPreviewEndMessage[];
8486
_links: AgentPreviewMessageLinks;
8587
};
8688

@@ -120,7 +122,6 @@ export class AgentPreview {
120122

121123
public async send(sessionId: string, message: string): Promise<AgentPreviewSendResponse> {
122124
const url = `${this.apiBase}/sessions/${sessionId}/messages`;
123-
124125
const body = {
125126
message: {
126127
// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-examples.html#send-synchronous-messages

src/maybe-mock.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class MaybeMock {
134134
* Will either use mocked responses, or the real server response, as the library/APIs become more feature complete,
135135
* there will be fewer mocks and more real responses
136136
*
137-
* @param {"GET" | "POST"} method
137+
* @param {"GET" | "POST" | "DELETE"} method
138138
* @param {string} url
139139
* @param {nock.RequestBodyMatcher} body
140140
* @returns {Promise<T>}
@@ -150,20 +150,30 @@ export class MaybeMock {
150150
const responses = await readResponses<T>(this.mockDir, url, this.logger);
151151
const baseUrl = this.connection.baseUrl();
152152
const scope = this.scopes.get(baseUrl) ?? nock(baseUrl);
153+
// Look up status code to determine if it's successful or not
154+
// Be have to assert this is a number because AgentTester has a status that is non-numeric
155+
const getCode = (response: T): number =>
156+
typeof response === 'object' && 'status' in response && typeof response.status === 'number'
157+
? response.status
158+
: 200;
159+
// This is a hack to work with SFAP endpoints
160+
url = url.replace('https://api.salesforce.com', '');
153161
this.scopes.set(baseUrl, scope);
154162
switch (method) {
155163
case 'GET':
156164
for (const response of responses) {
157-
scope.get(url).reply(200, response);
165+
scope.get(url).reply(getCode(response), response);
158166
}
159167
break;
160168
case 'POST':
161169
for (const response of responses) {
162-
scope.post(url, body).reply(200, response);
170+
scope.post(url, body).reply(getCode(response), response);
163171
}
164172
break;
165173
case 'DELETE':
166-
// Support mocked DELETE when needed
174+
for (const response of responses) {
175+
scope.delete(url).reply(getCode(response), response);
176+
}
167177
break;
168178
}
169179
}

test/agentPreview.test.ts

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { join } from 'node:path';
9+
import { expect } from 'chai';
10+
import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup';
11+
import { Connection, SfError } from '@salesforce/core';
12+
import { AgentPreview } from '../src/agentPreview';
13+
14+
describe('AgentPreview', () => {
15+
const $$ = new TestContext();
16+
let testOrg: MockTestOrgData;
17+
let connection: Connection;
18+
const session = 'e17fe68d-8509-4da7-8715-f270da5d64be';
19+
const agentId = '0Xxed00000002Q1CAI';
20+
21+
beforeEach(async () => {
22+
$$.inProject(true);
23+
testOrg = new MockTestOrgData();
24+
process.env.SF_MOCK_DIR = join('test', 'mocks');
25+
connection = await testOrg.getConnection();
26+
connection.instanceUrl = 'https://api.salesforce.com';
27+
// restore the connection sandbox so that it doesn't override the builtin mocking (MaybeMock)
28+
$$.SANDBOXES.CONNECTION.restore();
29+
});
30+
31+
afterEach(() => {
32+
delete process.env.SF_MOCK_DIR;
33+
});
34+
35+
describe('start', () => {
36+
it('should start a session and return an AgentPreviewStartResponse', async () => {
37+
process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-Start');
38+
39+
const agentPreview = new AgentPreview(connection);
40+
const result = await agentPreview.start(agentId);
41+
42+
expect(result.sessionId).to.deep.equal(session);
43+
expect(result.messages[0].type).to.deep.equal('Inform');
44+
expect(result.messages[0].message).to.deep.equal("Hi, I'm an AI service assistant. How can I help you?");
45+
});
46+
47+
it('should wrap errors in SfError on start', async () => {
48+
process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-Start-Error');
49+
const agentPreview = new AgentPreview(connection);
50+
try {
51+
await agentPreview.start(agentId);
52+
expect.fail('Expected error to be thrown');
53+
} catch (err) {
54+
expect(err).to.be.instanceOf(SfError);
55+
// @ts-expect-error We just confirmed it's an SfError
56+
expect((err as SfError).cause.message).to.include('An unexpected error occurred');
57+
}
58+
});
59+
});
60+
61+
describe('send', () => {
62+
it('should send a message and return an AgentPreviewSendResponse', async () => {
63+
process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-Send');
64+
65+
const agentPreview = new AgentPreview(connection);
66+
const message = 'Hello, Agent!';
67+
const result = await agentPreview.send(session, message);
68+
69+
expect(result.messages[0].type).to.deep.equal('Inform');
70+
expect(result.messages[0].message).to.deep.equal(
71+
'How can I assist you with any questions or issues you might have?'
72+
);
73+
});
74+
75+
it('should wrap errors in SfError on start', async () => {
76+
process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-Send-Error');
77+
const agentPreview = new AgentPreview(connection);
78+
79+
try {
80+
const message = 'Hello, Agent!';
81+
await agentPreview.send(session, message);
82+
expect.fail('Expected error to be thrown');
83+
} catch (err) {
84+
expect(err).to.be.instanceOf(SfError);
85+
// @ts-expect-error We just confirmed it's an SfError
86+
expect((err as SfError).cause.message).to.include('V6Session not found for sessionId');
87+
}
88+
});
89+
});
90+
91+
describe('end', () => {
92+
it('should end a session and return an AgentPreviewEndResponse', async () => {
93+
process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-End');
94+
const agentPreview = new AgentPreview(connection);
95+
const reason = 'UserRequest' as const;
96+
const result = await agentPreview.end(session, reason);
97+
98+
expect(result.messages[0].type).to.deep.equal('SessionEnded');
99+
expect(result.messages[0].id).to.exist;
100+
expect(result.messages[0].reason).to.deep.equal('ClientRequest');
101+
});
102+
103+
it('should wrap errors in SfError on end', async () => {
104+
process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-End-Error');
105+
const agentPreview = new AgentPreview(connection);
106+
107+
try {
108+
await agentPreview.end(session, 'UserRequest');
109+
expect.fail('Expected error to be thrown');
110+
} catch (err) {
111+
expect(err).to.be.instanceOf(SfError);
112+
// @ts-expect-error We just confirmed it's an SfError
113+
expect((err as SfError).cause.message).to.include('V6Session not found for sessionId');
114+
}
115+
});
116+
});
117+
118+
// describe('status', () => {
119+
// it('should return the API status', async () => {
120+
// process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-Status');
121+
// const agentPreview = new AgentPreview(connection);
122+
// const result = await agentPreview.status();
123+
124+
// expect(result.status).to.deep.equal('UP');
125+
// });
126+
127+
// it('should wrap errors in SfError on status', async () => {
128+
// process.env.SF_MOCK_DIR = join('test', 'mocks', 'createPreview-Status-Error');
129+
// const agentPreview = new AgentPreview(connection);
130+
// try {
131+
// await agentPreview.status();
132+
// expect.fail('Expected error to be thrown');
133+
// } catch (err) {
134+
// expect(err).to.be.instanceOf(SfError);
135+
// }
136+
// });
137+
// });
138+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"status": 404,
3+
"path": "v6.0.0/sessions/71bcf43a-a234-4936-a1c8-f85265e7cf94",
4+
"mode": "unknown",
5+
"requestId": "30ac4229-6732-4f1c-b99a-cc13957f0fc5",
6+
"error": "NotFoundException",
7+
"message": "V6Session not found for sessionId: 71bcf43a-a234-4936-a1c8-f85265e7cf94",
8+
"timestamp": 1740762627208,
9+
"expected": true
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"messages": [
3+
{
4+
"type": "SessionEnded",
5+
"id": "86b7fba9-1295-4125-90df-50984cbc0048",
6+
"reason": "ClientRequest",
7+
"feedbackId": "6ee5a14f-420c-4ed6-9ab8-40d676f87b9d"
8+
}
9+
],
10+
"_links": {
11+
"self": null,
12+
"messages": {
13+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be/messages"
14+
},
15+
"messagesStream": {
16+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be/messages/stream"
17+
},
18+
"session": {
19+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/agents/0Xxed00000002Q1CAI/sessions"
20+
},
21+
"end": {
22+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be"
23+
}
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"status": 404,
3+
"path": "v6.0.0/sessions/94081416-c6dc-43a8-95e2-14a3f3580d34/messages",
4+
"mode": "unknown",
5+
"requestId": "e6840f72-e567-4eb3-94ad-0a1da8f83dd0",
6+
"error": "NotFoundException",
7+
"message": "V6Session not found for sessionId: 94081416-c6dc-43a8-95e2-14a3f3580d34",
8+
"timestamp": 1740760662466,
9+
"expected": true
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"messages": [
3+
{
4+
"type": "Inform",
5+
"id": "0ea5baf3-2539-45e3-857c-73625d633f1f",
6+
"feedbackId": "54f274f4-cecc-4e11-a3c9-8a08a7d2dd11",
7+
"planId": "54f274f4-cecc-4e11-a3c9-8a08a7d2dd11",
8+
"isContentSafe": true,
9+
"message": "How can I assist you with any questions or issues you might have?",
10+
"result": [],
11+
"citedReferences": []
12+
}
13+
],
14+
"_links": {
15+
"self": null,
16+
"messages": {
17+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be/messages"
18+
},
19+
"messagesStream": {
20+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be/messages/stream"
21+
},
22+
"session": {
23+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/agents/0Xxed00000002Q1CAI/sessions"
24+
},
25+
"end": {
26+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be"
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"status": 500,
3+
"path": "v6.0.0/agents/sfd/sessions",
4+
"mode": "unknown",
5+
"requestId": "adae6d09-a517-48a0-b501-5bbcac1c22c3",
6+
"error": "HttpServerErrorException",
7+
"message": "500 [{\"message\":\"An unexpected error occurred. Please include this ErrorId if you contact support: 12345678-1234 (123456787)\",\"errorCode\":\"UNKNOWN_EXCEPTION\"}]",
8+
"timestamp": 1740710748115,
9+
"expected": false
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"sessionId": "e17fe68d-8509-4da7-8715-f270da5d64be",
3+
"_links": {
4+
"self": null,
5+
"messages": {
6+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be/messages"
7+
},
8+
"messagesStream": {
9+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be/messages/stream"
10+
},
11+
"session": {
12+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/agents/0Xxed00000002Q1CAI/sessions"
13+
},
14+
"end": {
15+
"href": "https://api.salesforce.com/einstein/ai-agent/v1/sessions/e17fe68d-8509-4da7-8715-f270da5d64be"
16+
}
17+
},
18+
"messages": [
19+
{
20+
"type": "Inform",
21+
"id": "0adc259f-fdfd-42f7-9b1d-e2e0a0ec98be",
22+
"feedbackId": "",
23+
"planId": "",
24+
"isContentSafe": true,
25+
"message": "Hi, I'm an AI service assistant. How can I help you?",
26+
"result": [],
27+
"citedReferences": []
28+
}
29+
]
30+
}

0 commit comments

Comments
 (0)