Skip to content

Commit cc67cc5

Browse files
evanyan13Evan Yan
andauthored
[CI-4659] Rename Assistant Module to Agent (#386)
* Rename assistant module to agent * Add test cases for agent module * Add types for agent module * Update tests for tracker * Update docs * Update types in constructorio * Retain assistant method doc * Update doc in assistant module * Revert changes for docs * Revert docs for agent module * Add deprecation notice to assistantServiceUrl * Add missing statement * Update assistant method types * Use correct endpoints --------- Co-authored-by: Evan Yan <evan.yan@constructor.io>
1 parent e4f9c20 commit cc67cc5

File tree

10 files changed

+2591
-257
lines changed

10 files changed

+2591
-257
lines changed

spec/src/modules/agent.js

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/* eslint-disable no-unused-expressions, import/no-unresolved */
2+
const dotenv = require('dotenv');
3+
const chai = require('chai');
4+
const sinon = require('sinon');
5+
const sinonChai = require('sinon-chai');
6+
const EventSource = require('eventsource');
7+
const { ReadableStream } = require('web-streams-polyfill');
8+
const qs = require('qs');
9+
const { createAgentUrl, setupEventListeners } = require('../../../src/modules/agent');
10+
const Agent = require('../../../src/modules/agent');
11+
let ConstructorIO = require('../../../test/constructorio'); // eslint-disable-line import/extensions
12+
const { encodeURIComponentRFC3986 } = require('../../../src/utils/helpers');
13+
const jsdom = require('../utils/jsdom-global');
14+
15+
const bundled = process.env.BUNDLED === 'true';
16+
const bundledDescriptionSuffix = bundled ? ' - Bundled' : '';
17+
18+
chai.use(sinonChai);
19+
dotenv.config();
20+
21+
const testApiKey = process.env.TEST_REQUEST_API_KEY;
22+
const clientVersion = 'cio-mocha';
23+
24+
const defaultOptions = {
25+
apiKey: testApiKey,
26+
version: clientVersion,
27+
agentServiceUrl: 'https://agent.cnstrc.com',
28+
clientId: '123',
29+
sessionId: 123,
30+
};
31+
32+
const defaultParameters = {
33+
domain: 'agent',
34+
};
35+
36+
describe(`ConstructorIO - Agent${bundledDescriptionSuffix}`, () => {
37+
const jsdomOptions = { url: 'http://localhost' };
38+
let cleanup;
39+
40+
beforeEach(() => {
41+
cleanup = jsdom(jsdomOptions);
42+
global.CLIENT_VERSION = clientVersion;
43+
window.CLIENT_VERSION = clientVersion;
44+
45+
if (bundled) {
46+
ConstructorIO = window.ConstructorioClient;
47+
}
48+
});
49+
50+
afterEach(() => {
51+
delete global.CLIENT_VERSION;
52+
delete window.CLIENT_VERSION;
53+
cleanup();
54+
});
55+
56+
// createAgentUrl util Tests
57+
describe('Test createAgentUrl', () => {
58+
59+
it('should throw an error if intent is not provided', () => {
60+
expect(() => createAgentUrl('', defaultParameters, defaultOptions)).throw('intent is a required parameter of type string');
61+
});
62+
63+
it('should throw an error if domain is not provided in parameters', () => {
64+
expect(() => createAgentUrl('testIntent', {}, defaultOptions)).throw('parameters.domain is a required parameter of type string');
65+
});
66+
67+
it('should correctly construct a URL with minimal valid inputs', () => {
68+
const intent = 'testIntent';
69+
const url = createAgentUrl(intent, defaultParameters, defaultOptions);
70+
71+
expect(url).contain('https://agent.cnstrc.com/v1/intent/');
72+
expect(url).contain(`intent/${encodeURIComponentRFC3986(intent)}`);
73+
74+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
75+
76+
expect(requestedUrlParams).to.have.property('key');
77+
expect(requestedUrlParams).to.have.property('i');
78+
expect(requestedUrlParams).to.have.property('s');
79+
expect(requestedUrlParams).to.have.property('c').to.equal(clientVersion);
80+
expect(requestedUrlParams).to.have.property('_dt');
81+
82+
});
83+
84+
it('should clean and encode parameters correctly', () => {
85+
const intentWithSpaces = 'test/ [Intent)';
86+
const url = createAgentUrl(
87+
intentWithSpaces,
88+
{ ...defaultParameters },
89+
defaultOptions,
90+
);
91+
92+
// Ensure spaces are encoded and parameters are cleaned
93+
expect(url).not.contain(' ');
94+
expect(url).contain(encodeURIComponentRFC3986(intentWithSpaces));
95+
});
96+
});
97+
98+
// setupEventListeners util Tests
99+
describe('Test setupEventListeners', () => {
100+
let mockEventSource;
101+
let mockStreamController;
102+
103+
beforeEach(() => {
104+
mockEventSource = {
105+
addEventListener: sinon.stub(),
106+
close: sinon.stub(),
107+
};
108+
109+
mockStreamController = {
110+
enqueue: sinon.stub(),
111+
close: sinon.stub(),
112+
error: sinon.stub(),
113+
};
114+
});
115+
116+
afterEach(() => {
117+
sinon.restore(); // Restore all mocks
118+
});
119+
120+
it('should set up event listeners for all event types', () => {
121+
const eventTypes = Agent.EventTypes;
122+
123+
setupEventListeners(mockEventSource, mockStreamController, eventTypes);
124+
Object.values(Agent.EventTypes).forEach((event) => {
125+
expect(mockEventSource.addEventListener.calledWith(event)).to.be.true;
126+
});
127+
});
128+
129+
it('should enqueue event data into the stream', (done) => {
130+
const eventTypes = Agent.EventTypes;
131+
const eventType = Agent.EventTypes.SEARCH_RESULT;
132+
const eventData = { data: 'Hello, world!' };
133+
134+
setupEventListeners(mockEventSource, mockStreamController, eventTypes);
135+
136+
// Simulate an event being emitted
137+
const allEventsCallbacks = mockEventSource.addEventListener.getCalls();
138+
const searchResultsCallback = allEventsCallbacks.find((call) => call.args[0] === eventType).args[1];
139+
140+
searchResultsCallback({ data: JSON.stringify(eventData) });
141+
142+
setImmediate(() => { // Ensure stream processing completes
143+
expect(mockStreamController.enqueue.calledWith({ type: eventType, data: eventData })).to.be.true;
144+
done();
145+
});
146+
});
147+
148+
it('should close the EventSource and the stream when END event is received', () => {
149+
const eventTypes = Agent.EventTypes;
150+
const eventType = Agent.EventTypes.END;
151+
152+
setupEventListeners(mockEventSource, mockStreamController, eventTypes);
153+
154+
// Simulate the END event being emitted
155+
const endEventCallback = mockEventSource.addEventListener.getCalls()
156+
.find((call) => call.args[0] === eventType).args[1];
157+
158+
endEventCallback();
159+
160+
expect(mockEventSource.close.called).to.be.true;
161+
expect(mockStreamController.close.called).to.be.true;
162+
});
163+
164+
it('should handle errors from the EventSource', () => {
165+
const eventTypes = { START: 'start', END: 'end' };
166+
const mockError = new Error('Test Error');
167+
168+
setupEventListeners(mockEventSource, mockStreamController, eventTypes);
169+
170+
// Directly trigger the onerror handler
171+
mockEventSource.onerror(mockError);
172+
173+
// Assert that controller.error was called with the mock error
174+
sinon.assert.calledWith(mockStreamController.error, mockError);
175+
176+
// Assert that the event source was closed
177+
sinon.assert.calledOnce(mockEventSource.close);
178+
});
179+
180+
it('should correctly handle and enqueue events with specific data structures', (done) => {
181+
const eventType = Agent.EventTypes.SEARCH_RESULT;
182+
const complexData = { intent_result_id: 123, response: { results: [{ name: 'Item 1' }, { name: 'Item 2' }] } };
183+
const eventTypes = Agent.EventTypes;
184+
185+
setupEventListeners(mockEventSource, mockStreamController, eventTypes);
186+
187+
// Simulate an event with a complex data structure being emitted
188+
const dataStructureEventCallback = mockEventSource.addEventListener.getCalls()
189+
.find((call) => call.args[0] === eventType).args[1];
190+
191+
dataStructureEventCallback({ data: JSON.stringify(complexData) });
192+
193+
// Verify that the complex data structure was correctly enqueued
194+
setImmediate(() => {
195+
expect(mockStreamController.enqueue.calledWith({ type: eventType, data: complexData })).to.be.true;
196+
done();
197+
});
198+
});
199+
});
200+
201+
describe('getAgentResultsStream', () => {
202+
beforeEach(() => {
203+
global.EventSource = EventSource;
204+
global.ReadableStream = ReadableStream;
205+
window.EventSource = EventSource;
206+
window.ReadableStream = ReadableStream;
207+
});
208+
209+
afterEach(() => {
210+
delete global.EventSource;
211+
delete global.ReadableStream;
212+
delete window.EventSource;
213+
delete window.ReadableStream;
214+
});
215+
216+
it('should create a readable stream', () => {
217+
const { agent } = new ConstructorIO(defaultOptions);
218+
const stream = agent.getAgentResultsStream('I want shoes', { domain: 'assistant' });
219+
220+
// Assert it return a stream object
221+
expect(stream).to.have.property('getReader');
222+
});
223+
224+
it('should throw an error if missing domain parameter', () => {
225+
const { agent } = new ConstructorIO(defaultOptions);
226+
227+
expect(() => agent.getAgentResultsStream('I want shoes', {})).throw('parameters.domain is a required parameter of type string');
228+
});
229+
230+
it('should throw an error if missing intent', () => {
231+
const { agent } = new ConstructorIO(defaultOptions);
232+
233+
expect(() => agent.getAgentResultsStream('', {})).throw('intent is a required parameter of type string');
234+
});
235+
236+
it('should push expected data to the stream', async () => {
237+
const { agent } = new ConstructorIO(defaultOptions);
238+
const stream = await agent.getAgentResultsStream('query', { domain: 'assistant' });
239+
const reader = stream.getReader();
240+
const { value, done } = await reader.read();
241+
242+
// Assert that the stream is not empty and the first chunk contains expected data
243+
expect(done).to.be.false;
244+
expect(value.type).to.equal('start');
245+
reader.releaseLock();
246+
});
247+
248+
it('should handle cancel to the stream gracefully', async () => {
249+
const { agent } = new ConstructorIO(defaultOptions);
250+
const stream = await agent.getAgentResultsStream('query', { domain: 'assistant' });
251+
const reader = stream.getReader();
252+
const { value, done } = await reader.read();
253+
254+
// Assert that the stream is not empty and the first chunk contains expected data
255+
expect(done).to.be.false;
256+
expect(value.type).to.equal('start');
257+
reader.cancel();
258+
});
259+
260+
it('should handle pre maturely cancel before reading any data', async () => {
261+
const { agent } = new ConstructorIO(defaultOptions);
262+
const stream = await agent.getAgentResultsStream('query', { domain: 'assistant' });
263+
const reader = stream.getReader();
264+
265+
reader.cancel();
266+
});
267+
});
268+
});

0 commit comments

Comments
 (0)