Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,16 @@ const defaultMockResults = [
origin_id: 'att-1',
type: 'visualization',
title: 'Pacific Sales',
content: 'content',
score: 1,
},
{
id: 'chunk-2',
origin_id: 'att-2',
type: 'visualization',
title: 'Atlantic Metrics',
content: 'content',
score: 0.9,
},
];

let mockUseSmlSearchReturn: {
let mockUseSmlAutocompleteReturn: {
results: typeof defaultMockResults;
total: number;
isLoading: boolean;
Expand All @@ -44,8 +40,10 @@ let mockUseSmlSearchReturn: {
error: null,
};

jest.mock('../../../../../../../hooks/sml/use_sml_search', () => ({
useSmlSearch: () => mockUseSmlSearchReturn,
const mockUseSmlAutocomplete = jest.fn(() => mockUseSmlAutocompleteReturn);

jest.mock('../../../../../../../hooks/sml/use_sml_autocomplete', () => ({
useSmlAutocomplete: (...args: unknown[]) => mockUseSmlAutocomplete(...(args as [])),
}));

jest.mock('../../../../../../../hooks/use_conversation', () => ({
Expand All @@ -57,29 +55,30 @@ jest.mock('../../../../../../../hooks/agents/use_agent_by_id', () => ({
}));

beforeEach(() => {
mockUseSmlSearchReturn = {
mockUseSmlAutocompleteReturn = {
results: defaultMockResults,
total: defaultMockResults.length,
isLoading: false,
isError: false,
error: null,
};
mockUseSmlAutocomplete.mockClear();
});

const renderWithProvider = (ui: React.ReactElement) => {
return render(<EuiProvider>{ui}</EuiProvider>);
};

describe('Sml', () => {
it('renders SML search results as type/title', () => {
it('renders SML autocomplete results as type/title', () => {
const { container } = renderWithProvider(<Sml query="" onSelect={jest.fn()} />);

expect(container.textContent).toContain('visualization/Pacific Sales');
expect(container.textContent).toContain('visualization/Atlantic Metrics');
});

it('shows loading state when search is loading', () => {
mockUseSmlSearchReturn = {
it('shows loading state when autocomplete is loading', () => {
mockUseSmlAutocompleteReturn = {
results: [],
total: 0,
isLoading: true,
Expand Down Expand Up @@ -107,8 +106,8 @@ describe('Sml', () => {
});
});

it('shows default empty list when search errors with no results (errors surface via toast from useSmlSearch)', () => {
mockUseSmlSearchReturn = {
it('shows default empty list when autocomplete errors with no results', () => {
mockUseSmlAutocompleteReturn = {
results: [],
total: 0,
isLoading: false,
Expand All @@ -123,8 +122,8 @@ describe('Sml', () => {
expect(screen.getByText('No matching results')).toBeInTheDocument();
});

it('still lists cached results when useSmlSearch reports error', () => {
mockUseSmlSearchReturn = {
it('still lists cached results when useSmlAutocomplete reports error', () => {
mockUseSmlAutocompleteReturn = {
results: defaultMockResults,
total: defaultMockResults.length,
isLoading: false,
Expand All @@ -138,4 +137,10 @@ describe('Sml', () => {
expect(screen.queryByTestId('smlMenu-loading')).not.toBeInTheDocument();
expect(screen.queryByTestId('smlMenuError')).not.toBeInTheDocument();
});

it('passes undefined scoping to useSmlAutocomplete when the agent has no connector scoping', () => {
renderWithProvider(<Sml query="git" onSelect={jest.fn()} />);

expect(mockUseSmlAutocomplete).toHaveBeenCalledWith('git', { scoping: undefined });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@
import React, { forwardRef, useCallback, useMemo } from 'react';
import { css } from '@emotion/react';
import { EuiHighlight, useEuiTheme } from '@elastic/eui';
import { useSmlSearch } from '../../../../../../../hooks/sml/use_sml_search';
import { useSmlAutocomplete } from '../../../../../../../hooks/sml/use_sml_autocomplete';
import { useAgentId } from '../../../../../../../hooks/use_conversation';
import { useAgentBuilderAgentById } from '../../../../../../../hooks/agents/use_agent_by_id';
import type { CommandMenuComponentProps, CommandMenuHandle } from '../../types';
import { CommandId } from '../../types';
import { getSmlMenuHighlightSearchStrings } from '../../utils/sml_command_menu_highlight';
import { buildSmlFiltersFromAgent } from '../../utils/sml_filters';
import { buildSmlScopingFromAgent } from '../../utils/sml_filters';
import { CommandMenuList } from '../components/command_menu_list';
import type { CommandMenuListOption } from '../components/command_menu_list';

export const Sml = forwardRef<CommandMenuHandle, CommandMenuComponentProps>(
({ query, onSelect }, ref) => {
const agentId = useAgentId();
const { agent } = useAgentBuilderAgentById(agentId);
const filters = useMemo(() => buildSmlFiltersFromAgent(agent), [agent]);
const scoping = useMemo(() => buildSmlScopingFromAgent(agent), [agent]);
const { euiTheme } = useEuiTheme();
const { results, isLoading } = useSmlSearch(query, { skipContent: true, filters });
const { results, isLoading } = useSmlAutocomplete(query, { scoping });
const { type, title } = useMemo(() => getSmlMenuHighlightSearchStrings(query), [query]);

const smlMenuLabelStyles = useMemo(
Expand Down Expand Up @@ -56,7 +56,6 @@ export const Sml = forwardRef<CommandMenuHandle, CommandMenuComponentProps>(
</EuiHighlight>
</span>
<span>/</span>

<EuiHighlight strict={false} search={title}>
{titlePlain}
</EuiHighlight>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
*/

import { act, renderHook } from '@testing-library/react';
import { SmlSearchFilterType } from '@kbn/agent-context-layer-plugin/public';
import { SML_SEARCH_DEFAULT_SIZE } from '../../../../../../../../services/sml/constants';
import { queryKeys } from '../../../../../../../query_keys';
import { usePrefetchSml } from './use_prefetch_sml';

const mockPrefetchQuery = jest.fn();
const mockSearch = jest.fn();
const mockAutocomplete = jest.fn();

jest.mock('@kbn/react-query', () => ({
useQueryClient: () => ({
Expand All @@ -21,7 +22,7 @@ jest.mock('@kbn/react-query', () => ({

jest.mock('../../../../../../../hooks/use_agent_builder_service', () => ({
useAgentBuilderServices: () => ({
smlService: { search: mockSearch },
smlService: { autocomplete: mockAutocomplete },
}),
}));

Expand All @@ -36,7 +37,7 @@ describe('usePrefetchSml', () => {
mockExperimentalEnabled = true;
});

it('prefetches wildcard SML search when experimental features are enabled', () => {
it('prefetches wildcard SML autocomplete when experimental features are enabled', () => {
const { result } = renderHook(() => usePrefetchSml());

act(() => {
Expand All @@ -45,15 +46,15 @@ describe('usePrefetchSml', () => {

expect(mockPrefetchQuery).toHaveBeenCalledTimes(1);
expect(mockPrefetchQuery).toHaveBeenCalledWith({
queryKey: queryKeys.sml.search('*', true),
queryKey: queryKeys.sml.autocomplete('*'),
queryFn: expect.any(Function),
});
const queryFn = mockPrefetchQuery.mock.calls[0][0].queryFn as () => Promise<unknown>;
void queryFn();
expect(mockSearch).toHaveBeenCalledWith({
expect(mockAutocomplete).toHaveBeenCalledWith({
query: '*',
size: SML_SEARCH_DEFAULT_SIZE,
skipContent: true,
scoping: undefined,
});
});

Expand All @@ -67,4 +68,25 @@ describe('usePrefetchSml', () => {

expect(mockPrefetchQuery).not.toHaveBeenCalled();
});

it('threads agent-derived scoping into the prefetch call and query key', () => {
const scoping = { [SmlSearchFilterType.connector]: { ids: ['gh-1'] } };
const { result } = renderHook(() => usePrefetchSml(scoping));

act(() => {
result.current();
});

expect(mockPrefetchQuery).toHaveBeenCalledWith({
queryKey: queryKeys.sml.autocomplete('*', scoping),
queryFn: expect.any(Function),
});
const queryFn = mockPrefetchQuery.mock.calls[0][0].queryFn as () => Promise<unknown>;
void queryFn();
expect(mockAutocomplete).toHaveBeenCalledWith({
query: '*',
size: SML_SEARCH_DEFAULT_SIZE,
scoping,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

import { useCallback } from 'react';
import { useQueryClient } from '@kbn/react-query';
import type { SmlSearchFilters } from '@kbn/agent-context-layer-plugin/public';
import type { SmlSearchFilters, SmlSearchScoping } from '@kbn/agent-context-layer-plugin/public';
import { SML_SEARCH_DEFAULT_SIZE } from '../../../../../../../../services/sml/constants';
import { queryKeys } from '../../../../../../../query_keys';
import { useAgentBuilderServices } from '../../../../../../../hooks/use_agent_builder_service';
import { useExperimentalFeatures } from '../../../../../../../hooks/use_experimental_features';

export const usePrefetchSml = (filters?: SmlSearchFilters) => {
export const usePrefetchSml = (scoping?: SmlSearchScoping, filters?: SmlSearchFilters) => {
const queryClient = useQueryClient();
const { smlService } = useAgentBuilderServices();
const experimentalFeaturesEnabled = useExperimentalFeatures();
Expand All @@ -23,14 +23,14 @@ export const usePrefetchSml = (filters?: SmlSearchFilters) => {
return;
}
queryClient.prefetchQuery({
queryKey: queryKeys.sml.search('*', true, filters),
queryKey: queryKeys.sml.autocomplete('*', scoping, filters),
queryFn: () =>
smlService.search({
smlService.autocomplete({
query: '*',
size: SML_SEARCH_DEFAULT_SIZE,
skipContent: true,
scoping,
filters,
}),
});
}, [experimentalFeaturesEnabled, queryClient, smlService, filters]);
}, [experimentalFeaturesEnabled, queryClient, smlService, scoping, filters]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,35 @@ import { usePrefetchSkills } from './menus/skills/use_prefetch_skills';
import { usePrefetchSml } from './menus/sml/use_prefetch_sml';
import { useAgentId } from '../../../../../hooks/use_conversation';
import { useAgentBuilderAgentById } from '../../../../../hooks/agents/use_agent_by_id';
import { buildSmlFiltersFromAgent } from './utils/sml_filters';
import { buildSmlScopingFromAgent } from './utils/sml_filters';

/**
* Prefetches data for all command menus on first invocation.
* Re-prefetches SML when the agent's filters change (e.g. after the async
* Re-prefetches SML when the agent's scoping changes (e.g. after the async
* agent fetch resolves with connector_ids).
* Returns a callback that should be called when the editor receives focus.
*/
const NOT_YET_PREFETCHED = Symbol('not-yet-prefetched');

export const useCommandMenuPrefetch = () => {
const hasPrefetchedSkills = useRef(false);
const lastSmlFiltersJson = useRef<string | symbol>(NOT_YET_PREFETCHED);
const lastSmlScopingJson = useRef<string | symbol>(NOT_YET_PREFETCHED);
const agentId = useAgentId();
const { agent } = useAgentBuilderAgentById(agentId);
const filters = useMemo(() => buildSmlFiltersFromAgent(agent), [agent]);
const scoping = useMemo(() => buildSmlScopingFromAgent(agent), [agent]);
const prefetchSkills = usePrefetchSkills();
const prefetchSml = usePrefetchSml(filters);
const prefetchSml = usePrefetchSml(scoping);

return useCallback(() => {
if (!hasPrefetchedSkills.current) {
hasPrefetchedSkills.current = true;
prefetchSkills();
}

const filtersJson = filters ? JSON.stringify(filters) : '';
if (filtersJson !== lastSmlFiltersJson.current) {
lastSmlFiltersJson.current = filtersJson;
const scopingJson = scoping ? JSON.stringify(scoping) : '';
if (scopingJson !== lastSmlScopingJson.current) {
lastSmlScopingJson.current = scopingJson;
prefetchSml();
}
}, [prefetchSkills, prefetchSml, filters]);
}, [prefetchSkills, prefetchSml, scoping]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
*/

import type { AgentDefinition } from '@kbn/agent-builder-common/agents/definition';
import type { SmlSearchFilters } from '@kbn/agent-context-layer-plugin/public';
import type { SmlSearchScoping } from '@kbn/agent-context-layer-plugin/public';
import { SmlSearchFilterType } from '@kbn/agent-context-layer-plugin/public';

// Three states: undefined → no filtering (all connectors visible),
// Three states: undefined → no scoping (all connectors visible),
// [] → no connectors allowed, ['id1', ...] → only those connectors.
export const buildSmlFiltersFromAgent = (
export const buildSmlScopingFromAgent = (
agent: AgentDefinition | null
): SmlSearchFilters | undefined => {
): SmlSearchScoping | undefined => {
const connectorIds = agent?.configuration?.connector_ids;
if (connectorIds === undefined) {
return undefined;
Expand Down
Loading