Skip to content
Merged
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
4 changes: 2 additions & 2 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "129 kB"
"maxSize": "130 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
Expand All @@ -22,7 +22,7 @@
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "101.5 kB"
"maxSize": "102 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('connectChat', () => {
...(!('agentId' in widgetParams) ? { agentId: 'agentId' } : {}),
disableTriggerValidation: true,
...widgetParams,
});
} as ChatConnectorParams);

const helper = algoliasearchHelper(createSearchClient(), '');

Expand Down Expand Up @@ -70,6 +70,62 @@ describe('connectChat', () => {
})
);
});

it('types requestOptions as agentId-only', () => {
const assertChatConnectorParams = <TParams extends ChatConnectorParams>(
params: TParams
) => params;
const customChat = undefined as unknown as Chat<UIMessage>;

const agentParams = assertChatConnectorParams({
agentId: 'agentId',
requestOptions: {
queryParameters: { cache: false },
headers: { 'x-algolia-referer': 'chat-widget' },
},
});

const legacyAgentWithTransportParams = assertChatConnectorParams({
agentId: 'agentId',
transport: { api: 'https://custom.api' },
});

// @ts-expect-error requestOptions is only valid with agentId
assertChatConnectorParams({
transport: { api: 'https://custom.api' },
requestOptions: {
queryParameters: { cache: false },
},
});

// @ts-expect-error requestOptions is not valid when a custom transport is provided
assertChatConnectorParams({
agentId: 'agentId',
transport: { api: 'https://custom.api' },
requestOptions: {
queryParameters: { cache: false },
},
});

assertChatConnectorParams({
// @ts-expect-error requestOptions is not valid with a custom chat instance
chat: customChat,
requestOptions: {
queryParameters: { cache: false },
},
});
Comment thread
anjola-adeuyi marked this conversation as resolved.
Comment thread
anjola-adeuyi marked this conversation as resolved.

expect(agentParams.requestOptions?.queryParameters).toEqual({
cache: false,
});
expect(agentParams.requestOptions?.headers).toEqual({
'x-algolia-referer': 'chat-widget',
});
expect(legacyAgentWithTransportParams).toEqual({
agentId: 'agentId',
transport: { api: 'https://custom.api' },
});
});
});

describe('getWidgetRenderState', () => {
Expand Down Expand Up @@ -1060,8 +1116,9 @@ data: [DONE]`,
});

function getRequestPayload() {
const [, init] = fetchMock.mock.calls[0];
const [url, init] = fetchMock.mock.calls[0];
return {
url: String(url),
headers: init.headers as Record<string, string>,
body: JSON.parse(init.body as string),
};
Expand Down Expand Up @@ -1108,6 +1165,120 @@ data: [DONE]`,
);
});

it('sends persistent query parameters on agent requests', async () => {
const { widget } = getInitializedWidget({
agentId: 'agentId',
requestOptions: {
queryParameters: {
cache: false,
hitsPerPage: 4,
explain: true,
userToken: 'user-1',
},
},
});

await widget.chatInstance.sendMessage({ text: 'hello' });

const { url } = getRequestPayload();
const searchParams = new URL(url).searchParams;
expect(searchParams.get('compatibilityMode')).toBe('ai-sdk-5');
expect(searchParams.get('cache')).toBe('false');
expect(searchParams.get('hitsPerPage')).toBe('4');
expect(searchParams.get('explain')).toBe('true');
expect(searchParams.get('userToken')).toBe('user-1');
});

it('keeps the built-in compatibility mode on agent requests', async () => {
const { widget } = getInitializedWidget({
agentId: 'agentId',
requestOptions: {
queryParameters: {
compatibilityMode: 'custom',
userToken: 'user-1',
},
},
});

await widget.chatInstance.sendMessage({ text: 'hello' });

const { url } = getRequestPayload();
const searchParams = new URL(url).searchParams;
expect(searchParams.get('compatibilityMode')).toBe('ai-sdk-5');
expect(searchParams.get('userToken')).toBe('user-1');
});

it('sends persistent headers on agent requests', async () => {
const { widget } = getInitializedWidget({
agentId: 'agentId',
requestOptions: {
headers: {
'x-algolia-referer': 'chat-widget',
'x-session-id': 'session-1',
},
},
});

await widget.chatInstance.sendMessage({ text: 'hello' });

const { headers } = getRequestPayload();
expect(headers).toEqual(
expect.objectContaining({
'x-algolia-application-id': 'appId',
'x-algolia-api-key': 'apiKey',
'x-algolia-referer': 'chat-widget',
'x-session-id': 'session-1',
})
);
});

it('sends persistent Headers instance on agent requests', async () => {
const { widget } = getInitializedWidget({
agentId: 'agentId',
requestOptions: {
headers: new Headers({
'x-algolia-referer': 'chat-widget',
'x-session-id': 'session-1',
}),
},
});

await widget.chatInstance.sendMessage({ text: 'hello' });

const { headers } = getRequestPayload();
expect(headers).toEqual(
expect.objectContaining({
'x-algolia-application-id': 'appId',
'x-algolia-api-key': 'apiKey',
'x-algolia-referer': 'chat-widget',
'x-session-id': 'session-1',
})
);
});

it('keeps the x-algolia-agent chat marker even when requestOptions tries to override it', async () => {
const { widget } = getInitializedWidget({
agentId: 'agentId',
requestOptions: {
headers: {
'x-algolia-application-id': 'spoofed-app',
'x-algolia-api-key': 'spoofed-key',
'x-algolia-agent': 'spoofed-agent',
'x-algolia-referer': 'chat-widget',
},
},
});

await widget.chatInstance.sendMessage({ text: 'hello' });

const { headers } = getRequestPayload();
expect(headers['x-algolia-application-id']).toBe('appId');
expect(headers['x-algolia-api-key']).toBe('apiKey');
expect(headers['x-algolia-agent']).toContain('; chat');
expect(headers['x-algolia-agent']).not.toBe('spoofed-agent');
expect(headers['x-algolia-referer']).toBe('chat-widget');
});

it('does not register `chat` on the search client user-agent', () => {
const addAlgoliaAgent = jest.fn();
const client = Object.assign(createSearchClient(), {
Expand Down Expand Up @@ -1147,6 +1318,41 @@ data: [DONE]`,
});
});

it('lets per-call headers override persistent headers for one request', async () => {
const { widget } = getInitializedWidget({
agentId: 'agentId',
requestOptions: {
headers: {
'x-algolia-referer': 'chat-widget',
},
},
});

await widget.chatInstance.sendMessage(
{ text: 'hello' },
{ headers: { 'x-algolia-referer': 'prompt-suggestions' } }
);
await widget.chatInstance.sendMessage({ text: 'follow-up' });

const firstHeaders = fetchMock.mock.calls[0][1].headers as Record<
string,
string
>;
const secondHeaders = fetchMock.mock.calls[1][1].headers as Record<
string,
string
>;

expect(firstHeaders).toHaveProperty(
'x-algolia-referer',
'prompt-suggestions'
);
expect(secondHeaders).toHaveProperty(
'x-algolia-referer',
'chat-widget'
);
});

it('does not carry over the x-algolia-referer to follow-up messages', async () => {
const { widget } = getInitializedWidget({ agentId: 'agentId' });

Expand All @@ -1172,13 +1378,33 @@ data: [DONE]`,
expect(secondHeaders).not.toHaveProperty('x-algolia-referer');
});

it('forces cache=false when regenerating with persistent cache query parameter', async () => {
const { widget } = getInitializedWidget({
agentId: 'agentId',
requestOptions: {
queryParameters: {
cache: true,
},
},
});

await widget.chatInstance.regenerate();

const { url } = getRequestPayload();
expect(new URL(url).searchParams.get('cache')).toBe('false');
});

it('does not duplicate transport metadata in the request body', async () => {
const { widget } = getInitializedWidget({ agentId: 'agentId' });

await widget.chatInstance.sendMessage({ text: 'hello' });

const { body } = getRequestPayload();
expect(Object.keys(body).sort()).toEqual(['id', 'messageId', 'messages']);
expect(Object.keys(body).sort()).toEqual([
'id',
'messageId',
'messages',
]);
expect(body).not.toHaveProperty('headers');
expect(body).not.toHaveProperty('api');
expect(body).not.toHaveProperty('credentials');
Expand Down Expand Up @@ -1250,11 +1476,10 @@ data: [DONE]`,
return new Chat<UIMessage>({ transport: createMockTransport() });
}

function createChatWidgetWithContext(
params: Omit<ChatConnectorParams<UIMessage>, 'transport' | 'agentId'> & {
chat: Chat<UIMessage>;
}
) {
function createChatWidgetWithContext(params: {
chat: Chat<UIMessage>;
context?: ChatConnectorParams<UIMessage>['context'];
}) {
const renderFn = jest.fn();
const makeWidget = connectChat(renderFn);
const widget = makeWidget({
Expand Down
Loading
Loading