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
70 changes: 50 additions & 20 deletions packages/toolbar/__tests__/ai-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@ describe('describeWidgetTypes', () => {
expect(result).toContain('attribute');
});

it('marks params with available suggestions', async () => {
it('marks params with suggest kind annotations', async () => {
const result = describeWidgetTypes();
expect(result).toMatch(/attribute.*\[has suggestions\]/);
expect(result).toMatch(/agentId.*\[has suggestions\]/);
expect(result).toMatch(/attributes.*\[has suggestions\]/);
expect(result).toMatch(/includedAttributes.*\[has suggestions\]/);
expect(result).toMatch(/excludedAttributes.*\[has suggestions\]/);
expect(result).toMatch(/attribute.*\[suggests: facetAttributes\]/);
expect(result).toMatch(/agentId.*\[suggests: agentStudioAgents\]/);
expect(result).toMatch(/attributes.*\[suggests: indexAttributes\]/);
expect(result).toMatch(/includedAttributes.*\[suggests: facetAttributes\]/);
expect(result).toMatch(/excludedAttributes.*\[suggests: facetAttributes\]/);
expect(result).toMatch(/indexName.*\[suggests: indices\]/);
expect(result).toMatch(/\(indexName \[suggests: indices:qs\]\)/);
expect(result).toMatch(/\(value \[suggests: indices:replicas\]\)/);
});

it('includes default placement per widget type', async () => {
Expand Down Expand Up @@ -3360,10 +3363,10 @@ describe('describeToolAction', () => {
expect(
describeToolAction(
'get_suggestions',
{ param: 'attribute', indexName: 'products' },
{ suggestKind: 'facetAttributes', indexName: 'products' },
{ values: ['brand', 'color'] }
)
).toBe('Fetched suggestions for attribute');
).toBe('Fetched suggestions for facetAttributes');
});
});

Expand Down Expand Up @@ -3402,7 +3405,7 @@ describe('buildToolDefinitions', () => {
});

describe('get_suggestions', () => {
it('returns error when param is missing', async () => {
it('returns error when suggestKind is missing', async () => {
const callbacks = createCallbacks();
const result = await executeToolCall(
'get_suggestions',
Expand All @@ -3411,15 +3414,15 @@ describe('get_suggestions', () => {
);
expect(result).toEqual({
success: false,
error: 'Missing required parameter: param',
error: 'Missing required parameter: suggestKind',
});
});

it('returns error when indexName is missing for index-bound param', async () => {
it('returns error when indexName is missing for index-bound kind', async () => {
const callbacks = createCallbacks();
const result = await executeToolCall(
'get_suggestions',
{ param: 'attribute' },
{ suggestKind: 'facetAttributes' },
callbacks
);
expect(result).toEqual({
Expand All @@ -3428,24 +3431,23 @@ describe('get_suggestions', () => {
});
});

it('returns error for param with no suggestion source', async () => {
it('returns error for unknown suggestKind', async () => {
const callbacks = createCallbacks();
const result = await executeToolCall(
'get_suggestions',
{ param: 'showRecent', indexName: 'products' },
{ suggestKind: 'unknownKind', indexName: 'products' },
callbacks
);
expect(result).toEqual({
success: false,
error: 'No suggestions available for parameter "showRecent"',
error: 'Unknown suggestion source: unknownKind',
});
});

it('calls getCredentials and returns fetch error for valid param', async () => {
it('calls getCredentials and returns fetch error for valid kind', async () => {
const callbacks = createCallbacks();
const result = await executeToolCall(
'get_suggestions',
{ param: 'attribute', indexName: 'products' },
{ suggestKind: 'facetAttributes', indexName: 'products' },
callbacks
);
expect(callbacks.getCredentials).toHaveBeenCalled();
Expand All @@ -3455,11 +3457,11 @@ describe('get_suggestions', () => {
});
});

it('does not require indexName for agentId param', async () => {
it('does not require indexName for agentStudioAgents kind', async () => {
const callbacks = createCallbacks();
const result = await executeToolCall(
'get_suggestions',
{ param: 'agentId' },
{ suggestKind: 'agentStudioAgents' },
callbacks
);
expect(callbacks.getCredentials).toHaveBeenCalled();
Expand All @@ -3469,4 +3471,32 @@ describe('get_suggestions', () => {
error: 'Failed to fetch suggestions from agentStudioAgents',
});
});

it('does not require indexName for indices kind', async () => {
const callbacks = createCallbacks();
const result = await executeToolCall(
'get_suggestions',
{ suggestKind: 'indices' },
callbacks
);
expect(callbacks.getCredentials).toHaveBeenCalled();
// Returns empty (fetchIndices returns [] on network error) — no indexName error
expect(result).toEqual({
values: [],
description: 'Available Algolia indices',
});
});

it('requires indexName for indices:replicas kind', async () => {
const callbacks = createCallbacks();
const result = await executeToolCall(
'get_suggestions',
{ suggestKind: 'indices:replicas' },
callbacks
);
expect(result).toEqual({
success: false,
error: 'Missing required parameter: indexName',
});
});
});
218 changes: 197 additions & 21 deletions packages/toolbar/__tests__/suggestions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';

import {
fetchSuggestions,
getSuggestionSourceForParam,
suggestionSourceRequiresIndexName,
} from '../src/ai/suggestions';

Expand All @@ -20,36 +19,31 @@ afterAll(() => {
return server.close();
});

describe('getSuggestionSourceForParam', () => {
it('returns source name for attribute param', () => {
expect(getSuggestionSourceForParam('attribute')).toBe('facetAttributes');
expect(getSuggestionSourceForParam('attributes')).toBe('facetAttributes');
expect(getSuggestionSourceForParam('includedAttributes')).toBe(
'facetAttributes'
);
expect(getSuggestionSourceForParam('excludedAttributes')).toBe(
'facetAttributes'
);
});

it('returns source name for agentId param', () => {
expect(getSuggestionSourceForParam('agentId')).toBe('agentStudioAgents');
describe('suggestionSourceRequiresIndexName', () => {
it('returns true for facetAttributes', () => {
expect(suggestionSourceRequiresIndexName('facetAttributes')).toBe(true);
});

it('returns undefined for unknown param', () => {
expect(getSuggestionSourceForParam('unknownParam')).toBeUndefined();
it('returns true for indices:replicas', () => {
expect(suggestionSourceRequiresIndexName('indices:replicas')).toBe(true);
});
});

describe('suggestionSourceRequiresIndexName', () => {
it('returns true for facetAttributes', () => {
expect(suggestionSourceRequiresIndexName('facetAttributes')).toBe(true);
it('returns true for indexAttributes', () => {
expect(suggestionSourceRequiresIndexName('indexAttributes')).toBe(true);
});

it('returns false for agentStudioAgents', () => {
expect(suggestionSourceRequiresIndexName('agentStudioAgents')).toBe(false);
});

it('returns false for indices', () => {
expect(suggestionSourceRequiresIndexName('indices')).toBe(false);
});

it('returns false for indices:qs', () => {
expect(suggestionSourceRequiresIndexName('indices:qs')).toBe(false);
});

it('returns false for unknown source', () => {
expect(suggestionSourceRequiresIndexName('unknown')).toBe(false);
});
Expand Down Expand Up @@ -243,4 +237,186 @@ describe('fetchSuggestions', () => {
labels: { 'id-prod': 'Prod Agent' },
});
});

it('returns sorted index names', async () => {
server.use(
http.get('https://APP_ID-dsn.algolia.net/1/indexes', () => {
return HttpResponse.json({
items: [
{ name: 'products' },
{ name: 'articles' },
{ name: 'categories' },
],
nbPages: 1,
});
})
);

const result = await fetchSuggestions('indices', credentials, 'beta');

expect(result).toEqual({
values: ['articles', 'categories', 'products'],
description: 'Available Algolia indices',
});
});

it('returns empty values when indices fetch fails', async () => {
server.use(
http.get('https://APP_ID-dsn.algolia.net/1/indexes', () => {
return HttpResponse.error();
})
);

const result = await fetchSuggestions('indices', credentials, 'beta');

expect(result).toEqual({
values: [],
description: 'Available Algolia indices',
});
});

it('returns sorted replica names for a given index', async () => {
server.use(
http.get('https://APP_ID-dsn.algolia.net/1/indexes', () => {
return HttpResponse.json({
items: [
{
name: 'products',
replicas: ['products_price_asc', 'products_name_desc'],
},
{ name: 'articles' },
],
nbPages: 1,
});
})
);

const result = await fetchSuggestions(
'indices:replicas',
credentials,
'beta',
'products'
);

expect(result).toEqual({
values: ['products_name_desc', 'products_price_asc'],
description: 'Replica indices for the given index',
});
});

it('returns empty values when index has no replicas', async () => {
server.use(
http.get('https://APP_ID-dsn.algolia.net/1/indexes', () => {
return HttpResponse.json({
items: [{ name: 'products' }],
nbPages: 1,
});
})
);

const result = await fetchSuggestions(
'indices:replicas',
credentials,
'beta',
'products'
);

expect(result).toEqual({
values: [],
description: 'Replica indices for the given index',
});
});

it('returns sorted query suggestion index names', async () => {
server.use(
http.get('https://query-suggestions.us.algolia.com/1/configs', () => {
return HttpResponse.json([
{
indexName: 'products_query_suggestions',
sourceIndices: [{ indexName: 'products' }],
},
{
indexName: 'articles_query_suggestions',
sourceIndices: [{ indexName: 'articles' }],
},
]);
})
);

const result = await fetchSuggestions('indices:qs', credentials, 'beta');

expect(result).toEqual({
values: ['articles_query_suggestions', 'products_query_suggestions'],
description: 'Query Suggestion indices',
});
});

it('returns error when QS config fetch fails', async () => {
server.use(
http.get('https://query-suggestions.us.algolia.com/1/configs', () => {
return HttpResponse.error();
}),
http.get('https://query-suggestions.eu.algolia.com/1/configs', () => {
return HttpResponse.error();
})
);

const result = await fetchSuggestions('indices:qs', credentials, 'beta');

expect(result).toEqual({
values: [],
description: 'Query Suggestion indices',
});
});

it('returns sorted attribute names from index records', async () => {
server.use(
http.post(
'https://APP_ID-dsn.algolia.net/1/indexes/products/query',
() => {
return HttpResponse.json({
hits: [
{ objectID: '1', name: 'Widget', price: 9.99, brand: 'Acme' },
{ objectID: '2', name: 'Gadget', color: 'red', brand: 'Beta' },
],
});
}
)
);

const result = await fetchSuggestions(
'indexAttributes',
credentials,
'beta',
'products'
);

expect(result).toEqual({
values: ['brand', 'color', 'name', 'price'],
description: 'Attributes found in index records',
});
});

it('returns error when index records fetch fails', async () => {
server.use(
http.post(
'https://APP_ID-dsn.algolia.net/1/indexes/products/query',
() => {
return HttpResponse.error();
}
)
);

const result = await fetchSuggestions(
'indexAttributes',
credentials,
'beta',
'products'
);

expect(result).toEqual({
error:
'Failed to fetch suggestions from indexAttributes for index "products"',
});
});
});
Loading